mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-28 22:42:11 +01:00
Compare commits
55 Commits
feature/en
...
0a1f25a1ee
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0a1f25a1ee | ||
|
|
609a7ed6dd | ||
|
|
5bebd3de4d | ||
|
|
b7e69dacf7 | ||
|
|
a8cca9143e | ||
|
|
16b9761573 | ||
|
|
7a86fcf507 | ||
|
|
de3edaa0f9 | ||
|
|
964a6026a0 | ||
|
|
83ad5f526e | ||
|
|
ccc5285602 | ||
|
|
7200eaefbf | ||
|
|
1cc13eebe1 | ||
|
|
39e56a275e | ||
|
|
44426109bd | ||
|
|
bb9e9ff90e | ||
|
|
e5c7c18c40 | ||
|
|
6c41214d69 | ||
|
|
6e55b7b0da | ||
|
|
5711a75188 | ||
|
|
3696fb5b2d | ||
|
|
e3c60f14f7 | ||
|
|
5fe85282e7 | ||
|
|
9a8eac3f9a | ||
|
|
93752efb9d | ||
|
|
0c546802fa | ||
|
|
3ed3d0b466 | ||
|
|
daf79d55a5 | ||
|
|
062a8044f2 | ||
|
|
7e7721b222 | ||
|
|
14be1365bd | ||
|
|
d5324675ef | ||
|
|
86b0493591 | ||
|
|
85f1184648 | ||
|
|
803a53253c | ||
|
|
abcb8e2cb4 | ||
|
|
f10338a48b | ||
|
|
598a77b288 | ||
|
|
aa57d27924 | ||
|
|
6cb9aea7d1 | ||
|
|
fdfb54a3a0 | ||
|
|
5f94549539 | ||
|
|
e5dd1e312d | ||
|
|
aee63711e4 | ||
|
|
a3c865e39c | ||
|
|
68f50b911d | ||
|
|
0670dbfdb1 | ||
|
|
db4f30af86 | ||
|
|
39b945ae88 | ||
|
|
a2b29c0525 | ||
|
|
2c385210db | ||
|
|
46999cc04c | ||
|
|
ee9f030a99 | ||
|
|
5aded6ff8e | ||
|
|
7884e1af32 |
@@ -174,6 +174,42 @@ Example of splitting commits:
|
||||
## 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
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -80,3 +80,6 @@ CLAUDE.md
|
||||
*.pyc
|
||||
.vite
|
||||
reports/
|
||||
|
||||
# Local iPad dev setup (proxy)
|
||||
/local-dev/
|
||||
|
||||
46
CLAUDE.md
46
CLAUDE.md
@@ -112,6 +112,52 @@ angular-template → html-template → logging → tailwind
|
||||
|
||||
Use `/clear` between unrelated tasks to prevent context degradation.
|
||||
|
||||
### Long-Running Task Pattern
|
||||
|
||||
For complex tasks approaching context limits:
|
||||
1. Dump progress to a `.md` file (e.g., `progress.md`)
|
||||
2. Use `/clear` to reset context
|
||||
3. Resume by reading the progress file
|
||||
4. Continue from where you left off
|
||||
|
||||
### Context Monitoring
|
||||
|
||||
- Use `/context` to check current token usage
|
||||
- Fresh session baseline: ~20k tokens
|
||||
- Consider `/compact` when approaching 150k tokens
|
||||
- Prefer `/clear` over `/compact` when starting new topics
|
||||
|
||||
## Extended Thinking
|
||||
|
||||
Use progressive thinking depth for complex analysis:
|
||||
|
||||
| Trigger | Thinking Budget | Use Case |
|
||||
|---------|----------------|----------|
|
||||
| `"think"` | ~4k tokens | Basic analysis, simple decisions |
|
||||
| `"think hard"` | ~10k tokens | Moderate complexity, multi-factor decisions |
|
||||
| `"think harder"` | ~16k tokens | Deep analysis, architectural decisions |
|
||||
| `"ultrathink"` | ~32k tokens | Maximum depth, critical planning |
|
||||
|
||||
**Examples:**
|
||||
- "Think about how to structure this component"
|
||||
- "Think hard about the best approach for state management"
|
||||
- "Ultrathink about the architecture for this feature"
|
||||
|
||||
## Code Investigation (MANDATORY)
|
||||
|
||||
**Never speculate about code you haven't read.**
|
||||
|
||||
If user references a specific file:
|
||||
1. **READ the file first** using the Read tool
|
||||
2. **THEN provide analysis** based on actual contents
|
||||
3. If file doesn't exist, **say so explicitly**
|
||||
|
||||
**Anti-Hallucination Rules:**
|
||||
- Never describe code you haven't opened
|
||||
- Never assume file contents based on names
|
||||
- Never guess API signatures without documentation
|
||||
- Always verify imports and dependencies exist
|
||||
|
||||
## Implementation Decisions
|
||||
|
||||
| Task Type | Required Agent | Escalation Path |
|
||||
|
||||
@@ -3,6 +3,9 @@ import { DevScanAdapter } from './dev.scan-adapter';
|
||||
import { NativeScanAdapter } from './native.scan-adapter';
|
||||
import { SCAN_ADAPTER } from './tokens';
|
||||
|
||||
/**
|
||||
* @deprecated Use '@isa/shared/scanner' instead.
|
||||
*/
|
||||
@NgModule({})
|
||||
export class ScanAdapterModule {
|
||||
static forRoot() {
|
||||
|
||||
@@ -5,6 +5,9 @@ import { ScanditOverlayComponent } from './scandit-overlay.component';
|
||||
import { ScanditScanAdapter } from './scandit.scan-adapter';
|
||||
import { SCAN_ADAPTER } from '../tokens';
|
||||
|
||||
/**
|
||||
* @deprecated Use @isa/shared/scanner instead.
|
||||
*/
|
||||
@NgModule({
|
||||
imports: [CommonModule],
|
||||
exports: [ScanditOverlayComponent],
|
||||
@@ -14,7 +17,9 @@ export class ScanditScanAdapterModule {
|
||||
static forRoot() {
|
||||
return {
|
||||
ngModule: ScanditScanAdapterModule,
|
||||
providers: [{ provide: SCAN_ADAPTER, useClass: ScanditScanAdapter, multi: true }],
|
||||
providers: [
|
||||
{ provide: SCAN_ADAPTER, useClass: ScanditScanAdapter, multi: true },
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import { Config } from '@core/config';
|
||||
import { ComponentPortal } from '@angular/cdk/portal';
|
||||
import { ScanditOverlayComponent } from './scandit-overlay.component';
|
||||
import { EnvironmentService } from '@core/environment';
|
||||
import { injectNetworkStatus$ } from 'apps/isa-app/src/app/services/network-status.service';
|
||||
import { injectNetworkStatus$ } from '@isa/core/connectivity';
|
||||
import { toSignal } from '@angular/core/rxjs-interop';
|
||||
|
||||
@Injectable()
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { DomainAvailabilityModule } from '@domain/availability';
|
||||
import { DomainCatalogModule } from '@domain/catalog';
|
||||
import { DomainIsaModule } from '@domain/isa';
|
||||
import { DomainCheckoutModule } from '@domain/checkout';
|
||||
import { DomainOmsModule } from '@domain/oms';
|
||||
import { DomainRemissionModule } from '@domain/remission';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
DomainIsaModule.forRoot(),
|
||||
DomainCatalogModule.forRoot(),
|
||||
DomainAvailabilityModule.forRoot(),
|
||||
DomainCheckoutModule.forRoot(),
|
||||
DomainOmsModule.forRoot(),
|
||||
DomainRemissionModule.forRoot(),
|
||||
],
|
||||
})
|
||||
export class AppDomainModule {}
|
||||
@@ -1,34 +0,0 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { EffectsModule } from '@ngrx/effects';
|
||||
import { ActionReducer, MetaReducer, StoreModule } from '@ngrx/store';
|
||||
import { StoreDevtoolsModule } from '@ngrx/store-devtools';
|
||||
import { environment } from '../environments/environment';
|
||||
import { rootReducer } from './store/root.reducer';
|
||||
import { RootState } from './store/root.state';
|
||||
|
||||
export function storeInLocalStorage(
|
||||
reducer: ActionReducer<any>,
|
||||
): ActionReducer<any> {
|
||||
return function (state, action) {
|
||||
if (action.type === 'HYDRATE') {
|
||||
return reducer(action['payload'], action);
|
||||
}
|
||||
return reducer(state, action);
|
||||
};
|
||||
}
|
||||
|
||||
export const metaReducers: MetaReducer<RootState>[] = !environment.production
|
||||
? [storeInLocalStorage]
|
||||
: [storeInLocalStorage];
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
StoreModule.forRoot(rootReducer, { metaReducers }),
|
||||
EffectsModule.forRoot([]),
|
||||
StoreDevtoolsModule.instrument({
|
||||
name: 'ISA Ngrx Application Store',
|
||||
connectInZone: true,
|
||||
}),
|
||||
],
|
||||
})
|
||||
export class AppStoreModule {}
|
||||
@@ -1,40 +0,0 @@
|
||||
import { HttpInterceptorFn, provideHttpClient, withInterceptors } from '@angular/common/http';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { Config } from '@core/config';
|
||||
import { AvConfiguration } from '@generated/swagger/availability-api';
|
||||
import { CatConfiguration } from '@generated/swagger/cat-search-api';
|
||||
import { CheckoutConfiguration } from '@generated/swagger/checkout-api';
|
||||
import { CrmConfiguration } from '@generated/swagger/crm-api';
|
||||
import { EisConfiguration } from '@generated/swagger/eis-api';
|
||||
import { IsaConfiguration } from '@generated/swagger/isa-api';
|
||||
import { OmsConfiguration } from '@generated/swagger/oms-api';
|
||||
import { PrintConfiguration } from '@generated/swagger/print-api';
|
||||
import { RemiConfiguration } from '@generated/swagger/inventory-api';
|
||||
import { WwsConfiguration } from '@generated/swagger/wws-api';
|
||||
|
||||
export function createConfigurationFactory(name: string) {
|
||||
return function (config: Config): { rootUrl: string } {
|
||||
return config.get(`@swagger/${name}`);
|
||||
};
|
||||
}
|
||||
|
||||
const serviceWorkerInterceptor: HttpInterceptorFn = (req, next) => {
|
||||
return next(req.clone({ setHeaders: { 'ngsw-bypass': 'true' } }));
|
||||
};
|
||||
|
||||
@NgModule({
|
||||
providers: [
|
||||
provideHttpClient(withInterceptors([serviceWorkerInterceptor])),
|
||||
{ provide: AvConfiguration, useFactory: createConfigurationFactory('av'), deps: [Config] },
|
||||
{ provide: CatConfiguration, useFactory: createConfigurationFactory('cat'), deps: [Config] },
|
||||
{ provide: CheckoutConfiguration, useFactory: createConfigurationFactory('checkout'), deps: [Config] },
|
||||
{ provide: CrmConfiguration, useFactory: createConfigurationFactory('crm'), deps: [Config] },
|
||||
{ provide: EisConfiguration, useFactory: createConfigurationFactory('eis'), deps: [Config] },
|
||||
{ provide: IsaConfiguration, useFactory: createConfigurationFactory('isa'), deps: [Config] },
|
||||
{ provide: OmsConfiguration, useFactory: createConfigurationFactory('oms'), deps: [Config] },
|
||||
{ provide: PrintConfiguration, useFactory: createConfigurationFactory('print'), deps: [Config] },
|
||||
{ provide: RemiConfiguration, useFactory: createConfigurationFactory('remi'), deps: [Config] },
|
||||
{ provide: WwsConfiguration, useFactory: createConfigurationFactory('wws'), deps: [Config] },
|
||||
],
|
||||
})
|
||||
export class AppSwaggerModule {}
|
||||
@@ -1,4 +1,4 @@
|
||||
@if ($offlineBannerVisible()) {
|
||||
<!-- @if ($offlineBannerVisible()) {
|
||||
<div [@fadeInOut] class="bg-brand text-white text-center fixed inset-x-0 top-0 z-tooltip p-4">
|
||||
<h3 class="font-bold grid grid-flow-col items-center justify-center text-xl gap-4">
|
||||
<div>
|
||||
@@ -25,4 +25,4 @@
|
||||
</div>
|
||||
}
|
||||
|
||||
<router-outlet></router-outlet>
|
||||
<router-outlet></router-outlet> -->
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
:host {
|
||||
@apply block;
|
||||
}
|
||||
@@ -1,137 +0,0 @@
|
||||
import { Spectator, createComponentFactory, SpyObject, createSpyObject } from '@ngneat/spectator';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { AppComponent } from './app.component';
|
||||
import { Config } from '@core/config';
|
||||
import { ApplicationService } from '@core/application';
|
||||
import { of } from 'rxjs';
|
||||
import { Renderer2 } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { SwUpdate } from '@angular/service-worker';
|
||||
import { NotificationsHub } from '@hub/notifications';
|
||||
import { UserStateService } from '@generated/swagger/isa-api';
|
||||
import { UiModalService } from '@ui/modal';
|
||||
import { AuthService } from '@core/auth';
|
||||
|
||||
describe('AppComponent', () => {
|
||||
let spectator: Spectator<AppComponent>;
|
||||
let config: SpyObject<Config>;
|
||||
let renderer: SpyObject<Renderer2>;
|
||||
let applicationServiceMock: SpyObject<ApplicationService>;
|
||||
let notificationsHubMock: SpyObject<NotificationsHub>;
|
||||
let swUpdateMock: SpyObject<SwUpdate>;
|
||||
const createComponent = createComponentFactory({
|
||||
component: AppComponent,
|
||||
imports: [CommonModule, RouterTestingModule],
|
||||
providers: [],
|
||||
mocks: [Config, SwUpdate, UserStateService, UiModalService, AuthService],
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
applicationServiceMock = createSpyObject(ApplicationService);
|
||||
applicationServiceMock.getSection$.and.returnValue(of('customer'));
|
||||
applicationServiceMock.getActivatedProcessId$.and.returnValue(of(undefined));
|
||||
renderer = jasmine.createSpyObj('Renderer2', ['addClass', 'removeClass']);
|
||||
|
||||
notificationsHubMock = createSpyObject(NotificationsHub);
|
||||
notificationsHubMock.notifications$ = of({});
|
||||
swUpdateMock = createSpyObject(SwUpdate);
|
||||
|
||||
spectator = createComponent({
|
||||
providers: [
|
||||
{ provide: ApplicationService, useValue: applicationServiceMock },
|
||||
{
|
||||
provide: Renderer2,
|
||||
useValue: renderer,
|
||||
},
|
||||
{ provide: NotificationsHub, useValue: notificationsHubMock },
|
||||
{ provide: SwUpdate, useValue: swUpdateMock },
|
||||
],
|
||||
});
|
||||
config = spectator.inject(Config);
|
||||
});
|
||||
|
||||
it('should create the app', () => {
|
||||
expect(spectator.component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should have a router outlet', () => {
|
||||
expect(spectator.query('router-outlet')).toExist();
|
||||
});
|
||||
|
||||
describe('ngOnInit', () => {
|
||||
it('should call setTitle', () => {
|
||||
const spy = spyOn(spectator.component, 'setTitle');
|
||||
spectator.component.ngOnInit();
|
||||
expect(spy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call logVersion', () => {
|
||||
const spy = spyOn(spectator.component, 'logVersion');
|
||||
spectator.component.ngOnInit();
|
||||
expect(spy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('setTitle', () => {
|
||||
it('should call Title.setTitle()', () => {
|
||||
const spyTitleSetTitle = spyOn(spectator.component['_title'], 'setTitle');
|
||||
config.get.and.returnValue('test');
|
||||
spectator.component.setTitle();
|
||||
expect(spyTitleSetTitle).toHaveBeenCalledWith('test');
|
||||
});
|
||||
});
|
||||
|
||||
describe('logVersion', () => {
|
||||
it('should call console.log()', () => {
|
||||
const spyConsoleLog = spyOn(console, 'log');
|
||||
config.get.and.returnValue('test');
|
||||
spectator.component.logVersion();
|
||||
expect(spyConsoleLog).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
// --------------------------------------------------------
|
||||
// Unit Tests Implementation for Angular Version 13.x.x
|
||||
|
||||
// describe('updateClient()', () => {
|
||||
// it('should call checkForUpdate() if SwUpdate.isEnabled is True', () => {
|
||||
// spyOn(spectator.component, 'checkForUpdate');
|
||||
// spyOn(spectator.component, 'initialCheckForUpdate');
|
||||
// (swUpdateMock as any).isEnabled = true;
|
||||
// spectator.component.updateClient();
|
||||
// expect(spectator.component.initialCheckForUpdate).toHaveBeenCalled();
|
||||
// expect(spectator.component.checkForUpdate).toHaveBeenCalled();
|
||||
// });
|
||||
|
||||
// it('should not call checkForUpdate() if SwUpdate.isEnabled is False', () => {
|
||||
// spyOn(spectator.component, 'checkForUpdate');
|
||||
// spyOn(spectator.component, 'initialCheckForUpdate');
|
||||
// (swUpdateMock as any).isEnabled = false;
|
||||
// spectator.component.updateClient();
|
||||
// expect(spectator.component.initialCheckForUpdate).not.toHaveBeenCalled();
|
||||
// expect(spectator.component.checkForUpdate).not.toHaveBeenCalled();
|
||||
// });
|
||||
// });
|
||||
|
||||
// describe('checkForUpdate', () => {
|
||||
// it('should call swUpdate.checkForUpdate() and notifications.updateNotification() every second', fakeAsync(() => {
|
||||
// swUpdateMock.checkForUpdate.and.returnValue(Promise.resolve());
|
||||
// spectator.component.checkForUpdates = 1000;
|
||||
// spectator.component.checkForUpdate();
|
||||
|
||||
// spectator.detectChanges();
|
||||
// tick(1100);
|
||||
|
||||
// expect(notificationsHubMock.updateNotification).toHaveBeenCalled();
|
||||
// discardPeriodicTasks();
|
||||
// }));
|
||||
// });
|
||||
|
||||
// describe('initialCheckForUpdate', () => {
|
||||
// it('should call swUpdate.checkForUpdate()', () => {
|
||||
// swUpdateMock.checkForUpdate.and.returnValue(new Promise(undefined));
|
||||
// spectator.component.initialCheckForUpdate();
|
||||
// expect(swUpdateMock.checkForUpdate).toHaveBeenCalled();
|
||||
// });
|
||||
// });
|
||||
});
|
||||
@@ -1,206 +1,205 @@
|
||||
// import {
|
||||
// Component,
|
||||
// effect,
|
||||
// HostListener,
|
||||
// inject,
|
||||
// Inject,
|
||||
// Injector,
|
||||
// OnInit,
|
||||
// Renderer2,
|
||||
// signal,
|
||||
// untracked,
|
||||
// DOCUMENT
|
||||
// } from '@angular/core';
|
||||
// import { Title } from '@angular/platform-browser';
|
||||
// import { SwUpdate } from '@angular/service-worker';
|
||||
// import { ApplicationService } from '@core/application';
|
||||
// import { Config } from '@core/config';
|
||||
// import { NotificationsHub } from '@hub/notifications';
|
||||
// import packageInfo from 'packageJson';
|
||||
// import { asapScheduler, interval, Subscription } from 'rxjs';
|
||||
// import { UserStateService } from '@generated/swagger/isa-api';
|
||||
// import { IsaLogProvider } from './providers';
|
||||
// import { EnvironmentService } from '@core/environment';
|
||||
// import { AuthService, LoginStrategy } from '@core/auth';
|
||||
// import { UiMessageModalComponent, UiModalService } from '@ui/modal';
|
||||
// import { injectNetworkStatus } from '@isa/core/connectivity';
|
||||
// import { animate, style, transition, trigger } from '@angular/animations';
|
||||
|
||||
import {
|
||||
Component,
|
||||
effect,
|
||||
HostListener,
|
||||
inject,
|
||||
Inject,
|
||||
Injector,
|
||||
OnInit,
|
||||
Renderer2,
|
||||
signal,
|
||||
untracked,
|
||||
DOCUMENT
|
||||
} from '@angular/core';
|
||||
import { Title } from '@angular/platform-browser';
|
||||
import { SwUpdate } from '@angular/service-worker';
|
||||
import { ApplicationService } from '@core/application';
|
||||
import { Config } from '@core/config';
|
||||
import { NotificationsHub } from '@hub/notifications';
|
||||
import packageInfo from 'packageJson';
|
||||
import { asapScheduler, interval, Subscription } from 'rxjs';
|
||||
import { UserStateService } from '@generated/swagger/isa-api';
|
||||
import { IsaLogProvider } from './providers';
|
||||
import { EnvironmentService } from '@core/environment';
|
||||
import { AuthService, LoginStrategy } from '@core/auth';
|
||||
import { UiMessageModalComponent, UiModalService } from '@ui/modal';
|
||||
import { injectOnline$ } from './services/network-status.service';
|
||||
import { toSignal } from '@angular/core/rxjs-interop';
|
||||
import { animate, style, transition, trigger } from '@angular/animations';
|
||||
// @Component({
|
||||
// selector: 'app-root',
|
||||
// templateUrl: './app.component.html',
|
||||
// styleUrls: ['./app.component.scss'],
|
||||
// animations: [
|
||||
// trigger('fadeInOut', [
|
||||
// transition(':enter', [
|
||||
// // :enter wird ausgelöst, wenn das Element zum DOM hinzugefügt wird
|
||||
// style({ opacity: 0, transform: 'translateY(-100%)' }),
|
||||
// animate('300ms', style({ opacity: 1, transform: 'translateY(0)' })),
|
||||
// ]),
|
||||
// transition(':leave', [
|
||||
// // :leave wird ausgelöst, wenn das Element aus dem DOM entfernt wird
|
||||
// animate('300ms', style({ opacity: 0, transform: 'translateY(-100%)' })),
|
||||
// ]),
|
||||
// ]),
|
||||
// ],
|
||||
// standalone: false,
|
||||
// })
|
||||
// export class AppComponent implements OnInit {
|
||||
// readonly injector = inject(Injector);
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
templateUrl: './app.component.html',
|
||||
styleUrls: ['./app.component.scss'],
|
||||
animations: [
|
||||
trigger('fadeInOut', [
|
||||
transition(':enter', [
|
||||
// :enter wird ausgelöst, wenn das Element zum DOM hinzugefügt wird
|
||||
style({ opacity: 0, transform: 'translateY(-100%)' }),
|
||||
animate('300ms', style({ opacity: 1, transform: 'translateY(0)' })),
|
||||
]),
|
||||
transition(':leave', [
|
||||
// :leave wird ausgelöst, wenn das Element aus dem DOM entfernt wird
|
||||
animate('300ms', style({ opacity: 0, transform: 'translateY(-100%)' })),
|
||||
]),
|
||||
]),
|
||||
],
|
||||
standalone: false,
|
||||
})
|
||||
export class AppComponent implements OnInit {
|
||||
readonly injector = inject(Injector);
|
||||
// $networkStatus = injectNetworkStatus();
|
||||
|
||||
$online = toSignal(injectOnline$());
|
||||
// $offlineBannerVisible = signal(false);
|
||||
|
||||
$offlineBannerVisible = signal(false);
|
||||
// $onlineBannerVisible = signal(false);
|
||||
|
||||
$onlineBannerVisible = signal(false);
|
||||
// private onlineBannerDismissTimeout: any;
|
||||
|
||||
private onlineBannerDismissTimeout: any;
|
||||
// onlineEffects = effect(() => {
|
||||
// const status = this.$networkStatus();
|
||||
// const online = status === 'online';
|
||||
// const offlineBannerVisible = this.$offlineBannerVisible();
|
||||
|
||||
onlineEffects = effect(() => {
|
||||
const online = this.$online();
|
||||
const offlineBannerVisible = this.$offlineBannerVisible();
|
||||
// untracked(() => {
|
||||
// this.$offlineBannerVisible.set(!online);
|
||||
|
||||
untracked(() => {
|
||||
this.$offlineBannerVisible.set(!online);
|
||||
// if (!online) {
|
||||
// this.$onlineBannerVisible.set(false);
|
||||
// clearTimeout(this.onlineBannerDismissTimeout);
|
||||
// }
|
||||
|
||||
if (!online) {
|
||||
this.$onlineBannerVisible.set(false);
|
||||
clearTimeout(this.onlineBannerDismissTimeout);
|
||||
}
|
||||
// if (offlineBannerVisible && online) {
|
||||
// this.$onlineBannerVisible.set(true);
|
||||
// this.onlineBannerDismissTimeout = setTimeout(() => this.$onlineBannerVisible.set(false), 5000);
|
||||
// }
|
||||
// });
|
||||
// });
|
||||
|
||||
if (offlineBannerVisible && online) {
|
||||
this.$onlineBannerVisible.set(true);
|
||||
this.onlineBannerDismissTimeout = setTimeout(() => this.$onlineBannerVisible.set(false), 5000);
|
||||
}
|
||||
});
|
||||
});
|
||||
// private _checkForUpdates: number = this._config.get('checkForUpdates');
|
||||
|
||||
private _checkForUpdates: number = this._config.get('checkForUpdates');
|
||||
// get checkForUpdates(): number {
|
||||
// return this._checkForUpdates ?? 60 * 60 * 1000; // default 1 hour
|
||||
// }
|
||||
|
||||
get checkForUpdates(): number {
|
||||
return this._checkForUpdates ?? 60 * 60 * 1000; // default 1 hour
|
||||
}
|
||||
// // For Unit Testing
|
||||
// set checkForUpdates(time: number) {
|
||||
// this._checkForUpdates = time;
|
||||
// }
|
||||
|
||||
// For Unit Testing
|
||||
set checkForUpdates(time: number) {
|
||||
this._checkForUpdates = time;
|
||||
}
|
||||
// subscriptions = new Subscription();
|
||||
|
||||
subscriptions = new Subscription();
|
||||
// constructor(
|
||||
// private readonly _config: Config,
|
||||
// private readonly _title: Title,
|
||||
// private readonly _appService: ApplicationService,
|
||||
// @Inject(DOCUMENT) private readonly _document: Document,
|
||||
// private readonly _renderer: Renderer2,
|
||||
// private readonly _swUpdate: SwUpdate,
|
||||
// private readonly _notifications: NotificationsHub,
|
||||
// private infoService: UserStateService,
|
||||
// private readonly _environment: EnvironmentService,
|
||||
// private readonly _authService: AuthService,
|
||||
// private readonly _modal: UiModalService,
|
||||
// ) {
|
||||
// this.updateClient();
|
||||
// IsaLogProvider.InfoService = this.infoService;
|
||||
// }
|
||||
|
||||
constructor(
|
||||
private readonly _config: Config,
|
||||
private readonly _title: Title,
|
||||
private readonly _appService: ApplicationService,
|
||||
@Inject(DOCUMENT) private readonly _document: Document,
|
||||
private readonly _renderer: Renderer2,
|
||||
private readonly _swUpdate: SwUpdate,
|
||||
private readonly _notifications: NotificationsHub,
|
||||
private infoService: UserStateService,
|
||||
private readonly _environment: EnvironmentService,
|
||||
private readonly _authService: AuthService,
|
||||
private readonly _modal: UiModalService,
|
||||
) {
|
||||
this.updateClient();
|
||||
IsaLogProvider.InfoService = this.infoService;
|
||||
}
|
||||
// ngOnInit() {
|
||||
// this.setTitle();
|
||||
// this.logVersion();
|
||||
// asapScheduler.schedule(() => this.determinePlatform(), 250);
|
||||
// this._appService.getSection$().subscribe(this.sectionChangeHandler.bind(this));
|
||||
|
||||
ngOnInit() {
|
||||
this.setTitle();
|
||||
this.logVersion();
|
||||
asapScheduler.schedule(() => this.determinePlatform(), 250);
|
||||
this._appService.getSection$().subscribe(this.sectionChangeHandler.bind(this));
|
||||
// this.setupSilentRefresh();
|
||||
// }
|
||||
|
||||
this.setupSilentRefresh();
|
||||
}
|
||||
// // Setup interval for silent refresh
|
||||
// setupSilentRefresh() {
|
||||
// const silentRefreshInterval = this._config.get('silentRefresh.interval');
|
||||
// if (silentRefreshInterval > 0) {
|
||||
// interval(silentRefreshInterval).subscribe(() => {
|
||||
// if (this._authService.isAuthenticated()) {
|
||||
// this._authService.refresh();
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
|
||||
// Setup interval for silent refresh
|
||||
setupSilentRefresh() {
|
||||
const silentRefreshInterval = this._config.get('silentRefresh.interval');
|
||||
if (silentRefreshInterval > 0) {
|
||||
interval(silentRefreshInterval).subscribe(() => {
|
||||
if (this._authService.isAuthenticated()) {
|
||||
this._authService.refresh();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
// setTitle() {
|
||||
// this._title.setTitle(this._config.get('title'));
|
||||
// }
|
||||
|
||||
setTitle() {
|
||||
this._title.setTitle(this._config.get('title'));
|
||||
}
|
||||
// logVersion() {
|
||||
// console.log(
|
||||
// `%c${this._config.get('title')}\r\nVersion: ${packageInfo.version}`,
|
||||
// 'font-weight: bold; font-size: 20px;',
|
||||
// );
|
||||
// }
|
||||
|
||||
logVersion() {
|
||||
console.log(
|
||||
`%c${this._config.get('title')}\r\nVersion: ${packageInfo.version}`,
|
||||
'font-weight: bold; font-size: 20px;',
|
||||
);
|
||||
}
|
||||
// determinePlatform() {
|
||||
// if (this._environment.isNative()) {
|
||||
// this._renderer.addClass(this._document.body, 'tablet-native');
|
||||
// } else if (this._environment.isTablet()) {
|
||||
// this._renderer.addClass(this._document.body, 'tablet-browser');
|
||||
// }
|
||||
// if (this._environment.isTablet()) {
|
||||
// this._renderer.addClass(this._document.body, 'tablet');
|
||||
// }
|
||||
// if (this._environment.isDesktop()) {
|
||||
// this._renderer.addClass(this._document.body, 'desktop');
|
||||
// }
|
||||
// }
|
||||
|
||||
determinePlatform() {
|
||||
if (this._environment.isNative()) {
|
||||
this._renderer.addClass(this._document.body, 'tablet-native');
|
||||
} else if (this._environment.isTablet()) {
|
||||
this._renderer.addClass(this._document.body, 'tablet-browser');
|
||||
}
|
||||
if (this._environment.isTablet()) {
|
||||
this._renderer.addClass(this._document.body, 'tablet');
|
||||
}
|
||||
if (this._environment.isDesktop()) {
|
||||
this._renderer.addClass(this._document.body, 'desktop');
|
||||
}
|
||||
}
|
||||
// sectionChangeHandler(section: string) {
|
||||
// if (section === 'customer') {
|
||||
// this._renderer.removeClass(this._document.body, 'branch');
|
||||
// this._renderer.addClass(this._document.body, 'customer');
|
||||
// } else if (section === 'branch') {
|
||||
// this._renderer.removeClass(this._document.body, 'customer');
|
||||
// this._renderer.addClass(this._document.body, 'branch');
|
||||
// }
|
||||
// }
|
||||
|
||||
sectionChangeHandler(section: string) {
|
||||
if (section === 'customer') {
|
||||
this._renderer.removeClass(this._document.body, 'branch');
|
||||
this._renderer.addClass(this._document.body, 'customer');
|
||||
} else if (section === 'branch') {
|
||||
this._renderer.removeClass(this._document.body, 'customer');
|
||||
this._renderer.addClass(this._document.body, 'branch');
|
||||
}
|
||||
}
|
||||
// updateClient() {
|
||||
// if (!this._swUpdate.isEnabled) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
updateClient() {
|
||||
if (!this._swUpdate.isEnabled) {
|
||||
return;
|
||||
}
|
||||
// this.initialCheckForUpdate();
|
||||
// this.checkForUpdate();
|
||||
// }
|
||||
|
||||
this.initialCheckForUpdate();
|
||||
this.checkForUpdate();
|
||||
}
|
||||
// checkForUpdate() {
|
||||
// interval(this._checkForUpdates).subscribe(() => {
|
||||
// this._swUpdate.checkForUpdate().then((value) => {
|
||||
// console.log('check for update', value);
|
||||
// if (value) {
|
||||
// this._notifications.updateNotification();
|
||||
// }
|
||||
// });
|
||||
// });
|
||||
// }
|
||||
|
||||
checkForUpdate() {
|
||||
interval(this._checkForUpdates).subscribe(() => {
|
||||
this._swUpdate.checkForUpdate().then((value) => {
|
||||
console.log('check for update', value);
|
||||
if (value) {
|
||||
this._notifications.updateNotification();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
// initialCheckForUpdate() {
|
||||
// this._swUpdate.checkForUpdate().then((value) => {
|
||||
// console.log('initial check for update', value);
|
||||
// if (value) {
|
||||
// location.reload();
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
|
||||
initialCheckForUpdate() {
|
||||
this._swUpdate.checkForUpdate().then((value) => {
|
||||
console.log('initial check for update', value);
|
||||
if (value) {
|
||||
location.reload();
|
||||
}
|
||||
});
|
||||
}
|
||||
// @HostListener('window:visibilitychange', ['$event'])
|
||||
// onVisibilityChange(event: Event) {
|
||||
// // refresh token when app is in background
|
||||
// if (this._document.hidden && this._authService.isAuthenticated()) {
|
||||
// this._authService.refresh();
|
||||
// } else if (!this._authService.isAuthenticated()) {
|
||||
// const strategy = this.injector.get(LoginStrategy);
|
||||
|
||||
@HostListener('window:visibilitychange', ['$event'])
|
||||
onVisibilityChange(event: Event) {
|
||||
// refresh token when app is in background
|
||||
if (this._document.hidden && this._authService.isAuthenticated()) {
|
||||
this._authService.refresh();
|
||||
} else if (!this._authService.isAuthenticated()) {
|
||||
const strategy = this.injector.get(LoginStrategy);
|
||||
|
||||
return strategy.login('Sie sind nicht mehr angemeldet');
|
||||
}
|
||||
}
|
||||
}
|
||||
// return strategy.login('Sie sind nicht mehr angemeldet');
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
@@ -1,51 +1,52 @@
|
||||
import { version } from '../../../../package.json';
|
||||
import { IsaTitleStrategy } from '@isa/common/title-management';
|
||||
import {
|
||||
HTTP_INTERCEPTORS,
|
||||
HttpInterceptorFn,
|
||||
provideHttpClient,
|
||||
withInterceptors,
|
||||
withInterceptorsFromDi,
|
||||
} from '@angular/common/http';
|
||||
import {
|
||||
ApplicationConfig,
|
||||
DEFAULT_CURRENCY_CODE,
|
||||
ErrorHandler,
|
||||
importProvidersFrom,
|
||||
Injector,
|
||||
LOCALE_ID,
|
||||
NgModule,
|
||||
inject,
|
||||
provideAppInitializer,
|
||||
provideZoneChangeDetection,
|
||||
signal,
|
||||
isDevMode,
|
||||
} from '@angular/core';
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { PlatformModule } from '@angular/cdk/platform';
|
||||
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
|
||||
import {
|
||||
provideRouter,
|
||||
TitleStrategy,
|
||||
withComponentInputBinding,
|
||||
} from '@angular/router';
|
||||
import { ActionReducer, MetaReducer, provideStore } from '@ngrx/store';
|
||||
import { provideStoreDevtools } from '@ngrx/store-devtools';
|
||||
|
||||
import { Config } from '@core/config';
|
||||
import { AuthModule, AuthService, LoginStrategy } from '@core/auth';
|
||||
import { CoreCommandModule } from '@core/command';
|
||||
|
||||
import { AppRoutingModule } from './app-routing.module';
|
||||
import { AppComponent } from './app.component';
|
||||
import {
|
||||
ApplicationService,
|
||||
ApplicationServiceAdapter,
|
||||
CoreApplicationModule,
|
||||
} from '@core/application';
|
||||
import { AppStoreModule } from './app-store.module';
|
||||
import { routes } from './app.routes';
|
||||
|
||||
import { rootReducer } from './store/root.reducer';
|
||||
import { RootState } from './store/root.state';
|
||||
import { ServiceWorkerModule } from '@angular/service-worker';
|
||||
import { environment } from '../environments/environment';
|
||||
import { AppSwaggerModule } from './app-swagger.module';
|
||||
import { AppDomainModule } from './app-domain.module';
|
||||
import { UiModalModule } from '@ui/modal';
|
||||
import {
|
||||
NotificationsHubModule,
|
||||
NOTIFICATIONS_HUB_OPTIONS,
|
||||
} from '@hub/notifications';
|
||||
import { SignalRHubOptions } from '@core/signalr';
|
||||
import { CoreBreadcrumbModule } from '@core/breadcrumb';
|
||||
import { provideCoreBreadcrumb } from '@core/breadcrumb';
|
||||
import { UiCommonModule } from '@ui/common';
|
||||
import { registerLocaleData } from '@angular/common';
|
||||
|
||||
import localeDe from '@angular/common/locales/de';
|
||||
import localeDeExtra from '@angular/common/locales/extra/de';
|
||||
import { HttpErrorInterceptor } from './interceptors';
|
||||
import { CoreLoggerModule, LOG_PROVIDER } from '@core/logger';
|
||||
import { IsaLogProvider } from './providers';
|
||||
@@ -56,10 +57,8 @@ 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';
|
||||
import { IconModule } from '@shared/components/icon';
|
||||
import { NgIconsModule } from '@ng-icons/core';
|
||||
import {
|
||||
@@ -67,10 +66,9 @@ import {
|
||||
matWifi,
|
||||
matWifiOff,
|
||||
} from '@ng-icons/material-icons/baseline';
|
||||
import { NetworkStatusService } from './services/network-status.service';
|
||||
import { NetworkStatusService } from '@isa/core/connectivity';
|
||||
import { debounceTime, filter, firstValueFrom, switchMap } from 'rxjs';
|
||||
import { provideMatomo } from 'ngx-matomo-client';
|
||||
import { withRouter, withRouteData } from 'ngx-matomo-client';
|
||||
import { provideMatomo, withRouter, withRouteData } from 'ngx-matomo-client';
|
||||
import {
|
||||
provideLogging,
|
||||
withLogLevel,
|
||||
@@ -87,14 +85,58 @@ import {
|
||||
import { Store } from '@ngrx/store';
|
||||
import { OAuthService } from 'angular-oauth2-oidc';
|
||||
import z from 'zod';
|
||||
import { provideScrollPositionRestoration } from '@isa/utils/scroll-position';
|
||||
import { TabNavigationService } from '@isa/core/tabs';
|
||||
|
||||
registerLocaleData(localeDe, localeDeExtra);
|
||||
registerLocaleData(localeDe, 'de', localeDeExtra);
|
||||
// Domain modules
|
||||
import { provideDomainCheckout } from '@domain/checkout';
|
||||
|
||||
export function _appInitializerFactory(config: Config, injector: Injector) {
|
||||
// Swagger API configurations
|
||||
import { AvConfiguration } from '@generated/swagger/availability-api';
|
||||
import { CatConfiguration } from '@generated/swagger/cat-search-api';
|
||||
import { CheckoutConfiguration } from '@generated/swagger/checkout-api';
|
||||
import { CrmConfiguration } from '@generated/swagger/crm-api';
|
||||
import { EisConfiguration } from '@generated/swagger/eis-api';
|
||||
import { IsaConfiguration } from '@generated/swagger/isa-api';
|
||||
import { OmsConfiguration } from '@generated/swagger/oms-api';
|
||||
import { PrintConfiguration } from '@generated/swagger/print-api';
|
||||
import { RemiConfiguration } from '@generated/swagger/inventory-api';
|
||||
import { WwsConfiguration } from '@generated/swagger/wws-api';
|
||||
import { UiIconModule } from '@ui/icon';
|
||||
|
||||
// --- Store Configuration ---
|
||||
|
||||
function storeHydrateMetaReducer(
|
||||
reducer: ActionReducer<RootState>,
|
||||
): ActionReducer<RootState> {
|
||||
return function (state, action) {
|
||||
if (action.type === 'HYDRATE') {
|
||||
return reducer(action['payload'], action);
|
||||
}
|
||||
return reducer(state, action);
|
||||
};
|
||||
}
|
||||
|
||||
const metaReducers: MetaReducer<RootState>[] = [storeHydrateMetaReducer];
|
||||
|
||||
// --- Swagger Configuration ---
|
||||
|
||||
const swaggerConfigSchema = z.object({ rootUrl: z.string() });
|
||||
|
||||
function createSwaggerConfigFactory(name: string) {
|
||||
return function () {
|
||||
return inject(Config).get(`@swagger/${name}`, swaggerConfigSchema);
|
||||
};
|
||||
}
|
||||
|
||||
const serviceWorkerBypassInterceptor: HttpInterceptorFn = (req, next) => {
|
||||
return next(req.clone({ setHeaders: { 'ngsw-bypass': 'true' } }));
|
||||
};
|
||||
|
||||
// --- App Initializer ---
|
||||
|
||||
function appInitializerFactory(_config: Config, injector: Injector) {
|
||||
return async () => {
|
||||
// Get logging service for initialization logging
|
||||
const logger = loggerFactory(() => ({ service: 'AppInitializer' }));
|
||||
const statusElement = document.querySelector('#init-status');
|
||||
const laoderElement = document.querySelector('#init-loader');
|
||||
@@ -105,7 +147,8 @@ export function _appInitializerFactory(config: Config, injector: Injector) {
|
||||
let online = false;
|
||||
const networkStatus = injector.get(NetworkStatusService);
|
||||
while (!online) {
|
||||
online = await firstValueFrom(networkStatus.online$);
|
||||
const status = await firstValueFrom(networkStatus.status$);
|
||||
online = status === 'online';
|
||||
|
||||
if (!online) {
|
||||
logger.warn('Waiting for network connection');
|
||||
@@ -160,7 +203,6 @@ export function _appInitializerFactory(config: Config, injector: Injector) {
|
||||
await userStorage.init();
|
||||
|
||||
const store = injector.get(Store);
|
||||
// Hydrate Ngrx Store
|
||||
const state = userStorage.get('store');
|
||||
if (state && state['version'] === version) {
|
||||
store.dispatch({ type: 'HYDRATE', payload: userStorage.get('store') });
|
||||
@@ -170,7 +212,7 @@ export function _appInitializerFactory(config: Config, injector: Injector) {
|
||||
reason: state ? 'version mismatch' : 'no stored state',
|
||||
}));
|
||||
}
|
||||
// Subscribe on Store changes and save to user storage
|
||||
|
||||
auth.initialized$
|
||||
.pipe(
|
||||
filter((initialized) => initialized),
|
||||
@@ -181,7 +223,6 @@ export function _appInitializerFactory(config: Config, injector: Injector) {
|
||||
});
|
||||
|
||||
logger.info('Application initialization completed');
|
||||
// Inject tab navigation service to initialize it
|
||||
injector.get(TabNavigationService).init();
|
||||
} catch (error) {
|
||||
logger.error('Application initialization failed', error as Error, () => ({
|
||||
@@ -222,7 +263,7 @@ export function _appInitializerFactory(config: Config, injector: Injector) {
|
||||
};
|
||||
}
|
||||
|
||||
export function _notificationsHubOptionsFactory(
|
||||
function notificationsHubOptionsFactory(
|
||||
config: Config,
|
||||
auth: AuthService,
|
||||
): SignalRHubOptions {
|
||||
@@ -256,80 +297,151 @@ const USER_SUB_FACTORY = () => {
|
||||
return signal(validation.data);
|
||||
};
|
||||
|
||||
@NgModule({
|
||||
declarations: [AppComponent, MainComponent],
|
||||
bootstrap: [AppComponent],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
BrowserAnimationsModule,
|
||||
ShellModule.forRoot(),
|
||||
AppRoutingModule,
|
||||
AppSwaggerModule,
|
||||
AppDomainModule,
|
||||
CoreBreadcrumbModule.forRoot(),
|
||||
CoreCommandModule.forRoot(Object.values(Commands)),
|
||||
CoreLoggerModule.forRoot(),
|
||||
AppStoreModule,
|
||||
PreviewComponent,
|
||||
AuthModule.forRoot(),
|
||||
CoreApplicationModule.forRoot(),
|
||||
UiModalModule.forRoot(),
|
||||
UiCommonModule.forRoot(),
|
||||
NotificationsHubModule.forRoot(),
|
||||
ServiceWorkerModule.register('ngsw-worker.js', {
|
||||
enabled: environment.production,
|
||||
registrationStrategy: 'registerWhenStable:30000',
|
||||
}),
|
||||
ScanAdapterModule.forRoot(),
|
||||
ScanditScanAdapterModule.forRoot(),
|
||||
PlatformModule,
|
||||
IconModule.forRoot(),
|
||||
NgIconsModule.withIcons({ matWifiOff, matClose, matWifi }),
|
||||
],
|
||||
export const appConfig: ApplicationConfig = {
|
||||
providers: [
|
||||
provideZoneChangeDetection({ eventCoalescing: true }),
|
||||
provideAnimationsAsync('animations'),
|
||||
provideRouter(routes, withComponentInputBinding()),
|
||||
provideHttpClient(
|
||||
withInterceptorsFromDi(),
|
||||
withInterceptors([serviceWorkerBypassInterceptor]),
|
||||
),
|
||||
provideScrollPositionRestoration(),
|
||||
|
||||
// NgRx Store
|
||||
provideStore(rootReducer, { metaReducers }),
|
||||
provideCoreBreadcrumb(),
|
||||
provideDomainCheckout(),
|
||||
provideStoreDevtools({
|
||||
name: 'ISA Ngrx Application Store',
|
||||
connectInZone: true,
|
||||
}),
|
||||
|
||||
// Swagger API configurations
|
||||
{
|
||||
provide: AvConfiguration,
|
||||
useFactory: createSwaggerConfigFactory('av'),
|
||||
},
|
||||
{
|
||||
provide: CatConfiguration,
|
||||
useFactory: createSwaggerConfigFactory('cat'),
|
||||
},
|
||||
{
|
||||
provide: CheckoutConfiguration,
|
||||
useFactory: createSwaggerConfigFactory('checkout'),
|
||||
},
|
||||
{
|
||||
provide: CrmConfiguration,
|
||||
useFactory: createSwaggerConfigFactory('crm'),
|
||||
},
|
||||
{
|
||||
provide: EisConfiguration,
|
||||
useFactory: createSwaggerConfigFactory('eis'),
|
||||
},
|
||||
{
|
||||
provide: IsaConfiguration,
|
||||
useFactory: createSwaggerConfigFactory('isa'),
|
||||
},
|
||||
{
|
||||
provide: OmsConfiguration,
|
||||
useFactory: createSwaggerConfigFactory('oms'),
|
||||
},
|
||||
{
|
||||
provide: PrintConfiguration,
|
||||
useFactory: createSwaggerConfigFactory('print'),
|
||||
},
|
||||
{
|
||||
provide: RemiConfiguration,
|
||||
useFactory: createSwaggerConfigFactory('remi'),
|
||||
},
|
||||
{
|
||||
provide: WwsConfiguration,
|
||||
useFactory: createSwaggerConfigFactory('wws'),
|
||||
},
|
||||
|
||||
// App initializer
|
||||
provideAppInitializer(() => {
|
||||
const initializerFn = _appInitializerFactory(
|
||||
const initializerFn = appInitializerFactory(
|
||||
inject(Config),
|
||||
inject(Injector),
|
||||
);
|
||||
return initializerFn();
|
||||
}),
|
||||
|
||||
// Notifications hub
|
||||
{
|
||||
provide: NOTIFICATIONS_HUB_OPTIONS,
|
||||
useFactory: _notificationsHubOptionsFactory,
|
||||
useFactory: notificationsHubOptionsFactory,
|
||||
deps: [Config, AuthService],
|
||||
},
|
||||
|
||||
// HTTP interceptors
|
||||
{
|
||||
provide: HTTP_INTERCEPTORS,
|
||||
useClass: HttpErrorInterceptor,
|
||||
multi: true,
|
||||
},
|
||||
|
||||
// Logging
|
||||
{
|
||||
provide: LOG_PROVIDER,
|
||||
useClass: IsaLogProvider,
|
||||
multi: true,
|
||||
},
|
||||
provideLogging(
|
||||
withLogLevel(isDevMode() ? LogLevel.Debug : LogLevel.Info),
|
||||
withSink(ConsoleLogSink),
|
||||
),
|
||||
|
||||
// Error handling
|
||||
{
|
||||
provide: ErrorHandler,
|
||||
useClass: IsaErrorHandler,
|
||||
},
|
||||
{
|
||||
provide: ApplicationService,
|
||||
useClass: ApplicationServiceAdapter,
|
||||
},
|
||||
|
||||
// Locale settings
|
||||
{ provide: LOCALE_ID, useValue: 'de-DE' },
|
||||
provideHttpClient(withInterceptorsFromDi()),
|
||||
{ provide: DEFAULT_CURRENCY_CODE, useValue: 'EUR' },
|
||||
|
||||
// Analytics
|
||||
provideMatomo(
|
||||
{ trackerUrl: 'https://matomo.paragon-data.net', siteId: '1' },
|
||||
withRouter(),
|
||||
withRouteData(),
|
||||
),
|
||||
provideLogging(withLogLevel(LogLevel.Debug), withSink(ConsoleLogSink)),
|
||||
{
|
||||
provide: DEFAULT_CURRENCY_CODE,
|
||||
useValue: 'EUR',
|
||||
},
|
||||
|
||||
// User storage
|
||||
provideUserSubFactory(USER_SUB_FACTORY),
|
||||
|
||||
// Title strategy
|
||||
{ provide: TitleStrategy, useClass: IsaTitleStrategy },
|
||||
|
||||
// Import providers from NgModules
|
||||
importProvidersFrom(
|
||||
// Core modules
|
||||
CoreCommandModule.forRoot(Object.values(Commands)),
|
||||
CoreLoggerModule.forRoot(),
|
||||
AuthModule.forRoot(),
|
||||
|
||||
// UI modules
|
||||
UiModalModule.forRoot(),
|
||||
UiCommonModule.forRoot(),
|
||||
|
||||
// Hub modules
|
||||
NotificationsHubModule.forRoot(),
|
||||
|
||||
// Service Worker
|
||||
ServiceWorkerModule.register('ngsw-worker.js', {
|
||||
enabled: environment.production,
|
||||
registrationStrategy: 'registerWhenStable:30000',
|
||||
}),
|
||||
|
||||
// Scan adapter
|
||||
ScanAdapterModule.forRoot(),
|
||||
ScanditScanAdapterModule.forRoot(),
|
||||
|
||||
UiIconModule.forRoot(),
|
||||
IconModule.forRoot(),
|
||||
),
|
||||
],
|
||||
})
|
||||
export class AppModule {}
|
||||
};
|
||||
0
apps/isa-app/src/app/app.css
Normal file
0
apps/isa-app/src/app/app.css
Normal file
3
apps/isa-app/src/app/app.html
Normal file
3
apps/isa-app/src/app/app.html
Normal file
@@ -0,0 +1,3 @@
|
||||
<shell-layout>
|
||||
<router-outlet />
|
||||
</shell-layout>
|
||||
@@ -1,5 +1,4 @@
|
||||
import { isDevMode, NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { Routes } from '@angular/router';
|
||||
import {
|
||||
CanActivateCartGuard,
|
||||
CanActivateCartWithProcessIdGuard,
|
||||
@@ -7,23 +6,16 @@ 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 { TokenLoginComponent } from './token-login';
|
||||
import {
|
||||
ActivateProcessIdGuard,
|
||||
ActivateProcessIdWithConfigKeyGuard,
|
||||
@@ -34,9 +26,8 @@ import {
|
||||
processResolverFn,
|
||||
hasTabIdGuard,
|
||||
} from '@isa/core/tabs';
|
||||
import { provideScrollPositionRestoration } from '@isa/utils/scroll-position';
|
||||
|
||||
const routes: Routes = [
|
||||
export const routes: Routes = [
|
||||
{ path: '', redirectTo: 'kunde/dashboard', pathMatch: 'full' },
|
||||
{
|
||||
path: 'login',
|
||||
@@ -51,7 +42,6 @@ const routes: Routes = [
|
||||
children: [
|
||||
{
|
||||
path: 'kunde',
|
||||
component: MainComponent,
|
||||
children: [
|
||||
{
|
||||
path: 'dashboard',
|
||||
@@ -74,7 +64,9 @@ const routes: Routes = [
|
||||
loadChildren: () =>
|
||||
import('@page/catalog').then((m) => m.PageCatalogModule),
|
||||
canActivate: [CanActivateProductWithProcessIdGuard],
|
||||
resolve: { processId: ProcessIdResolver },
|
||||
resolve: {
|
||||
processId: ProcessIdResolver,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'order',
|
||||
@@ -87,7 +79,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 +94,9 @@ const routes: Routes = [
|
||||
loadChildren: () =>
|
||||
import('@page/customer').then((m) => m.CustomerModule),
|
||||
canActivate: [CanActivateCustomerWithProcessIdGuard],
|
||||
resolve: { processId: ProcessIdResolver },
|
||||
resolve: {
|
||||
processId: ProcessIdResolver,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'cart',
|
||||
@@ -113,11 +109,13 @@ const routes: Routes = [
|
||||
loadChildren: () =>
|
||||
import('@page/checkout').then((m) => m.PageCheckoutModule),
|
||||
canActivate: [CanActivateCartWithProcessIdGuard],
|
||||
resolve: {
|
||||
processId: ProcessIdResolver,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'pickup-shelf',
|
||||
canActivate: [ProcessIdGuard],
|
||||
// NOTE: This is a workaround for the canActivate guard not being called
|
||||
canActivate: [ActivateProcessIdGuard],
|
||||
loadChildren: () =>
|
||||
import('@page/pickup-shelf').then((m) => m.PickupShelfOutModule),
|
||||
},
|
||||
@@ -126,6 +124,9 @@ const routes: Routes = [
|
||||
canActivate: [ActivateProcessIdGuard],
|
||||
loadChildren: () =>
|
||||
import('@page/pickup-shelf').then((m) => m.PickupShelfOutModule),
|
||||
resolve: {
|
||||
processId: ProcessIdResolver,
|
||||
},
|
||||
},
|
||||
{ path: '**', redirectTo: 'dashboard', pathMatch: 'full' },
|
||||
],
|
||||
@@ -133,7 +134,6 @@ const routes: Routes = [
|
||||
},
|
||||
{
|
||||
path: 'filiale',
|
||||
component: MainComponent,
|
||||
children: [
|
||||
{
|
||||
path: 'task-calendar',
|
||||
@@ -141,12 +141,11 @@ const routes: Routes = [
|
||||
import('@page/task-calendar').then(
|
||||
(m) => m.PageTaskCalendarModule,
|
||||
),
|
||||
canActivate: [CanActivateTaskCalendarGuard],
|
||||
canActivate: [ActivateProcessIdWithConfigKeyGuard('taskCalendar')],
|
||||
},
|
||||
{
|
||||
path: 'pickup-shelf',
|
||||
canActivate: [ActivateProcessIdWithConfigKeyGuard('pickupShelf')],
|
||||
// NOTE: This is a workaround for the canActivate guard not being called
|
||||
loadChildren: () =>
|
||||
import('@page/pickup-shelf').then((m) => m.PickupShelfInModule),
|
||||
},
|
||||
@@ -154,27 +153,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' },
|
||||
],
|
||||
@@ -184,7 +179,6 @@ const routes: Routes = [
|
||||
},
|
||||
{
|
||||
path: ':tabId',
|
||||
component: MainComponent,
|
||||
resolve: { process: processResolverFn, tab: tabResolverFn },
|
||||
canActivate: [IsAuthenticatedGuard, hasTabIdGuard],
|
||||
children: [
|
||||
@@ -214,7 +208,6 @@ const routes: Routes = [
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
path: 'return',
|
||||
loadChildren: () =>
|
||||
@@ -242,23 +235,3 @@ const routes: Routes = [
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
if (isDevMode()) {
|
||||
routes.unshift({
|
||||
path: 'preview',
|
||||
component: PreviewComponent,
|
||||
});
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
RouterModule.forRoot(routes, {
|
||||
bindToComponentInputs: true,
|
||||
enableTracing: false,
|
||||
}),
|
||||
TokenLoginModule,
|
||||
],
|
||||
exports: [RouterModule],
|
||||
providers: [provideScrollPositionRestoration()],
|
||||
})
|
||||
export class AppRoutingModule {}
|
||||
11
apps/isa-app/src/app/app.ts
Normal file
11
apps/isa-app/src/app/app.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { RouterOutlet } from '@angular/router';
|
||||
import { ShellLayoutComponent } from '@isa/shell/layout';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
templateUrl: './app.html',
|
||||
styleUrls: ['./app.css'],
|
||||
imports: [RouterOutlet, ShellLayoutComponent],
|
||||
})
|
||||
export class App {}
|
||||
@@ -1,8 +1,13 @@
|
||||
import { inject } from '@angular/core';
|
||||
import { ActivatedRouteSnapshot, CanActivateFn, RouterStateSnapshot } from '@angular/router';
|
||||
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,
|
||||
@@ -19,10 +24,18 @@ export const ActivateProcessIdGuard: CanActivateFn = async (
|
||||
const processId = Number(processIdStr);
|
||||
|
||||
// Check if Process already exists
|
||||
const process = await application.getProcessById$(processId).pipe(take(1)).toPromise();
|
||||
const process = await application
|
||||
.getProcessById$(processId)
|
||||
.pipe(take(1))
|
||||
.toPromise();
|
||||
|
||||
if (!process) {
|
||||
application.createCustomerProcess(processId);
|
||||
application.createProcess({
|
||||
id: processId,
|
||||
type: 'cart',
|
||||
section: 'customer',
|
||||
name: `Vorgang ${processId}`,
|
||||
});
|
||||
}
|
||||
|
||||
application.activateProcess(processId);
|
||||
@@ -30,17 +43,39 @@ export const ActivateProcessIdGuard: CanActivateFn = async (
|
||||
return true;
|
||||
};
|
||||
|
||||
export const ActivateProcessIdWithConfigKeyGuard: (key: string) => CanActivateFn =
|
||||
(key) => async (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => {
|
||||
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}`);
|
||||
const processId = config.get(`process.ids.${key}`, z.coerce.number());
|
||||
|
||||
if (isNaN(processId)) {
|
||||
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;
|
||||
|
||||
@@ -23,7 +23,7 @@ export class CanActivateGoodsInGuard {
|
||||
id: this._config.get('process.ids.goodsIn'),
|
||||
type: 'goods-in',
|
||||
section: 'branch',
|
||||
name: '',
|
||||
name: 'Abholfach',
|
||||
});
|
||||
}
|
||||
this._applicationService.activateProcess(
|
||||
|
||||
@@ -5,7 +5,7 @@ import { ScanAdapterService } from '@adapter/scan';
|
||||
import { AuthService as IsaAuthService } from '@generated/swagger/isa-api';
|
||||
import { UiConfirmModalComponent, UiErrorModalComponent, UiModalResult, UiModalService } from '@ui/modal';
|
||||
import { EnvironmentService } from '@core/environment';
|
||||
import { injectNetworkStatus$ } from '../services/network-status.service';
|
||||
import { injectNetworkStatus$ } from '@isa/core/connectivity';
|
||||
import { toSignal } from '@angular/core/rxjs-interop';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
import { from, NEVER, Observable, throwError } from 'rxjs';
|
||||
import { catchError, filter, mergeMap, takeUntil } from 'rxjs/operators';
|
||||
import { AuthService, LoginStrategy } from '@core/auth';
|
||||
import { injectOnline$ } from '../services/network-status.service';
|
||||
import { injectNetworkStatus$ } from '@isa/core/connectivity';
|
||||
import { logger } from '@isa/core/logging';
|
||||
|
||||
@Injectable()
|
||||
@@ -17,7 +17,7 @@ export class HttpErrorInterceptor implements HttpInterceptor {
|
||||
#logger = logger(() => ({
|
||||
'http-interceptor': 'HttpErrorInterceptor',
|
||||
}));
|
||||
#offline$ = injectOnline$().pipe(filter((online) => !online));
|
||||
#offline$ = injectNetworkStatus$().pipe(filter((status) => status === 'offline'));
|
||||
#injector = inject(Injector);
|
||||
#auth = inject(AuthService);
|
||||
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
<shell-root>
|
||||
<router-outlet></router-outlet>
|
||||
</shell-root>
|
||||
@@ -1,11 +0,0 @@
|
||||
import { Component, ChangeDetectionStrategy } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-main',
|
||||
templateUrl: 'main.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: false,
|
||||
})
|
||||
export class MainComponent {
|
||||
constructor() {}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export * from './network-status.service';
|
||||
@@ -1,25 +0,0 @@
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import { map, Observable } from 'rxjs';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class NetworkStatusService {
|
||||
online$ = new Observable<boolean>((subscriber) => {
|
||||
const handler = () => subscriber.next(navigator.onLine);
|
||||
|
||||
window.addEventListener('online', handler);
|
||||
window.addEventListener('offline', handler);
|
||||
|
||||
handler();
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('online', handler);
|
||||
window.removeEventListener('offline', handler);
|
||||
};
|
||||
});
|
||||
|
||||
status$ = this.online$.pipe(map((online) => (online ? 'online' : 'offline')));
|
||||
}
|
||||
|
||||
export const injectNetworkStatus$ = () => inject(NetworkStatusService).status$;
|
||||
|
||||
export const injectOnline$ = () => inject(NetworkStatusService).online$;
|
||||
@@ -1,2 +1 @@
|
||||
export * from './token-login.component';
|
||||
export * from './token-login.module';
|
||||
|
||||
@@ -7,7 +7,6 @@ import { AuthService } from '@core/auth';
|
||||
templateUrl: 'token-login.component.html',
|
||||
styleUrls: ['token-login.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: false,
|
||||
})
|
||||
export class TokenLoginComponent implements OnInit {
|
||||
constructor(
|
||||
@@ -17,7 +16,10 @@ export class TokenLoginComponent implements OnInit {
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
if (this._route.snapshot.params.token && !this._authService.isAuthenticated()) {
|
||||
if (
|
||||
this._route.snapshot.params.token &&
|
||||
!this._authService.isAuthenticated()
|
||||
) {
|
||||
this._authService.setKeyCardToken(this._route.snapshot.params.token);
|
||||
this._authService.login();
|
||||
} else if (!this._authService.isAuthenticated()) {
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
import { TokenLoginComponent } from './token-login.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule],
|
||||
exports: [TokenLoginComponent],
|
||||
declarations: [TokenLoginComponent],
|
||||
})
|
||||
export class TokenLoginModule {}
|
||||
@@ -67,10 +67,9 @@
|
||||
"goodsOut": 1000,
|
||||
"goodsIn": 2000,
|
||||
"taskCalendar": 3000,
|
||||
"remission": 4000,
|
||||
"packageInspection": 5000,
|
||||
"assortment": 6000,
|
||||
"pickupShelf": 7000
|
||||
"pickupShelf": 2000
|
||||
}
|
||||
},
|
||||
"checkForUpdates": 3600000,
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
import { NgModule, ModuleWithProviders } from '@angular/core';
|
||||
import { StoreModule } from '@ngrx/store';
|
||||
import { applicationReducer } from './store';
|
||||
import { ApplicationService } from './application.service';
|
||||
|
||||
@NgModule({
|
||||
declarations: [],
|
||||
imports: [],
|
||||
exports: [],
|
||||
})
|
||||
export class CoreApplicationModule {
|
||||
static forRoot(): ModuleWithProviders<CoreApplicationModule> {
|
||||
return {
|
||||
ngModule: RootCoreApplicationModule,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
imports: [StoreModule.forFeature('core-application', applicationReducer)],
|
||||
providers: [ApplicationService],
|
||||
})
|
||||
export class RootCoreApplicationModule {}
|
||||
@@ -1,337 +0,0 @@
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import { BehaviorSubject, Observable, of, firstValueFrom } from 'rxjs';
|
||||
import { map, filter, withLatestFrom } from 'rxjs/operators';
|
||||
import { BranchDTO } from '@generated/swagger/checkout-api';
|
||||
import { isBoolean, isNumber } from '@utils/common';
|
||||
import { ApplicationService } from './application.service';
|
||||
import { TabService } from '@isa/core/tabs';
|
||||
import { ApplicationProcess } from './defs/application-process';
|
||||
import { Tab, TabMetadata } from '@isa/core/tabs';
|
||||
import { toObservable } from '@angular/core/rxjs-interop';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { removeProcess } from './store/application.actions';
|
||||
|
||||
/**
|
||||
* Adapter service that bridges the old ApplicationService interface with the new TabService.
|
||||
*
|
||||
* This adapter allows existing code that depends on ApplicationService to work with the new
|
||||
* TabService without requiring immediate code changes. It maps ApplicationProcess concepts
|
||||
* to Tab entities, storing process-specific data in tab metadata.
|
||||
*
|
||||
* Key mappings:
|
||||
* - ApplicationProcess.id <-> Tab.id
|
||||
* - ApplicationProcess.name <-> Tab.name
|
||||
* - ApplicationProcess metadata (section, type, etc.) <-> Tab.metadata with 'process_' prefix
|
||||
* - ApplicationProcess.data <-> Tab.metadata with 'data_' prefix
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Inject the adapter instead of the original service
|
||||
* constructor(private applicationService: ApplicationServiceAdapter) {}
|
||||
*
|
||||
* // Use the same API as before
|
||||
* const process = await this.applicationService.createCustomerProcess();
|
||||
* this.applicationService.activateProcess(process.id);
|
||||
* ```
|
||||
*/
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class ApplicationServiceAdapter extends ApplicationService {
|
||||
#store = inject(Store);
|
||||
|
||||
#tabService = inject(TabService);
|
||||
|
||||
#activatedProcessId$ = toObservable(this.#tabService.activatedTabId);
|
||||
|
||||
#tabs$ = toObservable(this.#tabService.entities);
|
||||
|
||||
#processes$ = this.#tabs$.pipe(
|
||||
map((tabs) => tabs.map((tab) => this.mapTabToProcess(tab))),
|
||||
);
|
||||
|
||||
#section = new BehaviorSubject<'customer' | 'branch'>('customer');
|
||||
|
||||
readonly REGEX_PROCESS_NAME = /^Vorgang \d+$/;
|
||||
|
||||
get activatedProcessId() {
|
||||
return this.#tabService.activatedTabId();
|
||||
}
|
||||
|
||||
get activatedProcessId$() {
|
||||
return this.#activatedProcessId$;
|
||||
}
|
||||
|
||||
getProcesses$(
|
||||
section?: 'customer' | 'branch',
|
||||
): Observable<ApplicationProcess[]> {
|
||||
return this.#processes$.pipe(
|
||||
map((processes) =>
|
||||
processes.filter((process) =>
|
||||
section ? process.section === section : true,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
getProcessById$(processId: number): Observable<ApplicationProcess> {
|
||||
return this.#processes$.pipe(
|
||||
map((processes) => processes.find((process) => process.id === processId)),
|
||||
);
|
||||
}
|
||||
|
||||
getSection$(): Observable<'customer' | 'branch'> {
|
||||
return this.#section.asObservable();
|
||||
}
|
||||
|
||||
getTitle$(): Observable<'Kundenbereich' | 'Filialbereich'> {
|
||||
return this.getSection$().pipe(
|
||||
map((section) =>
|
||||
section === 'customer' ? 'Kundenbereich' : 'Filialbereich',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/** @deprecated */
|
||||
getActivatedProcessId$(): Observable<number> {
|
||||
return this.activatedProcessId$;
|
||||
}
|
||||
|
||||
activateProcess(activatedProcessId: number): void {
|
||||
this.#tabService.activateTab(activatedProcessId);
|
||||
}
|
||||
|
||||
removeProcess(processId: number): void {
|
||||
this.#tabService.removeTab(processId);
|
||||
this.#store.dispatch(removeProcess({ processId }));
|
||||
}
|
||||
|
||||
patchProcess(processId: number, changes: Partial<ApplicationProcess>): void {
|
||||
const tabChanges: {
|
||||
name?: string;
|
||||
tags?: string[];
|
||||
metadata?: Record<string, unknown>;
|
||||
} = {};
|
||||
|
||||
if (changes.name) {
|
||||
tabChanges.name = changes.name;
|
||||
}
|
||||
|
||||
// Store other ApplicationProcess properties in metadata
|
||||
const metadataKeys = [
|
||||
'section',
|
||||
'type',
|
||||
'closeable',
|
||||
'confirmClosing',
|
||||
'created',
|
||||
'activated',
|
||||
'data',
|
||||
];
|
||||
metadataKeys.forEach((key) => {
|
||||
if (tabChanges.metadata === undefined) {
|
||||
tabChanges.metadata = {};
|
||||
}
|
||||
|
||||
if (changes[key as keyof ApplicationProcess] !== undefined) {
|
||||
tabChanges.metadata[`process_${key}`] =
|
||||
changes[key as keyof ApplicationProcess];
|
||||
}
|
||||
});
|
||||
|
||||
// Apply the changes to the tab
|
||||
this.#tabService.patchTab(processId, tabChanges);
|
||||
}
|
||||
|
||||
patchProcessData(processId: number, data: Record<string, unknown>): void {
|
||||
const currentProcess = this.#tabService.entityMap()[processId];
|
||||
const currentData: TabMetadata =
|
||||
(currentProcess?.metadata?.['process_data'] as TabMetadata) ?? {};
|
||||
|
||||
this.#tabService.patchTab(processId, {
|
||||
metadata: { [`process_data`]: { ...currentData, ...data } },
|
||||
});
|
||||
}
|
||||
|
||||
getSelectedBranch$(): Observable<BranchDTO> {
|
||||
return this.#processes$.pipe(
|
||||
withLatestFrom(this.#activatedProcessId$),
|
||||
map(([processes, activatedProcessId]) =>
|
||||
processes.find((process) => process.id === activatedProcessId),
|
||||
),
|
||||
filter((process): process is ApplicationProcess => !!process),
|
||||
map((process) => process.data?.selectedBranch as BranchDTO),
|
||||
);
|
||||
}
|
||||
|
||||
async createCustomerProcess(processId?: number): Promise<ApplicationProcess> {
|
||||
const processes = await firstValueFrom(this.getProcesses$('customer'));
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new ApplicationProcess by first creating a Tab and then storing
|
||||
* process-specific properties in the tab's metadata.
|
||||
*
|
||||
* @param process - The ApplicationProcess to create
|
||||
* @throws {Error} If process ID already exists or is invalid
|
||||
*/
|
||||
async createProcess(process: ApplicationProcess): Promise<void> {
|
||||
const existingProcess = this.#tabService.entityMap()[process.id];
|
||||
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;
|
||||
|
||||
// Create tab with process data and preserve the process ID
|
||||
this.#tabService.addTab({
|
||||
id: process.id,
|
||||
name: process.name,
|
||||
tags: [process.section, process.type].filter(Boolean),
|
||||
metadata: {
|
||||
process_section: process.section,
|
||||
process_type: process.type,
|
||||
process_closeable: process.closeable,
|
||||
process_confirmClosing: process.confirmClosing,
|
||||
process_created: process.created,
|
||||
process_activated: process.activated,
|
||||
process_data: process.data,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
setSection(section: 'customer' | 'branch'): void {
|
||||
this.#section.next(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),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps Tab entities to ApplicationProcess objects by extracting process-specific
|
||||
* metadata and combining it with tab properties.
|
||||
*
|
||||
* @param tab - The tab entity to convert
|
||||
* @returns The corresponding ApplicationProcess object
|
||||
*/
|
||||
private mapTabToProcess(tab: Tab): ApplicationProcess {
|
||||
return {
|
||||
id: tab.id,
|
||||
name: tab.name,
|
||||
created:
|
||||
this.getMetadataValue<number>(tab.metadata, 'process_created') ??
|
||||
tab.createdAt,
|
||||
activated:
|
||||
this.getMetadataValue<number>(tab.metadata, 'process_activated') ??
|
||||
tab.activatedAt ??
|
||||
0,
|
||||
section:
|
||||
this.getMetadataValue<'customer' | 'branch'>(
|
||||
tab.metadata,
|
||||
'process_section',
|
||||
) ?? 'customer',
|
||||
type: this.getMetadataValue<string>(tab.metadata, 'process_type'),
|
||||
closeable:
|
||||
this.getMetadataValue<boolean>(tab.metadata, 'process_closeable') ??
|
||||
true,
|
||||
confirmClosing:
|
||||
this.getMetadataValue<boolean>(
|
||||
tab.metadata,
|
||||
'process_confirmClosing',
|
||||
) ?? true,
|
||||
data: this.extractDataFromMetadata(tab.metadata),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts ApplicationProcess data properties from tab metadata.
|
||||
* Data properties are stored with a 'data_' prefix in tab metadata.
|
||||
*
|
||||
* @param metadata - The tab metadata object
|
||||
* @returns The extracted data object or undefined if no data properties exist
|
||||
*/
|
||||
private extractDataFromMetadata(
|
||||
metadata: TabMetadata,
|
||||
): Record<string, unknown> | undefined {
|
||||
// Return the complete data object stored under 'process_data'
|
||||
const processData = metadata?.['process_data'];
|
||||
|
||||
if (
|
||||
processData &&
|
||||
typeof processData === 'object' &&
|
||||
processData !== null
|
||||
) {
|
||||
return processData as Record<string, unknown>;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private getMetadataValue<T>(
|
||||
metadata: TabMetadata,
|
||||
key: string,
|
||||
): T | undefined {
|
||||
return metadata?.[key] as T | undefined;
|
||||
}
|
||||
|
||||
private createTimestamp(): number {
|
||||
return Date.now();
|
||||
}
|
||||
}
|
||||
@@ -1,233 +0,0 @@
|
||||
// import { createServiceFactory, SpectatorService, SpyObject } from '@ngneat/spectator';
|
||||
// import { Store } from '@ngrx/store';
|
||||
// import { Observable, of } from 'rxjs';
|
||||
// import { first } from 'rxjs/operators';
|
||||
// import { ApplicationProcess } from './defs';
|
||||
|
||||
// import { ApplicationService } from './application.service';
|
||||
// import * as actions from './store/application.actions';
|
||||
|
||||
// describe('ApplicationService', () => {
|
||||
// let spectator: SpectatorService<ApplicationService>;
|
||||
// let store: SpyObject<Store>;
|
||||
// const createService = createServiceFactory({
|
||||
// service: ApplicationService,
|
||||
// mocks: [Store],
|
||||
// });
|
||||
|
||||
// beforeEach(() => {
|
||||
// spectator = createService({});
|
||||
// store = spectator.inject(Store);
|
||||
// });
|
||||
|
||||
// it('should be created', () => {
|
||||
// expect(spectator.service).toBeTruthy();
|
||||
// });
|
||||
|
||||
// describe('activatedProcessId$', () => {
|
||||
// it('should return an observable', () => {
|
||||
// expect(spectator.service.activatedProcessId$).toBeInstanceOf(Observable);
|
||||
// });
|
||||
// });
|
||||
|
||||
// describe('activatedProcessId', () => {
|
||||
// it('should return the process id as a number', () => {
|
||||
// spyOnProperty(spectator.service['activatedProcessIdSubject'] as any, 'value').and.returnValue(2);
|
||||
// expect(spectator.service.activatedProcessId).toBe(2);
|
||||
// });
|
||||
// });
|
||||
|
||||
// describe('getProcesses$()', () => {
|
||||
// it('should call select on store and return all selected processes', async () => {
|
||||
// const processes: ApplicationProcess[] = [
|
||||
// { id: 1, name: 'Vorgang', type: 'cart', section: 'customer', data: { count: 1 } },
|
||||
// { id: 2, name: 'Vorgang', type: 'task-calendar', section: 'branch' },
|
||||
// ];
|
||||
// store.select.and.returnValue(of(processes));
|
||||
// const result = await spectator.service.getProcesses$().pipe(first()).toPromise();
|
||||
// expect(result).toEqual(processes);
|
||||
// expect(store.select).toHaveBeenCalled();
|
||||
// });
|
||||
|
||||
// it('should call select on store and return all section customer processes', async () => {
|
||||
// const processes: ApplicationProcess[] = [
|
||||
// { id: 1, name: 'Vorgang', type: 'cart', section: 'customer', data: { count: 1 } },
|
||||
// { id: 2, name: 'Vorgang', type: 'task-calendar', section: 'branch' },
|
||||
// ];
|
||||
// store.select.and.returnValue(of(processes));
|
||||
// const result = await spectator.service.getProcesses$('customer').pipe(first()).toPromise();
|
||||
// expect(result).toEqual([processes[0]]);
|
||||
// expect(store.select).toHaveBeenCalled();
|
||||
// });
|
||||
|
||||
// it('should call select on store and return all section branch processes', async () => {
|
||||
// const processes: ApplicationProcess[] = [
|
||||
// { id: 1, name: 'Vorgang', type: 'cart', section: 'customer', data: { count: 1 } },
|
||||
// { id: 2, name: 'Vorgang', type: 'task-calendar', section: 'branch' },
|
||||
// ];
|
||||
// store.select.and.returnValue(of(processes));
|
||||
// const result = await spectator.service.getProcesses$('branch').pipe(first()).toPromise();
|
||||
// expect(result).toEqual([processes[1]]);
|
||||
// expect(store.select).toHaveBeenCalled();
|
||||
// });
|
||||
// });
|
||||
|
||||
// describe('getProcessById$()', () => {
|
||||
// it('should return the process by id', async () => {
|
||||
// const processes: ApplicationProcess[] = [
|
||||
// { id: 1, name: 'Vorgang 1', section: 'customer' },
|
||||
// { id: 2, name: 'Vorgang 2', section: 'customer' },
|
||||
// ];
|
||||
// spyOn(spectator.service, 'getProcesses$').and.returnValue(of(processes));
|
||||
|
||||
// const process = await spectator.service.getProcessById$(1).toPromise();
|
||||
// expect(process.id).toBe(1);
|
||||
// });
|
||||
// });
|
||||
|
||||
// describe('getSection$()', () => {
|
||||
// it('should return the selected section branch', async () => {
|
||||
// const section = 'branch';
|
||||
// store.select.and.returnValue(of(section));
|
||||
// const result = await spectator.service.getSection$().pipe(first()).toPromise();
|
||||
// expect(result).toEqual(section);
|
||||
// expect(store.select).toHaveBeenCalled();
|
||||
// });
|
||||
// });
|
||||
|
||||
// describe('getActivatedProcessId$', () => {
|
||||
// it('should return the current selected activated process id', async () => {
|
||||
// const activatedProcessId = 2;
|
||||
// store.select.and.returnValue(of({ id: activatedProcessId }));
|
||||
// const result = await spectator.service.getActivatedProcessId$().pipe(first()).toPromise();
|
||||
// expect(result).toEqual(activatedProcessId);
|
||||
// expect(store.select).toHaveBeenCalled();
|
||||
// });
|
||||
// });
|
||||
|
||||
// describe('activateProcess()', () => {
|
||||
// it('should dispatch action setActivatedProcess with argument activatedProcessId and action type', () => {
|
||||
// const activatedProcessId = 2;
|
||||
// spectator.service.activateProcess(activatedProcessId);
|
||||
// expect(store.dispatch).toHaveBeenCalledWith({ activatedProcessId, type: actions.setActivatedProcess.type });
|
||||
// });
|
||||
// });
|
||||
|
||||
// describe('removeProcess()', () => {
|
||||
// it('should dispatch action removeProcess with argument processId and action type', () => {
|
||||
// const processId = 2;
|
||||
// spectator.service.removeProcess(processId);
|
||||
// expect(store.dispatch).toHaveBeenCalledWith({ processId, type: actions.removeProcess.type });
|
||||
// });
|
||||
// });
|
||||
|
||||
// describe('createProcess()', () => {
|
||||
// it('should dispatch action addProcess with process', async () => {
|
||||
// const process: ApplicationProcess = {
|
||||
// id: 1,
|
||||
// name: 'Vorgang 1',
|
||||
// section: 'customer',
|
||||
// type: 'cart',
|
||||
// };
|
||||
|
||||
// const timestamp = 100;
|
||||
// spyOn(spectator.service as any, '_createTimestamp').and.returnValue(timestamp);
|
||||
// spyOn(spectator.service, 'getProcessById$').and.returnValue(of(undefined));
|
||||
// await spectator.service.createProcess(process);
|
||||
|
||||
// expect(store.dispatch).toHaveBeenCalledWith({
|
||||
// type: actions.addProcess.type,
|
||||
// process: {
|
||||
// ...process,
|
||||
// activated: 0,
|
||||
// created: timestamp,
|
||||
// },
|
||||
// });
|
||||
// });
|
||||
|
||||
// it('should throw an error if the process id is already existing', async () => {
|
||||
// const process: ApplicationProcess = {
|
||||
// id: 1,
|
||||
// name: 'Vorgang 1',
|
||||
// section: 'customer',
|
||||
// type: 'cart',
|
||||
// };
|
||||
// spyOn(spectator.service, 'getProcessById$').and.returnValue(of(process));
|
||||
// await expectAsync(spectator.service.createProcess(process)).toBeRejectedWithError('Process Id existiert bereits');
|
||||
// });
|
||||
|
||||
// it('should throw an error if the process id is not a number', async () => {
|
||||
// const process: ApplicationProcess = {
|
||||
// id: undefined,
|
||||
// name: 'Vorgang 1',
|
||||
// section: 'customer',
|
||||
// type: 'cart',
|
||||
// };
|
||||
// spyOn(spectator.service, 'getProcessById$').and.returnValue(of({ id: 5, name: 'Vorgang 2', section: 'customer' }));
|
||||
// await expectAsync(spectator.service.createProcess(process)).toBeRejectedWithError('Process Id nicht gesetzt');
|
||||
// });
|
||||
// });
|
||||
|
||||
// describe('patchProcess', () => {
|
||||
// it('should dispatch action patchProcess with changes', async () => {
|
||||
// const process: ApplicationProcess = {
|
||||
// id: 1,
|
||||
// name: 'Vorgang 1',
|
||||
// section: 'customer',
|
||||
// type: 'cart',
|
||||
// };
|
||||
|
||||
// await spectator.service.patchProcess(process.id, process);
|
||||
|
||||
// expect(store.dispatch).toHaveBeenCalledWith({
|
||||
// type: actions.patchProcess.type,
|
||||
// processId: process.id,
|
||||
// changes: {
|
||||
// ...process,
|
||||
// },
|
||||
// });
|
||||
// });
|
||||
// });
|
||||
|
||||
// describe('setSection()', () => {
|
||||
// it('should dispatch action setSection with argument section and action type', () => {
|
||||
// const section = 'customer';
|
||||
// spectator.service.setSection(section);
|
||||
// expect(store.dispatch).toHaveBeenCalledWith({ section, type: actions.setSection.type });
|
||||
// });
|
||||
// });
|
||||
|
||||
// describe('getLastActivatedProcessWithSectionAndType()', () => {
|
||||
// it('should return the last activated process by section and type', async () => {
|
||||
// const processes: ApplicationProcess[] = [
|
||||
// { id: 1, name: 'Vorgang 1', section: 'customer', type: 'cart', activated: 100 },
|
||||
// { id: 2, name: 'Vorgang 2', section: 'customer', type: 'cart', activated: 200 },
|
||||
// { id: 3, name: 'Vorgang 3', section: 'customer', type: 'goodsOut', activated: 300 },
|
||||
// ];
|
||||
// spyOn(spectator.service, 'getProcesses$').and.returnValue(of(processes));
|
||||
|
||||
// expect(await spectator.service.getLastActivatedProcessWithSectionAndType$('customer', 'cart').pipe(first()).toPromise()).toBe(
|
||||
// processes[1]
|
||||
// );
|
||||
// });
|
||||
// });
|
||||
|
||||
// describe('getLastActivatedProcessWithSection()', () => {
|
||||
// it('should return the last activated process by section', async () => {
|
||||
// const processes: ApplicationProcess[] = [
|
||||
// { id: 1, name: 'Vorgang 1', section: 'customer', activated: 100 },
|
||||
// { id: 2, name: 'Vorgang 2', section: 'customer', activated: 200 },
|
||||
// { id: 3, name: 'Vorgang 3', section: 'customer', activated: 300 },
|
||||
// ];
|
||||
// spyOn(spectator.service, 'getProcesses$').and.returnValue(of(processes));
|
||||
|
||||
// expect(await spectator.service.getLastActivatedProcessWithSection$('customer').pipe(first()).toPromise()).toBe(processes[2]);
|
||||
// });
|
||||
// });
|
||||
|
||||
// describe('_createTimestamp', () => {
|
||||
// it('should return the current timestamp in ms', () => {
|
||||
// expect(spectator.service['_createTimestamp']()).toBeCloseTo(Date.now());
|
||||
// });
|
||||
// });
|
||||
// });
|
||||
@@ -1,99 +1,171 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import { BehaviorSubject, Observable, of, firstValueFrom } from 'rxjs';
|
||||
import { map, filter, withLatestFrom } from 'rxjs/operators';
|
||||
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';
|
||||
import { TabService } from '@isa/core/tabs';
|
||||
import { ApplicationProcess } from './defs/application-process';
|
||||
import { Tab, TabMetadata } from '@isa/core/tabs';
|
||||
import { toObservable } from '@angular/core/rxjs-interop';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { removeProcess } from './store/application.actions';
|
||||
|
||||
@Injectable()
|
||||
/**
|
||||
* Adapter service that bridges the old ApplicationService interface with the new TabService.
|
||||
*
|
||||
* This adapter allows existing code that depends on ApplicationService to work with the new
|
||||
* TabService without requiring immediate code changes. It maps ApplicationProcess concepts
|
||||
* to Tab entities, storing process-specific data in tab metadata.
|
||||
*
|
||||
* Key mappings:
|
||||
* - ApplicationProcess.id <-> Tab.id
|
||||
* - ApplicationProcess.name <-> Tab.name
|
||||
* - ApplicationProcess metadata (section, type, etc.) <-> Tab.metadata with 'process_' prefix
|
||||
* - ApplicationProcess.data <-> Tab.metadata with 'data_' prefix
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Inject the adapter instead of the original service
|
||||
* constructor(private applicationService: ApplicationServiceAdapter) {}
|
||||
*
|
||||
* // Use the same API as before
|
||||
* const process = await this.applicationService.createCustomerProcess();
|
||||
* this.applicationService.activateProcess(process.id);
|
||||
* ```
|
||||
*/
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class ApplicationService {
|
||||
private activatedProcessIdSubject = new BehaviorSubject<number>(undefined);
|
||||
#store = inject(Store);
|
||||
|
||||
#tabService = inject(TabService);
|
||||
|
||||
#activatedProcessId$ = toObservable(this.#tabService.activatedTabId);
|
||||
|
||||
#tabs$ = toObservable(this.#tabService.entities);
|
||||
|
||||
#processes$ = this.#tabs$.pipe(
|
||||
map((tabs) => tabs.map((tab) => this.mapTabToProcess(tab))),
|
||||
);
|
||||
|
||||
#section = new BehaviorSubject<'customer' | 'branch'>('customer');
|
||||
|
||||
readonly REGEX_PROCESS_NAME = /^Vorgang \d+$/;
|
||||
|
||||
get activatedProcessId() {
|
||||
return this.activatedProcessIdSubject.value;
|
||||
return this.#tabService.activatedTabId();
|
||||
}
|
||||
|
||||
get activatedProcessId$() {
|
||||
return this.activatedProcessIdSubject.asObservable();
|
||||
return this.#activatedProcessId$;
|
||||
}
|
||||
|
||||
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))),
|
||||
getProcesses$(
|
||||
section?: 'customer' | 'branch',
|
||||
): Observable<ApplicationProcess[]> {
|
||||
return this.#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)));
|
||||
return this.#processes$.pipe(
|
||||
map((processes) => processes.find((process) => process.id === processId)),
|
||||
);
|
||||
}
|
||||
|
||||
getSection$() {
|
||||
return this.store.select(selectSection);
|
||||
getSection$(): Observable<'customer' | 'branch'> {
|
||||
return this.#section.asObservable();
|
||||
}
|
||||
|
||||
getTitle$() {
|
||||
getTitle$(): Observable<'Kundenbereich' | 'Filialbereich'> {
|
||||
return this.getSection$().pipe(
|
||||
map((section) => {
|
||||
return section === 'customer' ? 'Kundenbereich' : 'Filialbereich';
|
||||
}),
|
||||
map((section) =>
|
||||
section === 'customer' ? 'Kundenbereich' : 'Filialbereich',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/** @deprecated */
|
||||
getActivatedProcessId$() {
|
||||
return this.store.select(selectActivatedProcess).pipe(map((process) => process?.id));
|
||||
getActivatedProcessId$(): Observable<number> {
|
||||
return this.activatedProcessId$;
|
||||
}
|
||||
|
||||
activateProcess(activatedProcessId: number) {
|
||||
this.store.dispatch(setActivatedProcess({ activatedProcessId }));
|
||||
this.activatedProcessIdSubject.next(activatedProcessId);
|
||||
activateProcess(activatedProcessId: number): void {
|
||||
this.#tabService.activateTab(activatedProcessId);
|
||||
}
|
||||
|
||||
removeProcess(processId: number) {
|
||||
this.store.dispatch(removeProcess({ processId }));
|
||||
removeProcess(processId: number): void {
|
||||
this.#tabService.removeTab(processId);
|
||||
this.#store.dispatch(removeProcess({ processId }));
|
||||
}
|
||||
|
||||
patchProcess(processId: number, changes: Partial<ApplicationProcess>) {
|
||||
this.store.dispatch(patchProcess({ processId, changes }));
|
||||
patchProcess(processId: number, changes: Partial<ApplicationProcess>): void {
|
||||
const tabChanges: {
|
||||
name?: string;
|
||||
tags?: string[];
|
||||
metadata?: Record<string, unknown>;
|
||||
} = {};
|
||||
|
||||
if (changes.name) {
|
||||
tabChanges.name = changes.name;
|
||||
}
|
||||
|
||||
patchProcessData(processId: number, data: Record<string, any>) {
|
||||
this.store.dispatch(patchProcessData({ processId, data }));
|
||||
// Store other ApplicationProcess properties in metadata
|
||||
const metadataKeys = [
|
||||
'section',
|
||||
'type',
|
||||
'closeable',
|
||||
'confirmClosing',
|
||||
'created',
|
||||
'activated',
|
||||
'data',
|
||||
];
|
||||
metadataKeys.forEach((key) => {
|
||||
if (tabChanges.metadata === undefined) {
|
||||
tabChanges.metadata = {};
|
||||
}
|
||||
|
||||
getSelectedBranch$(processId?: number): Observable<BranchDTO> {
|
||||
if (!processId) {
|
||||
return this.activatedProcessId$.pipe(
|
||||
switchMap((processId) => this.getProcessById$(processId).pipe(map((process) => process?.data?.selectedBranch))),
|
||||
if (changes[key as keyof ApplicationProcess] !== undefined) {
|
||||
tabChanges.metadata[`process_${key}`] =
|
||||
changes[key as keyof ApplicationProcess];
|
||||
}
|
||||
});
|
||||
|
||||
// Apply the changes to the tab
|
||||
this.#tabService.patchTab(processId, tabChanges);
|
||||
}
|
||||
|
||||
patchProcessData(processId: number, data: Record<string, unknown>): void {
|
||||
const currentProcess = this.#tabService.entityMap()[processId];
|
||||
const currentData: TabMetadata =
|
||||
(currentProcess?.metadata?.['process_data'] as TabMetadata) ?? {};
|
||||
|
||||
this.#tabService.patchTab(processId, {
|
||||
metadata: { [`process_data`]: { ...currentData, ...data } },
|
||||
});
|
||||
}
|
||||
|
||||
getSelectedBranch$(): Observable<BranchDTO> {
|
||||
return this.#processes$.pipe(
|
||||
withLatestFrom(this.#activatedProcessId$),
|
||||
map(([processes, activatedProcessId]) =>
|
||||
processes.find((process) => process.id === activatedProcessId),
|
||||
),
|
||||
filter((process): process is ApplicationProcess => !!process),
|
||||
map((process) => process.data?.selectedBranch as BranchDTO),
|
||||
);
|
||||
}
|
||||
|
||||
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 processes = await firstValueFrom(this.getProcesses$('customer'));
|
||||
|
||||
const processIds = processes.filter((x) => this.REGEX_PROCESS_NAME.test(x.name)).map((x) => +x.name.split(' ')[1]);
|
||||
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;
|
||||
|
||||
@@ -106,12 +178,18 @@ export class ApplicationService {
|
||||
};
|
||||
|
||||
await this.createProcess(process);
|
||||
|
||||
return process;
|
||||
}
|
||||
|
||||
async createProcess(process: ApplicationProcess) {
|
||||
const existingProcess = await this.getProcessById$(process?.id).pipe(first()).toPromise();
|
||||
/**
|
||||
* Creates a new ApplicationProcess by first creating a Tab and then storing
|
||||
* process-specific properties in the tab's metadata.
|
||||
*
|
||||
* @param process - The ApplicationProcess to create
|
||||
* @throws {Error} If process ID already exists or is invalid
|
||||
*/
|
||||
async createProcess(process: ApplicationProcess): Promise<void> {
|
||||
const existingProcess = this.#tabService.entityMap()[process.id];
|
||||
if (existingProcess?.id === process?.id) {
|
||||
throw new Error('Process Id existiert bereits');
|
||||
}
|
||||
@@ -128,13 +206,28 @@ export class ApplicationService {
|
||||
process.confirmClosing = true;
|
||||
}
|
||||
|
||||
process.created = this._createTimestamp();
|
||||
process.created = this.createTimestamp();
|
||||
process.activated = 0;
|
||||
this.store.dispatch(addProcess({ process }));
|
||||
|
||||
// Create tab with process data and preserve the process ID
|
||||
this.#tabService.addTab({
|
||||
id: process.id,
|
||||
name: process.name,
|
||||
tags: [process.section, process.type].filter(Boolean),
|
||||
metadata: {
|
||||
process_section: process.section,
|
||||
process_type: process.type,
|
||||
process_closeable: process.closeable,
|
||||
process_confirmClosing: process.confirmClosing,
|
||||
process_created: process.created,
|
||||
process_activated: process.activated,
|
||||
process_data: process.data,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
setSection(section: 'customer' | 'branch') {
|
||||
this.store.dispatch(setSection({ section }));
|
||||
setSection(section: 'customer' | 'branch'): void {
|
||||
this.#section.next(section);
|
||||
}
|
||||
|
||||
getLastActivatedProcessWithSectionAndType$(
|
||||
@@ -155,7 +248,9 @@ export class ApplicationService {
|
||||
);
|
||||
}
|
||||
|
||||
getLastActivatedProcessWithSection$(section: 'customer' | 'branch'): Observable<ApplicationProcess> {
|
||||
getLastActivatedProcessWithSection$(
|
||||
section: 'customer' | 'branch',
|
||||
): Observable<ApplicationProcess> {
|
||||
return this.getProcesses$(section).pipe(
|
||||
map((processes) =>
|
||||
processes?.reduce((latest, current) => {
|
||||
@@ -168,7 +263,74 @@ export class ApplicationService {
|
||||
);
|
||||
}
|
||||
|
||||
private _createTimestamp() {
|
||||
/**
|
||||
* Maps Tab entities to ApplicationProcess objects by extracting process-specific
|
||||
* metadata and combining it with tab properties.
|
||||
*
|
||||
* @param tab - The tab entity to convert
|
||||
* @returns The corresponding ApplicationProcess object
|
||||
*/
|
||||
private mapTabToProcess(tab: Tab): ApplicationProcess {
|
||||
return {
|
||||
id: tab.id,
|
||||
name: tab.name,
|
||||
created:
|
||||
this.getMetadataValue<number>(tab.metadata, 'process_created') ??
|
||||
tab.createdAt,
|
||||
activated:
|
||||
this.getMetadataValue<number>(tab.metadata, 'process_activated') ??
|
||||
tab.activatedAt ??
|
||||
0,
|
||||
section:
|
||||
this.getMetadataValue<'customer' | 'branch'>(
|
||||
tab.metadata,
|
||||
'process_section',
|
||||
) ?? 'customer',
|
||||
type: this.getMetadataValue<string>(tab.metadata, 'process_type'),
|
||||
closeable:
|
||||
this.getMetadataValue<boolean>(tab.metadata, 'process_closeable') ??
|
||||
true,
|
||||
confirmClosing:
|
||||
this.getMetadataValue<boolean>(
|
||||
tab.metadata,
|
||||
'process_confirmClosing',
|
||||
) ?? true,
|
||||
data: this.extractDataFromMetadata(tab.metadata),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts ApplicationProcess data properties from tab metadata.
|
||||
* Data properties are stored with a 'data_' prefix in tab metadata.
|
||||
*
|
||||
* @param metadata - The tab metadata object
|
||||
* @returns The extracted data object or undefined if no data properties exist
|
||||
*/
|
||||
private extractDataFromMetadata(
|
||||
metadata: TabMetadata,
|
||||
): Record<string, unknown> | undefined {
|
||||
// Return the complete data object stored under 'process_data'
|
||||
const processData = metadata?.['process_data'];
|
||||
|
||||
if (
|
||||
processData &&
|
||||
typeof processData === 'object' &&
|
||||
processData !== null
|
||||
) {
|
||||
return processData as Record<string, unknown>;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private getMetadataValue<T>(
|
||||
metadata: TabMetadata,
|
||||
key: string,
|
||||
): T | undefined {
|
||||
return metadata?.[key] as T | undefined;
|
||||
}
|
||||
|
||||
private createTimestamp(): number {
|
||||
return Date.now();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
export * from './application.module';
|
||||
export * from './application.service';
|
||||
export * from './application.service-adapter';
|
||||
export * from './defs';
|
||||
export * from './store';
|
||||
export * from './store/application.actions';
|
||||
|
||||
@@ -1,27 +1,8 @@
|
||||
import { createAction, props } from '@ngrx/store';
|
||||
import { ApplicationProcess } from '..';
|
||||
|
||||
const prefix = '[CORE-APPLICATION]';
|
||||
|
||||
export const setTitle = createAction(`${prefix} Set Title`, props<{ title: string }>());
|
||||
|
||||
export const setSection = createAction(`${prefix} Set Section`, props<{ section: 'customer' | 'branch' }>());
|
||||
|
||||
export const addProcess = createAction(`${prefix} Add Process`, props<{ process: ApplicationProcess }>());
|
||||
|
||||
export const removeProcess = createAction(`${prefix} Remove Process`, props<{ processId: number }>());
|
||||
|
||||
export const setActivatedProcess = createAction(
|
||||
`${prefix} Set Activated Process`,
|
||||
props<{ activatedProcessId: number }>(),
|
||||
);
|
||||
|
||||
export const patchProcess = createAction(
|
||||
`${prefix} Patch Process`,
|
||||
props<{ processId: number; changes: Partial<ApplicationProcess> }>(),
|
||||
);
|
||||
|
||||
export const patchProcessData = createAction(
|
||||
`${prefix} Patch Process Data`,
|
||||
props<{ processId: number; data: Record<string, any> }>(),
|
||||
export const removeProcess = createAction(
|
||||
`${prefix} Remove Process`,
|
||||
props<{ processId: number }>(),
|
||||
);
|
||||
|
||||
@@ -1,200 +0,0 @@
|
||||
import { INITIAL_APPLICATION_STATE } from './application.state';
|
||||
import * as actions from './application.actions';
|
||||
import { applicationReducer } from './application.reducer';
|
||||
import { ApplicationProcess } from '../defs';
|
||||
import { ApplicationState } from './application.state';
|
||||
|
||||
describe('applicationReducer', () => {
|
||||
describe('setSection()', () => {
|
||||
it('should return modified state with section customer', () => {
|
||||
const initialState = INITIAL_APPLICATION_STATE;
|
||||
|
||||
const action = actions.setSection({ section: 'customer' });
|
||||
const state = applicationReducer(initialState, action);
|
||||
|
||||
expect(state).toEqual({
|
||||
...initialState,
|
||||
section: 'customer',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return modified state with section branch', () => {
|
||||
const initialState = INITIAL_APPLICATION_STATE;
|
||||
|
||||
const action = actions.setSection({ section: 'branch' });
|
||||
const state = applicationReducer(initialState, action);
|
||||
|
||||
expect(state).toEqual({
|
||||
...initialState,
|
||||
section: 'branch',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('addProcess()', () => {
|
||||
it('should return modified state with new process if no processes are registered in the state', () => {
|
||||
const initialState = INITIAL_APPLICATION_STATE;
|
||||
|
||||
const process: ApplicationProcess = {
|
||||
id: 1,
|
||||
name: 'Vorgang',
|
||||
section: 'customer',
|
||||
type: 'cart',
|
||||
data: {},
|
||||
};
|
||||
|
||||
const action = actions.addProcess({ process });
|
||||
const state = applicationReducer(initialState, action);
|
||||
expect(state.processes[0]).toEqual(process);
|
||||
});
|
||||
});
|
||||
|
||||
describe('patchProcess()', () => {
|
||||
it('should return modified state with updated process when id is found', () => {
|
||||
const initialState = INITIAL_APPLICATION_STATE;
|
||||
|
||||
const process: ApplicationProcess = {
|
||||
id: 1,
|
||||
name: 'Vorgang',
|
||||
section: 'customer',
|
||||
type: 'cart',
|
||||
};
|
||||
|
||||
const action = actions.patchProcess({ processId: process.id, changes: { ...process, name: 'Test' } });
|
||||
const state = applicationReducer(
|
||||
{
|
||||
...initialState,
|
||||
processes: [process],
|
||||
},
|
||||
action,
|
||||
);
|
||||
expect(state.processes[0].name).toEqual('Test');
|
||||
});
|
||||
|
||||
it('should return unmodified state when id is not existing', () => {
|
||||
const initialState = INITIAL_APPLICATION_STATE;
|
||||
|
||||
const process: ApplicationProcess = {
|
||||
id: 1,
|
||||
name: 'Vorgang',
|
||||
section: 'customer',
|
||||
type: 'cart',
|
||||
};
|
||||
|
||||
const action = actions.patchProcess({ processId: process.id, changes: { ...process, id: 2 } });
|
||||
const state = applicationReducer(
|
||||
{
|
||||
...initialState,
|
||||
processes: [process],
|
||||
},
|
||||
action,
|
||||
);
|
||||
expect(state.processes).toEqual([process]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeProcess()', () => {
|
||||
it('should return initial state if no processes are registered in the state', () => {
|
||||
const initialState = INITIAL_APPLICATION_STATE;
|
||||
|
||||
const action = actions.removeProcess({ processId: 2 });
|
||||
const state = applicationReducer(initialState, action);
|
||||
expect(state).toEqual(initialState);
|
||||
});
|
||||
|
||||
it('should return the unmodified state if processId not found', () => {
|
||||
const initialState = INITIAL_APPLICATION_STATE;
|
||||
const modifiedState: ApplicationState = {
|
||||
...initialState,
|
||||
section: 'customer',
|
||||
processes: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Vorgang',
|
||||
section: 'customer',
|
||||
type: 'cart',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: 'Vorgang',
|
||||
section: 'customer',
|
||||
type: 'goods-out',
|
||||
},
|
||||
] as ApplicationProcess[],
|
||||
};
|
||||
|
||||
const action = actions.removeProcess({ processId: 2 });
|
||||
const state = applicationReducer(modifiedState, action);
|
||||
expect(state).toEqual(modifiedState);
|
||||
});
|
||||
|
||||
it('should return modified state, after process gets removed', () => {
|
||||
const initialState = INITIAL_APPLICATION_STATE;
|
||||
const modifiedState: ApplicationState = {
|
||||
...initialState,
|
||||
section: 'customer',
|
||||
processes: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Vorgang',
|
||||
section: 'customer',
|
||||
type: 'cart',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Vorgang',
|
||||
section: 'customer',
|
||||
type: 'goods-out',
|
||||
},
|
||||
] as ApplicationProcess[],
|
||||
};
|
||||
|
||||
const action = actions.removeProcess({ processId: 2 });
|
||||
const state = applicationReducer(modifiedState, action);
|
||||
expect(state.processes).toEqual([
|
||||
{
|
||||
id: 1,
|
||||
name: 'Vorgang',
|
||||
section: 'customer',
|
||||
type: 'cart',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setActivatedProcess()', () => {
|
||||
it('should return modified state with process.activated value', () => {
|
||||
const process: ApplicationProcess = {
|
||||
id: 3,
|
||||
name: 'Vorgang 3',
|
||||
section: 'customer',
|
||||
};
|
||||
const initialState: ApplicationState = {
|
||||
...INITIAL_APPLICATION_STATE,
|
||||
processes: [process],
|
||||
};
|
||||
|
||||
const action = actions.setActivatedProcess({ activatedProcessId: 3 });
|
||||
const state = applicationReducer(initialState, action);
|
||||
|
||||
expect(state.processes[0].activated).toBeDefined();
|
||||
});
|
||||
|
||||
it('should return modified state without process.activated value when activatedProcessId doesnt exist', () => {
|
||||
const process: ApplicationProcess = {
|
||||
id: 1,
|
||||
name: 'Vorgang 3',
|
||||
section: 'customer',
|
||||
};
|
||||
const initialState: ApplicationState = {
|
||||
...INITIAL_APPLICATION_STATE,
|
||||
processes: [process],
|
||||
};
|
||||
|
||||
const action = actions.setActivatedProcess({ activatedProcessId: 3 });
|
||||
const state = applicationReducer(initialState, action);
|
||||
|
||||
expect(state.processes[0].activated).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,56 +0,0 @@
|
||||
import { Action, createReducer, on } from '@ngrx/store';
|
||||
import {
|
||||
setSection,
|
||||
addProcess,
|
||||
removeProcess,
|
||||
setActivatedProcess,
|
||||
patchProcess,
|
||||
patchProcessData,
|
||||
setTitle,
|
||||
} from './application.actions';
|
||||
import { ApplicationState, INITIAL_APPLICATION_STATE } from './application.state';
|
||||
|
||||
const _applicationReducer = createReducer(
|
||||
INITIAL_APPLICATION_STATE,
|
||||
on(setTitle, (state, { title }) => ({ ...state, title })),
|
||||
on(setSection, (state, { section }) => ({ ...state, section })),
|
||||
on(addProcess, (state, { process }) => ({ ...state, processes: [...state.processes, { data: {}, ...process }] })),
|
||||
on(removeProcess, (state, { processId }) => {
|
||||
const processes = state?.processes?.filter((process) => process.id !== processId) || [];
|
||||
return { ...state, processes };
|
||||
}),
|
||||
on(setActivatedProcess, (state, { activatedProcessId }) => {
|
||||
const processes = state.processes.map((process) => {
|
||||
if (process.id === activatedProcessId) {
|
||||
return { ...process, activated: Date.now() };
|
||||
}
|
||||
return process;
|
||||
});
|
||||
|
||||
return { ...state, processes };
|
||||
}),
|
||||
on(patchProcess, (state, { processId, changes }) => {
|
||||
const processes = state.processes.map((process) => {
|
||||
if (process.id === processId) {
|
||||
return { ...process, ...changes, id: processId };
|
||||
}
|
||||
return process;
|
||||
});
|
||||
|
||||
return { ...state, processes };
|
||||
}),
|
||||
on(patchProcessData, (state, { processId, data }) => {
|
||||
const processes = state.processes.map((process) => {
|
||||
if (process.id === processId) {
|
||||
return { ...process, data: { ...(process.data || {}), ...data } };
|
||||
}
|
||||
return process;
|
||||
});
|
||||
|
||||
return { ...state, processes };
|
||||
}),
|
||||
);
|
||||
|
||||
export function applicationReducer(state: ApplicationState, action: Action) {
|
||||
return _applicationReducer(state, action);
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
// import { ApplicationState } from './application.state';
|
||||
// import { ApplicationProcess } from '../defs';
|
||||
// import * as selectors from './application.selectors';
|
||||
|
||||
// describe('applicationSelectors', () => {
|
||||
// it('should select the processes', () => {
|
||||
// const processes: ApplicationProcess[] = [{ id: 1, name: 'Vorgang 1', section: 'customer' }];
|
||||
// const state: ApplicationState = {
|
||||
// processes,
|
||||
// section: 'customer',
|
||||
// };
|
||||
// expect(selectors.selectProcesses.projector(state)).toEqual(processes);
|
||||
// });
|
||||
|
||||
// it('should select the section', () => {
|
||||
// const state: ApplicationState = {
|
||||
// processes: [],
|
||||
// section: 'customer',
|
||||
// };
|
||||
// expect(selectors.selectSection.projector(state)).toEqual('customer');
|
||||
// });
|
||||
|
||||
// it('should select the activatedProcess', () => {
|
||||
// const processes: ApplicationProcess[] = [
|
||||
// { id: 1, name: 'Vorgang 1', section: 'customer', activated: 100 },
|
||||
// { id: 2, name: 'Vorgang 2', section: 'customer', activated: 300 },
|
||||
// { id: 3, name: 'Vorgang 3', section: 'customer', activated: 200 },
|
||||
// ];
|
||||
// const state: ApplicationState = {
|
||||
// processes,
|
||||
// section: 'customer',
|
||||
// };
|
||||
// expect(selectors.selectActivatedProcess.projector(state)).toEqual(processes[1]);
|
||||
// });
|
||||
// });
|
||||
@@ -1,18 +0,0 @@
|
||||
import { createFeatureSelector, createSelector } from '@ngrx/store';
|
||||
import { ApplicationState } from './application.state';
|
||||
export const selectApplicationState = createFeatureSelector<ApplicationState>('core-application');
|
||||
|
||||
export const selectTitle = createSelector(selectApplicationState, (s) => s.title);
|
||||
|
||||
export const selectSection = createSelector(selectApplicationState, (s) => s.section);
|
||||
|
||||
export const selectProcesses = createSelector(selectApplicationState, (s) => s.processes);
|
||||
|
||||
export const selectActivatedProcess = createSelector(selectApplicationState, (s) =>
|
||||
s?.processes?.reduce((process, current) => {
|
||||
if (!process) {
|
||||
return current;
|
||||
}
|
||||
return process.activated > current.activated ? process : current;
|
||||
}, undefined),
|
||||
);
|
||||
@@ -1,13 +0,0 @@
|
||||
import { ApplicationProcess } from '../defs';
|
||||
|
||||
export interface ApplicationState {
|
||||
title: string;
|
||||
processes: ApplicationProcess[];
|
||||
section: 'customer' | 'branch';
|
||||
}
|
||||
|
||||
export const INITIAL_APPLICATION_STATE: ApplicationState = {
|
||||
title: '',
|
||||
processes: [],
|
||||
section: 'customer',
|
||||
};
|
||||
@@ -1,6 +0,0 @@
|
||||
// start:ng42.barrel
|
||||
export * from './application.actions';
|
||||
export * from './application.reducer';
|
||||
export * from './application.selectors';
|
||||
export * from './application.state';
|
||||
// end:ng42.barrel
|
||||
@@ -3,7 +3,7 @@ import { inject, Injectable } from '@angular/core';
|
||||
import { toSignal } from '@angular/core/rxjs-interop';
|
||||
import { EnvironmentService } from '@core/environment';
|
||||
import { UiConfirmModalComponent, UiModalResult, UiModalService } from '@ui/modal';
|
||||
import { injectNetworkStatus$ } from '../../app/services';
|
||||
import { injectNetworkStatus$ } from '@isa/core/connectivity';
|
||||
import { AuthService } from './auth.service';
|
||||
import { AuthService as IsaAuthService } from '@generated/swagger/isa-api';
|
||||
import { firstValueFrom, lastValueFrom } from 'rxjs';
|
||||
|
||||
@@ -1,22 +1,15 @@
|
||||
import { ModuleWithProviders, NgModule } from '@angular/core';
|
||||
import { EffectsModule } from '@ngrx/effects';
|
||||
import { StoreModule } from '@ngrx/store';
|
||||
import { EnvironmentProviders, makeEnvironmentProviders } from '@angular/core';
|
||||
import { provideEffects } from '@ngrx/effects';
|
||||
import { provideState } from '@ngrx/store';
|
||||
import { BreadcrumbService } from './breadcrumb.service';
|
||||
import { BreadcrumbEffects } from './store/breadcrumb.effect';
|
||||
import { breadcrumbReducer } from './store/breadcrumb.reducer';
|
||||
import { featureName } from './store/breadcrumb.state';
|
||||
|
||||
@NgModule()
|
||||
export class CoreBreadcrumbModule {
|
||||
static forRoot(): ModuleWithProviders<CoreBreadcrumbModule> {
|
||||
return {
|
||||
ngModule: CoreBreadcrumbForRootModule,
|
||||
};
|
||||
}
|
||||
export function provideCoreBreadcrumb(): EnvironmentProviders {
|
||||
return makeEnvironmentProviders([
|
||||
provideState({ name: featureName, reducer: breadcrumbReducer }),
|
||||
provideEffects(BreadcrumbEffects),
|
||||
BreadcrumbService,
|
||||
]);
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
imports: [StoreModule.forFeature(featureName, breadcrumbReducer), EffectsModule.forFeature([BreadcrumbEffects])],
|
||||
providers: [BreadcrumbService],
|
||||
})
|
||||
export class CoreBreadcrumbForRootModule {}
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import { ModuleWithProviders, NgModule } from '@angular/core';
|
||||
import { DomainAvailabilityService } from './availability.service';
|
||||
|
||||
@NgModule()
|
||||
export class DomainAvailabilityModule {
|
||||
static forRoot(): ModuleWithProviders<DomainAvailabilityModule> {
|
||||
return {
|
||||
ngModule: DomainAvailabilityModule,
|
||||
providers: [DomainAvailabilityService],
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,15 @@ import {
|
||||
AvailabilityType,
|
||||
} from '@generated/swagger/availability-api';
|
||||
import { AvailabilityDTO as CatAvailabilityDTO } from '@generated/swagger/cat-search-api';
|
||||
import { map, shareReplay, switchMap, withLatestFrom, mergeMap, timeout, first } from 'rxjs/operators';
|
||||
import {
|
||||
map,
|
||||
shareReplay,
|
||||
switchMap,
|
||||
withLatestFrom,
|
||||
mergeMap,
|
||||
timeout,
|
||||
first,
|
||||
} from 'rxjs/operators';
|
||||
import { isArray, memorize } from '@utils/common';
|
||||
import { LogisticianDTO, LogisticianService } from '@generated/swagger/oms-api';
|
||||
import {
|
||||
@@ -30,7 +38,7 @@ import { AvailabilityByBranchDTO, ItemData, Ssc } from './defs';
|
||||
import { Availability } from './defs/availability';
|
||||
import { isEmpty } from 'lodash';
|
||||
|
||||
@Injectable()
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class DomainAvailabilityService {
|
||||
// Ticket #3378 Keep Result List Items and Details Page SSC in sync
|
||||
sscs$ = new BehaviorSubject<Array<Ssc>>([]);
|
||||
@@ -45,8 +53,12 @@ export class DomainAvailabilityService {
|
||||
) {}
|
||||
|
||||
@memorize({ ttl: 10000 })
|
||||
memorizedAvailabilityShippingAvailability(request: Array<AvailabilityRequestDTO>) {
|
||||
return this._availabilityService.AvailabilityShippingAvailability(request).pipe(shareReplay(1));
|
||||
memorizedAvailabilityShippingAvailability(
|
||||
request: Array<AvailabilityRequestDTO>,
|
||||
) {
|
||||
return this._availabilityService
|
||||
.AvailabilityShippingAvailability(request)
|
||||
.pipe(shareReplay(1));
|
||||
}
|
||||
|
||||
@memorize()
|
||||
@@ -60,7 +72,9 @@ export class DomainAvailabilityService {
|
||||
@memorize()
|
||||
getTakeAwaySupplier(): Observable<SupplierDTO> {
|
||||
return this._supplierService.StoreCheckoutSupplierGetSuppliers({}).pipe(
|
||||
map(({ result }) => result?.find((supplier) => supplier?.supplierNumber === 'F')),
|
||||
map(({ result }) =>
|
||||
result?.find((supplier) => supplier?.supplierNumber === 'F'),
|
||||
),
|
||||
shareReplay(1),
|
||||
);
|
||||
}
|
||||
@@ -117,7 +131,9 @@ export class DomainAvailabilityService {
|
||||
@memorize({})
|
||||
getLogisticians(): Observable<LogisticianDTO> {
|
||||
return this._logisticanService.LogisticianGetLogisticians({}).pipe(
|
||||
map((response) => response.result?.find((l) => l.logisticianNumber === '2470')),
|
||||
map((response) =>
|
||||
response.result?.find((l) => l.logisticianNumber === '2470'),
|
||||
),
|
||||
shareReplay(1),
|
||||
);
|
||||
}
|
||||
@@ -133,22 +149,27 @@ export class DomainAvailabilityService {
|
||||
price: PriceDTO;
|
||||
quantity: number;
|
||||
}): Observable<AvailabilityByBranchDTO[]> {
|
||||
return this._stockService.StockStockRequest({ stockRequest: { branchIds, itemId } }).pipe(
|
||||
return this._stockService
|
||||
.StockStockRequest({ stockRequest: { branchIds, itemId } })
|
||||
.pipe(
|
||||
map((response) => response.result),
|
||||
withLatestFrom(this.getTakeAwaySupplier()),
|
||||
map(([result, supplier]) => {
|
||||
const availabilities: AvailabilityByBranchDTO[] = result.map((stockInfo) => {
|
||||
const availabilities: AvailabilityByBranchDTO[] = result.map(
|
||||
(stockInfo) => {
|
||||
return {
|
||||
availableQuantity: stockInfo.availableQuantity,
|
||||
availabilityType: quantity <= stockInfo.inStock ? 1024 : 1, // 1024 (=Available)
|
||||
inStock: stockInfo.inStock,
|
||||
supplierSSC: quantity <= stockInfo.inStock ? '999' : '',
|
||||
supplierSSCText: quantity <= stockInfo.inStock ? 'Filialentnahme' : '',
|
||||
supplierSSCText:
|
||||
quantity <= stockInfo.inStock ? 'Filialentnahme' : '',
|
||||
price,
|
||||
supplier: { id: supplier?.id },
|
||||
branchId: stockInfo.branchId,
|
||||
};
|
||||
});
|
||||
},
|
||||
);
|
||||
return availabilities;
|
||||
}),
|
||||
shareReplay(1),
|
||||
@@ -165,11 +186,16 @@ export class DomainAvailabilityService {
|
||||
quantity: number;
|
||||
branch?: BranchDTO;
|
||||
}): Observable<AvailabilityDTO> {
|
||||
const request = branch ? this.getStockByBranch(branch.id) : this.getDefaultStock();
|
||||
const request = branch
|
||||
? this.getStockByBranch(branch.id)
|
||||
: this.getDefaultStock();
|
||||
return request.pipe(
|
||||
switchMap((s) =>
|
||||
combineLatest([
|
||||
this._stockService.StockInStock({ articleIds: [item.itemId], stockId: s.id }),
|
||||
this._stockService.StockInStock({
|
||||
articleIds: [item.itemId],
|
||||
stockId: s.id,
|
||||
}),
|
||||
this.getTakeAwaySupplier(),
|
||||
this.getDefaultBranch(),
|
||||
]),
|
||||
@@ -201,11 +227,19 @@ export class DomainAvailabilityService {
|
||||
quantity: number;
|
||||
}): Observable<AvailabilityDTO> {
|
||||
return combineLatest([
|
||||
this._stockService.StockStockRequest({ stockRequest: { branchIds: [branch.id], itemId } }),
|
||||
this._stockService.StockStockRequest({
|
||||
stockRequest: { branchIds: [branch.id], itemId },
|
||||
}),
|
||||
this.getTakeAwaySupplier(),
|
||||
]).pipe(
|
||||
map(([response, supplier]) => {
|
||||
return this._mapToTakeAwayAvailability({ response, supplier, branchId: branch.id, quantity, price });
|
||||
return this._mapToTakeAwayAvailability({
|
||||
response,
|
||||
supplier,
|
||||
branchId: branch.id,
|
||||
quantity,
|
||||
price,
|
||||
});
|
||||
}),
|
||||
shareReplay(1),
|
||||
);
|
||||
@@ -222,9 +256,13 @@ export class DomainAvailabilityService {
|
||||
quantity: number;
|
||||
branchId?: number;
|
||||
}): Observable<AvailabilityDTO> {
|
||||
const request = branchId ? this.getStockByBranch(branchId) : this.getDefaultStock();
|
||||
const request = branchId
|
||||
? this.getStockByBranch(branchId)
|
||||
: this.getDefaultStock();
|
||||
return request.pipe(
|
||||
switchMap((s) => this._stockService.StockInStockByEAN({ eans, stockId: s.id })),
|
||||
switchMap((s) =>
|
||||
this._stockService.StockInStockByEAN({ eans, stockId: s.id }),
|
||||
),
|
||||
withLatestFrom(this.getTakeAwaySupplier(), this.getDefaultBranch()),
|
||||
map(([response, supplier, defaultBranch]) => {
|
||||
return this._mapToTakeAwayAvailability({
|
||||
@@ -239,10 +277,19 @@ export class DomainAvailabilityService {
|
||||
);
|
||||
}
|
||||
|
||||
getTakeAwayAvailabilitiesByEans({ eans }: { eans: string[] }): Observable<StockInfoDTO[]> {
|
||||
getTakeAwayAvailabilitiesByEans({
|
||||
eans,
|
||||
}: {
|
||||
eans: string[];
|
||||
}): Observable<StockInfoDTO[]> {
|
||||
const eansFiltered = Array.from(new Set(eans));
|
||||
return this.getDefaultStock().pipe(
|
||||
switchMap((s) => this._stockService.StockInStockByEAN({ eans: eansFiltered, stockId: s.id })),
|
||||
switchMap((s) =>
|
||||
this._stockService.StockInStockByEAN({
|
||||
eans: eansFiltered,
|
||||
stockId: s.id,
|
||||
}),
|
||||
),
|
||||
withLatestFrom(this.getTakeAwaySupplier(), this.getDefaultBranch()),
|
||||
map((response) => response[0].result),
|
||||
shareReplay(1),
|
||||
@@ -276,7 +323,13 @@ export class DomainAvailabilityService {
|
||||
}
|
||||
|
||||
@memorize({ ttl: 10000 })
|
||||
getDeliveryAvailability({ item, quantity }: { item: ItemData; quantity: number }): Observable<AvailabilityDTO> {
|
||||
getDeliveryAvailability({
|
||||
item,
|
||||
quantity,
|
||||
}: {
|
||||
item: ItemData;
|
||||
quantity: number;
|
||||
}): Observable<AvailabilityDTO> {
|
||||
return this.memorizedAvailabilityShippingAvailability([
|
||||
{
|
||||
ean: item?.ean,
|
||||
@@ -292,7 +345,13 @@ export class DomainAvailabilityService {
|
||||
}
|
||||
|
||||
@memorize({ ttl: 10000 })
|
||||
getDigDeliveryAvailability({ item, quantity }: { item: ItemData; quantity: number }): Observable<AvailabilityDTO> {
|
||||
getDigDeliveryAvailability({
|
||||
item,
|
||||
quantity,
|
||||
}: {
|
||||
item: ItemData;
|
||||
quantity: number;
|
||||
}): Observable<AvailabilityDTO> {
|
||||
return this.memorizedAvailabilityShippingAvailability([
|
||||
{
|
||||
qty: quantity,
|
||||
@@ -312,7 +371,10 @@ export class DomainAvailabilityService {
|
||||
sscText: preferred?.sscText,
|
||||
supplier: { id: preferred?.supplierId },
|
||||
isPrebooked: preferred?.isPrebooked,
|
||||
estimatedShippingDate: preferred?.requestStatusCode === '32' ? preferred?.altAt : preferred?.at,
|
||||
estimatedShippingDate:
|
||||
preferred?.requestStatusCode === '32'
|
||||
? preferred?.altAt
|
||||
: preferred?.at,
|
||||
estimatedDelivery: preferred?.estimatedDelivery,
|
||||
price: preferred?.price,
|
||||
logistician: { id: preferred?.logisticianId },
|
||||
@@ -343,7 +405,11 @@ export class DomainAvailabilityService {
|
||||
return currentBranch$.pipe(
|
||||
timeout(5000),
|
||||
mergeMap((defaultBranch) =>
|
||||
this.getPickUpAvailability({ item, quantity, branch: branch ?? defaultBranch }).pipe(
|
||||
this.getPickUpAvailability({
|
||||
item,
|
||||
quantity,
|
||||
branch: branch ?? defaultBranch,
|
||||
}).pipe(
|
||||
mergeMap((availability) =>
|
||||
logistician$.pipe(
|
||||
map((logistician) => ({
|
||||
@@ -359,7 +425,11 @@ export class DomainAvailabilityService {
|
||||
}
|
||||
|
||||
@memorize({ ttl: 10000 })
|
||||
getDownloadAvailability({ item }: { item: ItemData }): Observable<AvailabilityDTO> {
|
||||
getDownloadAvailability({
|
||||
item,
|
||||
}: {
|
||||
item: ItemData;
|
||||
}): Observable<AvailabilityDTO> {
|
||||
return this.memorizedAvailabilityShippingAvailability([
|
||||
{
|
||||
ean: item?.ean,
|
||||
@@ -378,7 +448,10 @@ export class DomainAvailabilityService {
|
||||
sscText: preferred?.sscText,
|
||||
supplier: { id: preferred?.supplierId },
|
||||
isPrebooked: preferred?.isPrebooked,
|
||||
estimatedShippingDate: preferred?.requestStatusCode === '32' ? preferred?.altAt : preferred?.at,
|
||||
estimatedShippingDate:
|
||||
preferred?.requestStatusCode === '32'
|
||||
? preferred?.altAt
|
||||
: preferred?.at,
|
||||
price: preferred?.price,
|
||||
supplierProductNumber: preferred?.supplierProductNumber,
|
||||
logistician: { id: preferred?.logisticianId },
|
||||
@@ -392,12 +465,18 @@ export class DomainAvailabilityService {
|
||||
}
|
||||
|
||||
@memorize({ ttl: 10000 })
|
||||
getTakeAwayAvailabilities(items: { id: number; price: PriceDTO }[], branchId: number) {
|
||||
getTakeAwayAvailabilities(
|
||||
items: { id: number; price: PriceDTO }[],
|
||||
branchId: number,
|
||||
) {
|
||||
return this._stockService.StockGetStocksByBranch({ branchId }).pipe(
|
||||
map((req) => req.result?.find((_) => true)?.id),
|
||||
switchMap((stockId) =>
|
||||
stockId
|
||||
? this._stockService.StockInStock({ articleIds: items.map((i) => i.id), stockId })
|
||||
? this._stockService.StockInStock({
|
||||
articleIds: items.map((i) => i.id),
|
||||
stockId,
|
||||
})
|
||||
: of({ result: [] } as ResponseArgsOfIEnumerableOfStockInfoDTO),
|
||||
),
|
||||
timeout(20000),
|
||||
@@ -417,10 +496,19 @@ export class DomainAvailabilityService {
|
||||
}
|
||||
|
||||
@memorize({ ttl: 10000 })
|
||||
getPickUpAvailabilities(payload: AvailabilityRequestDTO[], preferred?: boolean) {
|
||||
return this._availabilityService.AvailabilityStoreAvailability(payload).pipe(
|
||||
getPickUpAvailabilities(
|
||||
payload: AvailabilityRequestDTO[],
|
||||
preferred?: boolean,
|
||||
) {
|
||||
return this._availabilityService
|
||||
.AvailabilityStoreAvailability(payload)
|
||||
.pipe(
|
||||
timeout(20000),
|
||||
map((response) => (preferred ? this._mapToPickUpAvailability(response.result) : response.result)),
|
||||
map((response) =>
|
||||
preferred
|
||||
? this._mapToPickUpAvailability(response.result)
|
||||
: response.result,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -448,7 +536,10 @@ export class DomainAvailabilityService {
|
||||
timeout(20000),
|
||||
switchMap((availability) =>
|
||||
logistician$.pipe(
|
||||
map((logistician) => ({ availability: [...availability], logistician: { id: logistician.id } })),
|
||||
map((logistician) => ({
|
||||
availability: [...availability],
|
||||
logistician: { id: logistician.id },
|
||||
})),
|
||||
),
|
||||
),
|
||||
shareReplay(1),
|
||||
@@ -465,7 +556,10 @@ export class DomainAvailabilityService {
|
||||
return availability?.price || catalogAvailability?.price;
|
||||
case 'delivery':
|
||||
case 'dig-delivery':
|
||||
if (catalogAvailability?.price?.value?.value < availability?.price?.value?.value) {
|
||||
if (
|
||||
catalogAvailability?.price?.value?.value <
|
||||
availability?.price?.value?.value
|
||||
) {
|
||||
return catalogAvailability?.price;
|
||||
}
|
||||
return availability?.price || catalogAvailability?.price;
|
||||
@@ -477,7 +571,9 @@ export class DomainAvailabilityService {
|
||||
if (availability?.supplier?.id === 16 && availability?.inStock == 0) {
|
||||
return false;
|
||||
}
|
||||
return [2, 32, 256, 1024, 2048, 4096].some((code) => availability?.availabilityType === code);
|
||||
return [2, 32, 256, 1024, 2048, 4096].some(
|
||||
(code) => availability?.availabilityType === code,
|
||||
);
|
||||
}
|
||||
|
||||
private _mapToTakeAwayAvailability({
|
||||
@@ -524,7 +620,10 @@ export class DomainAvailabilityService {
|
||||
|
||||
const availability = {
|
||||
itemId: stockInfo.itemId,
|
||||
availabilityType: quantity <= inStock ? (1024 as AvailabilityType) : (1 as AvailabilityType), // 1024 (=Available)
|
||||
availabilityType:
|
||||
quantity <= inStock
|
||||
? (1024 as AvailabilityType)
|
||||
: (1 as AvailabilityType), // 1024 (=Available)
|
||||
inStock: inStock,
|
||||
supplierSSC: quantity <= inStock ? '999' : '',
|
||||
supplierSSCText: quantity <= inStock ? 'Filialentnahme' : '',
|
||||
@@ -539,7 +638,10 @@ export class DomainAvailabilityService {
|
||||
): Availability<AvailabilityDTO, SwaggerAvailabilityDTO>[] {
|
||||
if (isArray(availabilities)) {
|
||||
const preferred = availabilities.filter((f) => f.preferred === 1);
|
||||
const totalAvailable = availabilities.reduce((sum, av) => sum + (av?.qty || 0), 0);
|
||||
const totalAvailable = availabilities.reduce(
|
||||
(sum, av) => sum + (av?.qty || 0),
|
||||
0,
|
||||
);
|
||||
|
||||
return preferred.map((p) => {
|
||||
return [
|
||||
@@ -550,7 +652,8 @@ export class DomainAvailabilityService {
|
||||
sscText: p?.sscText,
|
||||
supplier: { id: p?.supplierId },
|
||||
isPrebooked: p?.isPrebooked,
|
||||
estimatedShippingDate: p?.requestStatusCode === '32' ? p?.altAt : p?.at,
|
||||
estimatedShippingDate:
|
||||
p?.requestStatusCode === '32' ? p?.altAt : p?.at,
|
||||
price: p?.price,
|
||||
inStock: totalAvailable,
|
||||
supplierProductNumber: p?.supplierProductNumber,
|
||||
@@ -565,7 +668,9 @@ export class DomainAvailabilityService {
|
||||
}
|
||||
}
|
||||
|
||||
private _mapToShippingAvailability(availabilities: SwaggerAvailabilityDTO[]): AvailabilityDTO[] {
|
||||
private _mapToShippingAvailability(
|
||||
availabilities: SwaggerAvailabilityDTO[],
|
||||
): AvailabilityDTO[] {
|
||||
const preferred = availabilities.filter((f) => f.preferred === 1);
|
||||
return preferred.map((p) => {
|
||||
return {
|
||||
@@ -585,7 +690,10 @@ export class DomainAvailabilityService {
|
||||
});
|
||||
}
|
||||
|
||||
getInStockByEan(params: { eans: string[]; branchId?: number }): Observable<Record<string, StockInfoDTO>> {
|
||||
getInStockByEan(params: {
|
||||
eans: string[];
|
||||
branchId?: number;
|
||||
}): Observable<Record<string, StockInfoDTO>> {
|
||||
let branchId$ = of(params.branchId);
|
||||
|
||||
if (!params.branchId) {
|
||||
@@ -597,31 +705,41 @@ export class DomainAvailabilityService {
|
||||
|
||||
const stock$ = branchId$.pipe(
|
||||
mergeMap((branchId) =>
|
||||
this._stockService.StockGetStocksByBranch({ branchId }).pipe(map((response) => response.result?.[0])),
|
||||
this._stockService
|
||||
.StockGetStocksByBranch({ branchId })
|
||||
.pipe(map((response) => response.result?.[0])),
|
||||
),
|
||||
);
|
||||
|
||||
return stock$.pipe(
|
||||
mergeMap((stock) =>
|
||||
this._stockService.StockInStockByEAN({ eans: params.eans, stockId: stock.id }).pipe(
|
||||
this._stockService
|
||||
.StockInStockByEAN({ eans: params.eans, stockId: stock.id })
|
||||
.pipe(
|
||||
map((response) => {
|
||||
const result = response.result ?? [];
|
||||
|
||||
for (const stockInfo of result) {
|
||||
stockInfo.ean = stockInfo.ean;
|
||||
}
|
||||
|
||||
return result.reduce<Record<string, StockInfoDTO>>((acc, stockInfo) => {
|
||||
return result.reduce<Record<string, StockInfoDTO>>(
|
||||
(acc, stockInfo) => {
|
||||
acc[stockInfo.ean] = stockInfo;
|
||||
return acc;
|
||||
}, {});
|
||||
},
|
||||
{},
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
getInStock({ itemIds, branchId }: { itemIds: number[]; branchId: number }): Observable<StockInfoDTO[]> {
|
||||
getInStock({
|
||||
itemIds,
|
||||
branchId,
|
||||
}: {
|
||||
itemIds: number[];
|
||||
branchId: number;
|
||||
}): Observable<StockInfoDTO[]> {
|
||||
return this.getStockByBranch(branchId).pipe(
|
||||
mergeMap((stock) =>
|
||||
this._stockService
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
export * from './availability.module';
|
||||
export * from './availability.service';
|
||||
export * from './defs';
|
||||
export * from './in-stock.service';
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
import { ModuleWithProviders, NgModule } from '@angular/core';
|
||||
import { DomainCatalogService } from './catalog.service';
|
||||
import { ThumbnailUrlPipe } from './thumbnail-url.pipe';
|
||||
import { DomainCatalogThumbnailService } from './thumbnail.service';
|
||||
|
||||
@NgModule({
|
||||
declarations: [ThumbnailUrlPipe],
|
||||
imports: [],
|
||||
exports: [ThumbnailUrlPipe],
|
||||
})
|
||||
export class DomainCatalogModule {
|
||||
static forRoot(): ModuleWithProviders<DomainCatalogModule> {
|
||||
return {
|
||||
ngModule: DomainCatalogModule,
|
||||
providers: [DomainCatalogService, DomainCatalogThumbnailService],
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
import { memorize } from '@utils/common';
|
||||
import { map, share, shareReplay } from 'rxjs/operators';
|
||||
|
||||
@Injectable()
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class DomainCatalogService {
|
||||
constructor(
|
||||
private searchService: SearchService,
|
||||
@@ -34,7 +34,9 @@ export class DomainCatalogService {
|
||||
}
|
||||
|
||||
getSearchHistory({ take }: { take: number }) {
|
||||
return this.searchService.SearchHistory(take ?? 5).pipe(map((res) => res.result));
|
||||
return this.searchService
|
||||
.SearchHistory(take ?? 5)
|
||||
.pipe(map((res) => res.result));
|
||||
}
|
||||
|
||||
@memorize({ ttl: 120000 })
|
||||
@@ -84,7 +86,11 @@ export class DomainCatalogService {
|
||||
}
|
||||
|
||||
@memorize()
|
||||
getPromotionPoints({ items }: { items: { id: number; quantity: number; price?: number }[] }) {
|
||||
getPromotionPoints({
|
||||
items,
|
||||
}: {
|
||||
items: { id: number; quantity: number; price?: number }[];
|
||||
}) {
|
||||
return this.promotionService.PromotionLesepunkte(items).pipe(shareReplay());
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
export * from './catalog.module';
|
||||
export * from './catalog.service';
|
||||
export * from './thumbnail-url.pipe';
|
||||
export * from './thumbnail.service';
|
||||
|
||||
@@ -6,7 +6,7 @@ import { DomainCatalogThumbnailService } from './thumbnail.service';
|
||||
@Pipe({
|
||||
name: 'thumbnailUrl',
|
||||
pure: false,
|
||||
standalone: false,
|
||||
standalone: true,
|
||||
})
|
||||
export class ThumbnailUrlPipe implements PipeTransform, OnDestroy {
|
||||
private input$ = new BehaviorSubject<{ width?: number; height?: number; ean?: string }>(undefined);
|
||||
|
||||
@@ -3,15 +3,23 @@ import { memorize } from '@utils/common';
|
||||
import { map, shareReplay } from 'rxjs/operators';
|
||||
import { DomainCatalogService } from './catalog.service';
|
||||
|
||||
@Injectable()
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class DomainCatalogThumbnailService {
|
||||
constructor(private domainCatalogService: DomainCatalogService) {}
|
||||
|
||||
@memorize()
|
||||
getThumnaulUrl({ ean, height, width }: { width?: number; height?: number; ean?: string }) {
|
||||
getThumnaulUrl({
|
||||
ean,
|
||||
height,
|
||||
width,
|
||||
}: {
|
||||
width?: number;
|
||||
height?: number;
|
||||
ean?: string;
|
||||
}) {
|
||||
return this.domainCatalogService.getSettings().pipe(
|
||||
map((settings) => {
|
||||
let thumbnailUrl = settings.imageUrl.replace(/{ean}/, ean);
|
||||
const thumbnailUrl = settings.imageUrl.replace(/{ean}/, ean);
|
||||
return thumbnailUrl;
|
||||
}),
|
||||
shareReplay(),
|
||||
|
||||
@@ -1,29 +1,15 @@
|
||||
import { ModuleWithProviders, NgModule } from '@angular/core';
|
||||
import { StoreModule } from '@ngrx/store';
|
||||
import { EnvironmentProviders, makeEnvironmentProviders } from '@angular/core';
|
||||
import { provideEffects } from '@ngrx/effects';
|
||||
import { provideState } from '@ngrx/store';
|
||||
import { DomainCheckoutService } from './checkout.service';
|
||||
import { DomainCheckoutEffects } from './store/domain-checkout.effects';
|
||||
import { domainCheckoutReducer } from './store/domain-checkout.reducer';
|
||||
import { storeFeatureName } from './store/domain-checkout.state';
|
||||
import { EffectsModule } from '@ngrx/effects';
|
||||
import { DomainCheckoutEffects } from './store/domain-checkout.effects';
|
||||
|
||||
@NgModule({
|
||||
declarations: [],
|
||||
imports: [StoreModule.forFeature(storeFeatureName, domainCheckoutReducer)],
|
||||
providers: [DomainCheckoutService],
|
||||
})
|
||||
export class DomainCheckoutModule {
|
||||
static forRoot(): ModuleWithProviders<DomainCheckoutModule> {
|
||||
return {
|
||||
ngModule: RootDomainCheckoutModule,
|
||||
providers: [DomainCheckoutService],
|
||||
};
|
||||
}
|
||||
export function provideDomainCheckout(): EnvironmentProviders {
|
||||
return makeEnvironmentProviders([
|
||||
provideState({ name: storeFeatureName, reducer: domainCheckoutReducer }),
|
||||
provideEffects(DomainCheckoutEffects),
|
||||
DomainCheckoutService,
|
||||
]);
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
StoreModule.forFeature(storeFeatureName, domainCheckoutReducer),
|
||||
EffectsModule.forFeature([DomainCheckoutEffects]),
|
||||
],
|
||||
})
|
||||
export class RootDomainCheckoutModule {}
|
||||
|
||||
@@ -1071,7 +1071,7 @@ export class DomainCheckoutService {
|
||||
});
|
||||
} else if (orderType === 'B2B-Versand') {
|
||||
const branch = await this.applicationService
|
||||
.getSelectedBranch$(processId)
|
||||
.getSelectedBranch$()
|
||||
.pipe(first())
|
||||
.toPromise();
|
||||
availability$ =
|
||||
@@ -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 {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { InfoService } from '@generated/swagger/isa-api';
|
||||
|
||||
@Injectable()
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class DomainDashboardService {
|
||||
constructor(private readonly _infoService: InfoService) {}
|
||||
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import { ModuleWithProviders, NgModule } from '@angular/core';
|
||||
import { DomainDashboardService } from './dashboard.service';
|
||||
|
||||
@NgModule({})
|
||||
export class DomainIsaModule {
|
||||
static forRoot(): ModuleWithProviders<DomainIsaModule> {
|
||||
return {
|
||||
ngModule: DomainIsaModule,
|
||||
providers: [DomainDashboardService],
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,2 @@
|
||||
export * from './dashboard.service';
|
||||
export * from './defs';
|
||||
export * from './domain-isa.module';
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { AbholfachService, AutocompleteTokenDTO, QueryTokenDTO } from '@generated/swagger/oms-api';
|
||||
import {
|
||||
AbholfachService,
|
||||
AutocompleteTokenDTO,
|
||||
QueryTokenDTO,
|
||||
} from '@generated/swagger/oms-api';
|
||||
import { DateAdapter } from '@ui/common';
|
||||
import { memorize } from '@utils/common';
|
||||
import { shareReplay } from 'rxjs/operators';
|
||||
@Injectable()
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class DomainGoodsService {
|
||||
constructor(
|
||||
private abholfachService: AbholfachService,
|
||||
@@ -19,11 +23,15 @@ export class DomainGoodsService {
|
||||
}
|
||||
|
||||
wareneingangComplete(autocompleteToken: AutocompleteTokenDTO) {
|
||||
return this.abholfachService.AbholfachWareneingangAutocomplete(autocompleteToken);
|
||||
return this.abholfachService.AbholfachWareneingangAutocomplete(
|
||||
autocompleteToken,
|
||||
);
|
||||
}
|
||||
|
||||
warenausgabeComplete(autocompleteToken: AutocompleteTokenDTO) {
|
||||
return this.abholfachService.AbholfachWarenausgabeAutocomplete(autocompleteToken);
|
||||
return this.abholfachService.AbholfachWarenausgabeAutocomplete(
|
||||
autocompleteToken,
|
||||
);
|
||||
}
|
||||
|
||||
getWareneingangItemByOrderNumber(orderNumber: string) {
|
||||
@@ -81,12 +89,16 @@ export class DomainGoodsService {
|
||||
|
||||
@memorize()
|
||||
goodsInQuerySettings() {
|
||||
return this.abholfachService.AbholfachWareneingangQuerySettings().pipe(shareReplay());
|
||||
return this.abholfachService
|
||||
.AbholfachWareneingangQuerySettings()
|
||||
.pipe(shareReplay());
|
||||
}
|
||||
|
||||
@memorize()
|
||||
goodsOutQuerySettings() {
|
||||
return this.abholfachService.AbholfachWarenausgabeQuerySettings().pipe(shareReplay());
|
||||
return this.abholfachService
|
||||
.AbholfachWarenausgabeQuerySettings()
|
||||
.pipe(shareReplay());
|
||||
}
|
||||
|
||||
goodsInList(queryToken: QueryTokenDTO) {
|
||||
@@ -95,7 +107,9 @@ export class DomainGoodsService {
|
||||
|
||||
@memorize()
|
||||
goodsInListQuerySettings() {
|
||||
return this.abholfachService.AbholfachWareneingangslisteQuerySettings().pipe(shareReplay());
|
||||
return this.abholfachService
|
||||
.AbholfachWareneingangslisteQuerySettings()
|
||||
.pipe(shareReplay());
|
||||
}
|
||||
|
||||
goodsInCleanupList() {
|
||||
|
||||
@@ -2,6 +2,5 @@ export * from './action-handler-services';
|
||||
export * from './action-handlers';
|
||||
export * from './customer-order.service';
|
||||
export * from './goods.service';
|
||||
export * from './oms.module';
|
||||
export * from './oms.service';
|
||||
export * from './receipt.service';
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
import { ModuleWithProviders, NgModule } from '@angular/core';
|
||||
import { DomainGoodsService } from './goods.service';
|
||||
import { DomainOmsService } from './oms.service';
|
||||
import { DomainReceiptService } from './receipt.service';
|
||||
|
||||
@NgModule()
|
||||
export class DomainOmsModule {
|
||||
static forRoot(): ModuleWithProviders<DomainOmsModule> {
|
||||
return {
|
||||
ngModule: DomainOmsModule,
|
||||
providers: [DomainOmsService, DomainGoodsService, DomainReceiptService],
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,7 @@ import { memorize } from '@utils/common';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map, shareReplay } from 'rxjs/operators';
|
||||
|
||||
@Injectable()
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class DomainOmsService {
|
||||
constructor(
|
||||
private orderService: OrderService,
|
||||
@@ -33,9 +33,16 @@ export class DomainOmsService {
|
||||
private _orderCheckoutService: OrderCheckoutService,
|
||||
) {}
|
||||
|
||||
getOrderItemsByCustomerNumber(customerNumber: string, skip: number): Observable<OrderListItemDTO[]> {
|
||||
getOrderItemsByCustomerNumber(
|
||||
customerNumber: string,
|
||||
skip: number,
|
||||
): Observable<OrderListItemDTO[]> {
|
||||
return this.orderService
|
||||
.OrderGetOrdersByBuyerNumber({ buyerNumber: customerNumber, take: 20, skip })
|
||||
.OrderGetOrdersByBuyerNumber({
|
||||
buyerNumber: customerNumber,
|
||||
take: 20,
|
||||
skip,
|
||||
})
|
||||
.pipe(map((orders) => orders.result));
|
||||
}
|
||||
|
||||
@@ -55,7 +62,9 @@ export class DomainOmsService {
|
||||
|
||||
getReceipts(
|
||||
orderItemSubsetIds: number[],
|
||||
): Observable<ValueTupleOfLongAndReceiptTypeAndEntityDTOContainerOfReceiptDTO[]> {
|
||||
): Observable<
|
||||
ValueTupleOfLongAndReceiptTypeAndEntityDTOContainerOfReceiptDTO[]
|
||||
> {
|
||||
return this.receiptService
|
||||
.ReceiptGetReceiptsByOrderItemSubset({
|
||||
payload: {
|
||||
@@ -68,25 +77,43 @@ export class DomainOmsService {
|
||||
}
|
||||
|
||||
getReorderReasons() {
|
||||
return this._orderCheckoutService.OrderCheckoutGetReorderReasons().pipe(map((response) => response.result));
|
||||
return this._orderCheckoutService
|
||||
.OrderCheckoutGetReorderReasons()
|
||||
.pipe(map((response) => response.result));
|
||||
}
|
||||
|
||||
@memorize()
|
||||
getVATs() {
|
||||
return this.vatService.VATGetVATs({}).pipe(map((response) => response.result));
|
||||
return this.vatService
|
||||
.VATGetVATs({})
|
||||
.pipe(map((response) => response.result));
|
||||
}
|
||||
|
||||
// ttl 4 Stunden
|
||||
@memorize({ ttl: 14400000 })
|
||||
getStockStatusCodes({ supplierId, eagerLoading = 0 }: { supplierId: number; eagerLoading?: number }) {
|
||||
return this.stockStatusCodeService.StockStatusCodeGetStockStatusCodes({ supplierId, eagerLoading }).pipe(
|
||||
getStockStatusCodes({
|
||||
supplierId,
|
||||
eagerLoading = 0,
|
||||
}: {
|
||||
supplierId: number;
|
||||
eagerLoading?: number;
|
||||
}) {
|
||||
return this.stockStatusCodeService
|
||||
.StockStatusCodeGetStockStatusCodes({ supplierId, eagerLoading })
|
||||
.pipe(
|
||||
map((response) => response.result),
|
||||
shareReplay(),
|
||||
);
|
||||
}
|
||||
|
||||
patchOrderItem(payload: { orderItemId: number; orderId: number; orderItem: Partial<OrderItemDTO> }) {
|
||||
return this.orderService.OrderPatchOrderItem(payload).pipe(map((response) => response.result));
|
||||
patchOrderItem(payload: {
|
||||
orderItemId: number;
|
||||
orderId: number;
|
||||
orderItem: Partial<OrderItemDTO>;
|
||||
}) {
|
||||
return this.orderService
|
||||
.OrderPatchOrderItem(payload)
|
||||
.pipe(map((response) => response.result));
|
||||
}
|
||||
|
||||
patchOrderItemSubset(payload: {
|
||||
@@ -95,7 +122,9 @@ export class DomainOmsService {
|
||||
orderId: number;
|
||||
orderItemSubset: Partial<OrderItemSubsetDTO>;
|
||||
}) {
|
||||
return this.orderService.OrderPatchOrderItemSubset(payload).pipe(map((response) => response.result));
|
||||
return this.orderService
|
||||
.OrderPatchOrderItemSubset(payload)
|
||||
.pipe(map((response) => response.result));
|
||||
}
|
||||
|
||||
patchComment({
|
||||
@@ -150,13 +179,20 @@ export class DomainOmsService {
|
||||
orderItemSubsetId,
|
||||
orderItemSubset: {
|
||||
estimatedShippingDate:
|
||||
estimatedShippingDate instanceof Date ? estimatedShippingDate.toJSON() : estimatedShippingDate,
|
||||
estimatedShippingDate instanceof Date
|
||||
? estimatedShippingDate.toJSON()
|
||||
: estimatedShippingDate,
|
||||
},
|
||||
})
|
||||
.pipe(map((response) => response.result));
|
||||
}
|
||||
|
||||
setPickUpDeadline(orderId: number, orderItemId: number, orderItemSubsetId: number, pickUpDeadline: string) {
|
||||
setPickUpDeadline(
|
||||
orderId: number,
|
||||
orderItemId: number,
|
||||
orderItemSubsetId: number,
|
||||
pickUpDeadline: string,
|
||||
) {
|
||||
return this.orderService
|
||||
.OrderPatchOrderItemSubset({
|
||||
orderId,
|
||||
@@ -178,7 +214,9 @@ export class DomainOmsService {
|
||||
}
|
||||
|
||||
changeStockStatusCode(payload: ChangeStockStatusCodeValues[]) {
|
||||
return this.orderService.OrderChangeStockStatusCode(payload).pipe(map((response) => response.result));
|
||||
return this.orderService
|
||||
.OrderChangeStockStatusCode(payload)
|
||||
.pipe(map((response) => response.result));
|
||||
}
|
||||
|
||||
orderAtSupplier({
|
||||
@@ -197,7 +235,13 @@ export class DomainOmsService {
|
||||
});
|
||||
}
|
||||
|
||||
getNotifications(orderId: number): Observable<{ selected: NotificationChannel; email: string; mobile: string }> {
|
||||
getNotifications(
|
||||
orderId: number,
|
||||
): Observable<{
|
||||
selected: NotificationChannel;
|
||||
email: string;
|
||||
mobile: string;
|
||||
}> {
|
||||
return this.getOrder(orderId).pipe(
|
||||
map((order) => ({
|
||||
selected: order.notificationChannels,
|
||||
@@ -208,10 +252,15 @@ export class DomainOmsService {
|
||||
}
|
||||
|
||||
getOrderSource(orderId: number): Observable<string> {
|
||||
return this.getOrder(orderId).pipe(map((order) => order?.features?.orderSource));
|
||||
return this.getOrder(orderId).pipe(
|
||||
map((order) => order?.features?.orderSource),
|
||||
);
|
||||
}
|
||||
|
||||
updateNotifications(orderId: number, changes: { selected: NotificationChannel; email: string; mobile: string }) {
|
||||
updateNotifications(
|
||||
orderId: number,
|
||||
changes: { selected: NotificationChannel; email: string; mobile: string },
|
||||
) {
|
||||
const communicationDetails = {
|
||||
email: changes.email,
|
||||
mobile: changes.mobile,
|
||||
@@ -224,7 +273,11 @@ export class DomainOmsService {
|
||||
delete communicationDetails.mobile;
|
||||
}
|
||||
|
||||
return this.updateOrder({ orderId, notificationChannels: changes.selected, communicationDetails });
|
||||
return this.updateOrder({
|
||||
orderId,
|
||||
notificationChannels: changes.selected,
|
||||
communicationDetails,
|
||||
});
|
||||
}
|
||||
|
||||
updateOrder({
|
||||
@@ -270,7 +323,13 @@ export class DomainOmsService {
|
||||
.pipe(map((res) => res.result));
|
||||
}
|
||||
|
||||
generateNotifications({ orderId, taskTypes }: { orderId: number; taskTypes: string[] }) {
|
||||
generateNotifications({
|
||||
orderId,
|
||||
taskTypes,
|
||||
}: {
|
||||
orderId: number;
|
||||
taskTypes: string[];
|
||||
}) {
|
||||
return this.orderService.OrderRegenerateOrderItemStatusTasks({
|
||||
orderId,
|
||||
taskTypes,
|
||||
@@ -302,10 +361,16 @@ export class DomainOmsService {
|
||||
.pipe(
|
||||
map((res) =>
|
||||
res.result
|
||||
.sort((a, b) => new Date(b.completed).getTime() - new Date(a.completed).getTime())
|
||||
.sort(
|
||||
(a, b) =>
|
||||
new Date(b.completed).getTime() -
|
||||
new Date(a.completed).getTime(),
|
||||
)
|
||||
.reduce(
|
||||
(data, result) => {
|
||||
(data[result.name] = data[result.name] || []).push(new Date(result.completed));
|
||||
(data[result.name] = data[result.name] || []).push(
|
||||
new Date(result.completed),
|
||||
);
|
||||
return data;
|
||||
},
|
||||
{} as Record<string, Date[]>,
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { ReceiptOrderItemSubsetReferenceValues, ReceiptService } from '@generated/swagger/oms-api';
|
||||
import {
|
||||
ReceiptOrderItemSubsetReferenceValues,
|
||||
ReceiptService,
|
||||
} from '@generated/swagger/oms-api';
|
||||
import { memorize } from '@utils/common';
|
||||
import { shareReplay } from 'rxjs/operators';
|
||||
|
||||
@Injectable()
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class DomainReceiptService {
|
||||
constructor(private receiptService: ReceiptService) {}
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
export * from './defs';
|
||||
export * from './mappings';
|
||||
export * from './remission.module';
|
||||
export * from './remission.service';
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
import { ModuleWithProviders, NgModule } from '@angular/core';
|
||||
import { DomainRemissionService } from './remission.service';
|
||||
|
||||
@NgModule({})
|
||||
export class DomainRemissionModule {
|
||||
static forRoot(): ModuleWithProviders<DomainRemissionModule> {
|
||||
return {
|
||||
ngModule: RootDomainRemissionModule,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
imports: [],
|
||||
providers: [DomainRemissionService],
|
||||
})
|
||||
export class RootDomainRemissionModule {}
|
||||
@@ -30,7 +30,7 @@ import {
|
||||
import { Logger } from '@core/logger';
|
||||
import { RemissionPlacementType } from '@domain/remission';
|
||||
|
||||
@Injectable()
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class DomainRemissionService {
|
||||
constructor(
|
||||
private readonly _logger: Logger,
|
||||
@@ -214,7 +214,7 @@ export class DomainRemissionService {
|
||||
|
||||
getStockInformation(
|
||||
items: RemissionListItem[],
|
||||
recalculate: boolean = false,
|
||||
recalculate = false,
|
||||
) {
|
||||
return this.getCurrentStock().pipe(
|
||||
switchMap((stock) =>
|
||||
@@ -407,10 +407,10 @@ export class DomainRemissionService {
|
||||
|
||||
async deleteReturn(returnId: number) {
|
||||
const returnDto = await this.getReturn(returnId).toPromise();
|
||||
for (const receipt of returnDto?.receipts) {
|
||||
await this.deleteReceipt(returnDto.id, receipt.id);
|
||||
for (const receipt of returnDto?.receipts ?? []) {
|
||||
await this.deleteReceipt(returnDto!.id, receipt.id);
|
||||
}
|
||||
await this.deleteRemission(returnDto.id);
|
||||
await this.deleteRemission(returnDto!.id);
|
||||
}
|
||||
|
||||
addReturnItem({
|
||||
|
||||
@@ -1,28 +1,39 @@
|
||||
import { enableProdMode, isDevMode } from "@angular/core";
|
||||
import { platformBrowserDynamic } from "@angular/platform-browser-dynamic";
|
||||
import { CONFIG_DATA } from "@isa/core/config";
|
||||
import { setDefaultOptions } from "date-fns";
|
||||
import { de } from "date-fns/locale";
|
||||
import * as moment from "moment";
|
||||
import "moment/locale/de";
|
||||
import { enableProdMode, isDevMode } from '@angular/core';
|
||||
import { CONFIG_DATA } from '@isa/core/config';
|
||||
import { setDefaultOptions } from 'date-fns';
|
||||
import { de } from 'date-fns/locale';
|
||||
import localeDe from '@angular/common/locales/de';
|
||||
import localeDeExtra from '@angular/common/locales/extra/de';
|
||||
import * as moment from 'moment';
|
||||
import 'moment/locale/de';
|
||||
|
||||
setDefaultOptions({ locale: de });
|
||||
moment.locale("de");
|
||||
moment.locale('de');
|
||||
|
||||
import { AppModule } from "./app/app.module";
|
||||
registerLocaleData(localeDe, localeDeExtra);
|
||||
registerLocaleData(localeDe, 'de', localeDeExtra);
|
||||
|
||||
import { App } from './app/app';
|
||||
import { appConfig } from './app/app.config';
|
||||
import { bootstrapApplication } from '@angular/platform-browser';
|
||||
import { registerLocaleData } from '@angular/common';
|
||||
|
||||
if (!isDevMode()) {
|
||||
enableProdMode();
|
||||
}
|
||||
|
||||
async function bootstrap() {
|
||||
const configRes = await fetch("/config/config.json");
|
||||
const configRes = await fetch('/config/config.json');
|
||||
|
||||
const config = await configRes.json();
|
||||
|
||||
platformBrowserDynamic([
|
||||
await bootstrapApplication(App, {
|
||||
...appConfig,
|
||||
providers: [
|
||||
{ provide: CONFIG_DATA, useValue: config },
|
||||
]).bootstrapModule(AppModule);
|
||||
...appConfig.providers,
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
@@ -26,7 +26,7 @@ export class ModalAvailabilitiesComponent {
|
||||
item = this.modalRef.data.item;
|
||||
itemId = this.modalRef.data.itemId || this.modalRef.data.item.id;
|
||||
userbranch$ = combineLatest([
|
||||
this.applicationService.getSelectedBranch$(this.applicationService.activatedProcessId),
|
||||
this.applicationService.getSelectedBranch$(),
|
||||
this.domainAvailabilityService.getDefaultBranch(),
|
||||
]).pipe(map(([selectedBranch, defaultBranch]) => selectedBranch || defaultBranch));
|
||||
|
||||
|
||||
@@ -6,8 +6,13 @@ export const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: AssortmentComponent,
|
||||
title: 'Sortiment',
|
||||
children: [
|
||||
{ path: 'price-update', component: PriceUpdateComponent },
|
||||
{
|
||||
path: 'price-update',
|
||||
component: PriceUpdateComponent,
|
||||
title: 'Sortiment - Preisänderung',
|
||||
},
|
||||
{ path: '**', redirectTo: 'price-update' },
|
||||
],
|
||||
},
|
||||
|
||||
@@ -192,11 +192,7 @@ export class ArticleDetailsComponent implements OnInit, OnDestroy {
|
||||
}),
|
||||
);
|
||||
|
||||
selectedBranchId$ = this.applicationService.activatedProcessId$.pipe(
|
||||
switchMap((processId) =>
|
||||
this.applicationService.getSelectedBranch$(processId),
|
||||
),
|
||||
);
|
||||
selectedBranchId$ = this.applicationService.getSelectedBranch$();
|
||||
|
||||
get isTablet$() {
|
||||
return this._environment.matchTablet$;
|
||||
@@ -328,7 +324,7 @@ export class ArticleDetailsComponent implements OnInit, OnDestroy {
|
||||
debounceTime(0),
|
||||
switchMap((params) =>
|
||||
this.applicationService
|
||||
.getSelectedBranch$(Number(params.processId))
|
||||
.getSelectedBranch$()
|
||||
.pipe(map((selectedBranch) => ({ params, selectedBranch }))),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -98,11 +98,9 @@ export class ArticleSearchMainComponent implements OnInit, OnDestroy {
|
||||
);
|
||||
|
||||
this.subscriptions.add(
|
||||
this.application.activatedProcessId$
|
||||
.pipe(
|
||||
debounceTime(0),
|
||||
switchMap((processId) => this.application.getSelectedBranch$(processId)),
|
||||
)
|
||||
this.application
|
||||
.getSelectedBranch$()
|
||||
.pipe(debounceTime(0))
|
||||
.subscribe((selectedBranch) => {
|
||||
const branchChanged = selectedBranch?.id !== this.searchService?.selectedBranch?.id;
|
||||
if (branchChanged) {
|
||||
@@ -143,7 +141,7 @@ export class ArticleSearchMainComponent implements OnInit, OnDestroy {
|
||||
const clean = { ...params };
|
||||
|
||||
for (const key in clean) {
|
||||
if (key === 'main_qs' || key?.includes('order_by')) {
|
||||
if (key === 'main_qs') {
|
||||
clean[key] = undefined;
|
||||
} else if (key?.includes('order_by')) {
|
||||
delete clean[key];
|
||||
|
||||
@@ -40,7 +40,7 @@ export class SearchResultItemComponent extends ComponentStore<SearchResultItemCo
|
||||
|
||||
readonly item$ = this.select((s) => s.item);
|
||||
|
||||
@Input() selected: boolean = false;
|
||||
@Input() selected = false;
|
||||
|
||||
@Input()
|
||||
get selectable() {
|
||||
@@ -91,9 +91,7 @@ export class SearchResultItemComponent extends ComponentStore<SearchResultItemCo
|
||||
|
||||
defaultBranch$ = this._availability.getDefaultBranch();
|
||||
|
||||
selectedBranchId$ = this.applicationService.activatedProcessId$.pipe(
|
||||
switchMap((processId) => this.applicationService.getSelectedBranch$(processId)),
|
||||
);
|
||||
selectedBranchId$ = this.applicationService.getSelectedBranch$();
|
||||
|
||||
isOrderBranch$ = combineLatest([this.defaultBranch$, this.selectedBranchId$]).pipe(
|
||||
map(([defaultBranch, selectedBranch]) => {
|
||||
|
||||
@@ -157,7 +157,7 @@ export class ArticleSearchResultsComponent
|
||||
.pipe(
|
||||
debounceTime(0),
|
||||
switchMap(([processId, queryParams]) =>
|
||||
this.application.getSelectedBranch$(processId).pipe(
|
||||
this.application.getSelectedBranch$().pipe(
|
||||
map((selectedBranch) => ({
|
||||
processId,
|
||||
queryParams,
|
||||
|
||||
@@ -3,7 +3,7 @@ import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { DomainCatalogModule } from '@domain/catalog';
|
||||
import { ThumbnailUrlPipe } from '@domain/catalog';
|
||||
import { UiCommonModule } from '@ui/common';
|
||||
import { UiIconModule } from '@ui/icon';
|
||||
import { UiSelectBulletModule } from '@ui/select-bullet';
|
||||
@@ -26,7 +26,7 @@ import { MatomoModule } from 'ngx-matomo-client';
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
RouterModule,
|
||||
DomainCatalogModule,
|
||||
ThumbnailUrlPipe,
|
||||
UiCommonModule,
|
||||
UiIconModule,
|
||||
UiSelectBulletModule,
|
||||
|
||||
@@ -12,6 +12,7 @@ const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: PageCatalogComponent,
|
||||
title: 'Artikelsuche',
|
||||
data: {
|
||||
matomo: {
|
||||
title: 'Artikelsuche',
|
||||
@@ -21,6 +22,7 @@ const routes: Routes = [
|
||||
{
|
||||
path: 'filter',
|
||||
component: ArticleSearchFilterComponent,
|
||||
title: 'Artikelsuche - Filter',
|
||||
data: {
|
||||
matomo: {
|
||||
title: 'Artikelsuche - Filter',
|
||||
@@ -30,6 +32,7 @@ const routes: Routes = [
|
||||
{
|
||||
path: 'filter/:id',
|
||||
component: ArticleSearchFilterComponent,
|
||||
title: 'Artikelsuche - Filter',
|
||||
data: {
|
||||
matomo: {
|
||||
title: 'Artikelsuche - Filter',
|
||||
@@ -40,6 +43,7 @@ const routes: Routes = [
|
||||
path: 'search',
|
||||
component: ArticleSearchComponent,
|
||||
outlet: 'side',
|
||||
title: 'Artikelsuche',
|
||||
data: {
|
||||
matomo: {
|
||||
title: 'Artikelsuche',
|
||||
@@ -55,6 +59,7 @@ const routes: Routes = [
|
||||
{
|
||||
path: 'results',
|
||||
component: ArticleSearchResultsComponent,
|
||||
title: 'Artikelsuche - Trefferliste',
|
||||
data: {
|
||||
matomo: {
|
||||
title: 'Artikelsuche - Trefferliste',
|
||||
@@ -65,6 +70,7 @@ const routes: Routes = [
|
||||
path: 'results',
|
||||
component: ArticleSearchResultsComponent,
|
||||
outlet: 'side',
|
||||
title: 'Artikelsuche - Trefferliste',
|
||||
data: {
|
||||
matomo: {
|
||||
title: 'Artikelsuche - Trefferliste',
|
||||
@@ -75,6 +81,7 @@ const routes: Routes = [
|
||||
path: 'results/:id',
|
||||
component: ArticleSearchResultsComponent,
|
||||
outlet: 'side',
|
||||
title: 'Artikelsuche - Artikeldetails',
|
||||
data: {
|
||||
matomo: {
|
||||
title: 'Artikelsuche - Artikeldetails',
|
||||
@@ -85,6 +92,7 @@ const routes: Routes = [
|
||||
path: 'results/:ean/ean',
|
||||
component: ArticleSearchResultsComponent,
|
||||
outlet: 'side',
|
||||
title: 'Artikelsuche - Artikeldetails (EAN)',
|
||||
data: {
|
||||
matomo: {
|
||||
title: 'Artikelsuche - Artikeldetails (EAN)',
|
||||
@@ -94,6 +102,7 @@ const routes: Routes = [
|
||||
{
|
||||
path: 'details/:id',
|
||||
component: ArticleDetailsComponent,
|
||||
title: 'Artikelsuche - Artikeldetails (ID)',
|
||||
data: {
|
||||
matomo: {
|
||||
title: 'Artikelsuche - Artikeldetails (ID)',
|
||||
@@ -103,6 +112,7 @@ const routes: Routes = [
|
||||
{
|
||||
path: 'details/:ean/ean',
|
||||
component: ArticleDetailsComponent,
|
||||
title: 'Artikelsuche - Artikeldetails (EAN)',
|
||||
data: {
|
||||
matomo: {
|
||||
title: 'Artikelsuche - Artikeldetails (EAN)',
|
||||
|
||||
@@ -77,9 +77,7 @@ export class PageCatalogComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
ngOnInit() {
|
||||
this.activatedProcessId$ = this.application.activatedProcessId$.pipe(map((processId) => String(processId)));
|
||||
|
||||
this.selectedBranch$ = this.activatedProcessId$.pipe(
|
||||
switchMap((processId) => this.application.getSelectedBranch$(Number(processId))),
|
||||
);
|
||||
this.selectedBranch$ = this.application.getSelectedBranch$();
|
||||
|
||||
this.stockTooltipText$ = combineLatest([this.defaultBranch$, this.selectedBranch$]).pipe(
|
||||
map(([defaultBranch, selectedBranch]) => {
|
||||
|
||||
@@ -14,8 +14,8 @@ import { UiTooltipModule } from '@ui/tooltip';
|
||||
imports: [
|
||||
CommonModule,
|
||||
PageCatalogRoutingModule,
|
||||
ArticleSearchModule,
|
||||
ArticleDetailsModule,
|
||||
ArticleSearchModule,
|
||||
BreadcrumbModule,
|
||||
BranchSelectorComponent,
|
||||
SharedSplitscreenComponent,
|
||||
|
||||
@@ -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' },
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user