mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-31 09:37:15 +01:00
Merge branch 'release/4.2'
This commit is contained in:
438
.github/copilot-instructions.md
vendored
438
.github/copilot-instructions.md
vendored
@@ -1,23 +1,415 @@
|
|||||||
# Mentor Instructions
|
## ISA Frontend – AI Assistant Working Rules
|
||||||
|
|
||||||
## Introduction
|
Concise, project-specific guidance so an AI agent can be productive quickly. Focus on THESE patterns; avoid generic boilerplate.
|
||||||
|
|
||||||
You are Mentor, an AI assistant focused on ensuring code quality, strict adherence to best practices, and development efficiency. **Your core function is to enforce the coding standards and guidelines established in this workspace.** Your goal is to help me produce professional, maintainable, and high-performing code.
|
### 1. Monorepo & Tooling
|
||||||
|
|
||||||
**Always get the latest official documentation for Angular, Nx, or any related technology before implementing or when answering questions or providing feedback. Use Context7:**
|
- Nx workspace (Angular 20 + Libraries under `libs/**`, main app `apps/isa-app`).
|
||||||
|
- Scripts (see `package.json`):
|
||||||
## Tone and Personality
|
- Dev serve: `npm start` (=> `nx serve isa-app --ssl`).
|
||||||
|
- Library tests (exclude app): `npm test` (Jest + emerging Vitest). CI uses `npm run ci`.
|
||||||
Maintain a professional, objective, and direct tone consistently:
|
- Build dev: `npm run build`; prod: `npm run build-prod`.
|
||||||
|
- Storybook: `npm run storybook`.
|
||||||
- **Guideline Enforcement & Error Correction:** When code deviates from guidelines or contains errors, provide precise, technical feedback. Clearly state the issue, cite the relevant guideline or principle, and explain the required correction for optimal, maintainable results.
|
- Swagger codegen: `npm run generate:swagger` then `npm run fix:files:swagger`.
|
||||||
- **Technical Consultation:** In discussions about architecture, best practices, or complex coding inquiries, remain formal and analytical. Provide clear, well-reasoned explanations and recommendations grounded in industry standards and the project's specific guidelines.
|
- Default branch in Nx: `develop` (`nx.json: defaultBase`). Use affected commands when adding libs.
|
||||||
|
- Node >=22, TS 5.8, ESLint flat config (`eslint.config.js`).
|
||||||
## Behavioral Guidelines
|
|
||||||
|
### 1.a Project Tree (Detailed Overview)
|
||||||
- **Actionable Feedback:** Prioritize constructive, actionable feedback aimed at improving code quality, maintainability, and adherence to standards. Avoid rewriting code; focus on explaining the necessary changes and their rationale based on guidelines.
|
|
||||||
- **Strict Guideline Adherence:** Base _all_ feedback, suggestions, and explanations rigorously on the guidelines documented within this workspace. Cite specific rules and principles consistently.
|
```
|
||||||
- **Demand Clarity:** If a query or code snippet lacks sufficient detail for a thorough, professional analysis, request clarification.
|
.
|
||||||
- **Professional Framing:** Frame all feedback objectively, focusing on the technical aspects and the importance of meeting project standards for long-term success.
|
├─ apps/
|
||||||
- **Context-Specific Expertise:** Provide specific, context-aware advice tailored to the code or problem, always within the framework of our established guidelines.
|
│ └─ isa-app/ # Main Angular app (Jest). Legacy non-standalone root component pattern.
|
||||||
- **Enforce Standards:** Actively enforce project preferences for Type safety, Clean Code principles, and thorough documentation, as mandated by the workspace guidelines.
|
│ ├─ project.json # Build/serve/test targets
|
||||||
|
│ ├─ src/
|
||||||
|
│ │ ├─ main.ts / index.html # Angular bootstrap
|
||||||
|
│ │ ├─ app/main.component.ts # Root component (standalone:false)
|
||||||
|
│ │ ├─ environments/ # Environment files (prod replace)
|
||||||
|
│ │ ├─ assets/ # Static assets
|
||||||
|
│ │ └─ config/ # Runtime config JSON (read via Config service)
|
||||||
|
│ └─ .storybook/ # App Storybook config
|
||||||
|
│
|
||||||
|
├─ libs/ # All reusable code (grouped by domain / concern)
|
||||||
|
│ ├─ core/ # Cross-cutting infrastructure
|
||||||
|
│ │ ├─ logging/ # Logging service + providers + sinks
|
||||||
|
│ │ │ ├─ src/lib/logging.service.ts
|
||||||
|
│ │ │ ├─ src/lib/logging.providers.ts
|
||||||
|
│ │ │ └─ README.md # Full API & patterns
|
||||||
|
│ │ ├─ config/ # `Config` service (Zod validated lookup)
|
||||||
|
│ │ └─ storage/ # User-scoped storage + signal store feature (`withStorage`)
|
||||||
|
│ │ ├─ src/lib/signal-store-feature.ts
|
||||||
|
│ │ └─ src/lib/storage.ts
|
||||||
|
│ │
|
||||||
|
│ ├─ shared/ # Shared UI/services not domain specific
|
||||||
|
│ │ └─ scanner/ # Scandit integration (tokens, service, components, platform gating)
|
||||||
|
│ │ ├─ src/lib/scanner.service.ts
|
||||||
|
│ │ └─ src/lib/render-if-scanner-is-ready.directive.ts
|
||||||
|
│ │
|
||||||
|
│ ├─ remission/ # Remission domain features (newer pattern; Vitest)
|
||||||
|
│ │ ├─ feature/
|
||||||
|
│ │ │ ├─ remission-return-receipt-details/
|
||||||
|
│ │ │ │ ├─ vite.config.mts # Signals + Vitest example
|
||||||
|
│ │ │ │ └─ src/lib/resources/ # Resource factories (signals async pattern)
|
||||||
|
│ │ │ └─ remission-return-receipt-list/
|
||||||
|
│ │ └─ shared/ # Dialogs / shared remission UI pieces
|
||||||
|
│ │
|
||||||
|
│ ├─ common/ # Cross-domain utilities (decorators, print, data-access)
|
||||||
|
│ ├─ utils/ # Narrow utility libs (ean-validation, z-safe-parse, etc.)
|
||||||
|
│ ├─ ui/ # Generic UI components (presentational)
|
||||||
|
│ ├─ icons/ # Icon sets / wrappers
|
||||||
|
│ ├─ catalogue/ # Domain area (legacy Jest)
|
||||||
|
│ ├─ customer/ # Domain area (legacy Jest)
|
||||||
|
│ └─ oms/ # Domain area (legacy Jest)
|
||||||
|
│
|
||||||
|
├─ generated/swagger/ # Generated API clients (regen via scripts; do not hand edit)
|
||||||
|
├─ tools/ # Helper scripts (e.g. swagger fix script)
|
||||||
|
├─ testresults/ # JUnit XML (jest-junit). CI artifact pickup.
|
||||||
|
├─ coverage/ # Per-project coverage outputs
|
||||||
|
├─ tailwind-plugins/ # Custom Tailwind plugin modules used by `tailwind.config.js`
|
||||||
|
├─ vitest.workspace.ts # Glob enabling multi-lib Vitest detection
|
||||||
|
├─ nx.json / package.json # Workspace + scripts + defaultBase=develop
|
||||||
|
└─ eslint.config.js # Flat ESLint root config
|
||||||
|
```
|
||||||
|
|
||||||
|
Guidelines: create new code in the closest domain folder; expose public API via each lib `src/index.ts`; follow existing naming (`feature-name.type.ts`). Keep generated swagger untouched—extend via wrapper libs if needed.
|
||||||
|
|
||||||
|
### 1.b Import Path Aliases
|
||||||
|
|
||||||
|
Use existing TS path aliases (see `tsconfig.base.json`) instead of long relative paths:
|
||||||
|
|
||||||
|
Core / Cross-cutting:
|
||||||
|
|
||||||
|
- `@isa/core/logging`, `@isa/core/config`, `@isa/core/storage`, `@isa/core/tabs`, `@isa/core/notifications`
|
||||||
|
|
||||||
|
Domain & Features:
|
||||||
|
|
||||||
|
- Catalogue: `@isa/catalogue/data-access`
|
||||||
|
- Customer: `@isa/customer/data-access`
|
||||||
|
- OMS features: `@isa/oms/feature/return-details`, `.../return-process`, `.../return-review`, `.../return-search`, `.../return-summary`
|
||||||
|
- OMS shared: `@isa/oms/shared/product-info`, `@isa/oms/shared/task-list`
|
||||||
|
- Remission: `@isa/remission/data-access`, feature libs (`@isa/remission/feature/remission-return-receipt-details`, `...-list`) and shared (`@isa/remission/shared/remission-start-dialog`, `.../search-item-to-remit-dialog`, `.../return-receipt-actions`, `.../product`)
|
||||||
|
|
||||||
|
Shared / UI:
|
||||||
|
|
||||||
|
- Shared libs: `@isa/shared/scanner`, `@isa/shared/filter`, `@isa/shared/product-image`, `@isa/shared/product-router-link`, `@isa/shared/product-format`
|
||||||
|
- UI components: `@isa/ui/buttons`, `@isa/ui/dialog`, `@isa/ui/input-controls`, `@isa/ui/layout`, `@isa/ui/menu`, `@isa/ui/toolbar`, etc. (one alias per folder under `libs/ui/*`)
|
||||||
|
- Icons: `@isa/icons`
|
||||||
|
|
||||||
|
Utilities:
|
||||||
|
|
||||||
|
- `@isa/utils/ean-validation`, `@isa/utils/z-safe-parse`, `@isa/utils/scroll-position`
|
||||||
|
|
||||||
|
Generated Swagger Clients:
|
||||||
|
|
||||||
|
- `@generated/swagger/isa-api`, `@generated/swagger/oms-api`, `@generated/swagger/inventory-api`, etc. (one per subfolder). Never edit generated sources—wrap in a domain lib if extension needed.
|
||||||
|
|
||||||
|
App-local (only inside `apps/isa-app` context):
|
||||||
|
|
||||||
|
- Namespaced folders: `@adapter/*`, `@domain/*`, `@hub/*`, `@modal/*`, `@page/*`, `@shared/*` (and nested: `@shared/components/*`, `@shared/services/*`, etc.), `@ui/*`, `@utils/*`, `@swagger/*`.
|
||||||
|
|
||||||
|
Patterns:
|
||||||
|
|
||||||
|
- Always add new reusable code as a library then expose via an `@isa/...` alias; do not add new generic code under app-local aliases if it may be reused later.
|
||||||
|
- When introducing a new library ensure its `src/index.ts` re-exports only stable public surface; internal helpers stay un-exported.
|
||||||
|
- For new generated API groups, extend via thin wrappers in a domain `data-access` lib rather than patching generated code.
|
||||||
|
|
||||||
|
### 2. Testing Strategy
|
||||||
|
|
||||||
|
- Legacy tests: Jest (`@nx/jest:jest`). New feature libs (e.g. remission feature) use Vitest + Vite plugin (`vite.config.mts`).
|
||||||
|
- When adding a new library today prefer Vitest unless consistency with existing Jest-only area is required.
|
||||||
|
- Do NOT mix frameworks inside one lib. Check presence of `vite.config.*` to know it is Vitest-enabled.
|
||||||
|
- App (`isa-app`) still uses Jest.
|
||||||
|
|
||||||
|
### 3. Architecture & Cross-Cutting Services
|
||||||
|
|
||||||
|
- Core libraries underpin features: `@isa/core/logging`, `@isa/core/config`, `@isa/core/storage`.
|
||||||
|
- Feature domains grouped (e.g. `libs/remission/**`, `libs/shared/**`, `libs/common/**`). Keep domain-specific code there; UI-only pieces in `ui/` or `shared/`.
|
||||||
|
- Prefer standalone components but some legacy components set `standalone: false` (see `MainComponent`). Maintain existing pattern unless doing a focused migration.
|
||||||
|
|
||||||
|
### 4. Logging (Critical Pattern)
|
||||||
|
|
||||||
|
- Central logging via `@isa/core/logging` (files: `logging.service.ts`, `logging.providers.ts`).
|
||||||
|
- Configure once in app config using provider builders: `provideLogging(withLogLevel(...), withSink(ConsoleLogSink), withContext({...}))`.
|
||||||
|
- Use factory `logger(() => ({ dynamicContext }))` (see README) rather than injecting `LoggingService` directly unless extending framework code.
|
||||||
|
- Context hierarchy: global -> component (`provideLoggerContext`) -> instance (factory param) -> message (callback arg). Always pass context as lazy function `() => ({ ... })` for perf.
|
||||||
|
- Respect log level threshold; do not perform expensive serialization before calling (let sinks handle it or gate behind dev checks).
|
||||||
|
|
||||||
|
### 5. Configuration Access
|
||||||
|
|
||||||
|
- Use `Config` service (`@isa/core/config/src/lib/config.ts`). Fetch values with Zod schema: `config.get('licence.scandit', z.string())` (see `SCANDIT_LICENSE` token). Avoid deprecated untyped access.
|
||||||
|
|
||||||
|
### 6. Storage & State Persistence
|
||||||
|
|
||||||
|
- Storage abstraction: `injectStorage(SomeProvider)` wraps a `StorageProvider` (local/session/indexedDB/custom user storage) and prefixes keys with current authenticated user `sub` (OAuth `sub` fallback 'anonymous').
|
||||||
|
- When adding persisted signal stores, use `withStorage(storageKey, ProviderType)` feature (`signal-store-feature.ts`) to auto debounce-save (1s) + restore on init. Only pass plain serializable state.
|
||||||
|
|
||||||
|
### 7. Signals & State
|
||||||
|
|
||||||
|
- Internal state often via Angular signals & NgRx Signals (`@ngrx/signals`). Avoid manual subscriptions—prefer computed/signals and `rxMethod` for side effects.
|
||||||
|
- When persisting, ensure objects are JSON-safe; validation via Zod if deserializing external data.
|
||||||
|
|
||||||
|
#### 7.a NgRx Signals Deep Dive
|
||||||
|
|
||||||
|
Core building blocks we use:
|
||||||
|
|
||||||
|
- `signalStore(...)` + features: `withState`, `withComputed`, `withMethods`, `withHooks`, `withStorage` (custom feature in `core/storage`).
|
||||||
|
- `rxMethod` (from `@ngrx/signals/rxjs-interop`) to bridge imperative async flows (HTTP calls, debounce, switchMap) into store-driven mutations.
|
||||||
|
- `getState`, `patchState` for immutable, shallow merges; avoid manually mutating nested objects—spread + patch.
|
||||||
|
|
||||||
|
Patterns:
|
||||||
|
|
||||||
|
1. Store Shape: Keep initial state small & serializable (no class instances, functions, DOM nodes). Derive heavy or view-specific projections with `withComputed`.
|
||||||
|
2. Side Effects: Wrap fetch/update flows inside `rxMethod` pipes; ensure cancellation semantics (`switchMap`) to drop stale requests.
|
||||||
|
3. Persistence: Apply `withStorage(key, Provider)` last so hooks run after other features; persisted state must be plain JSON (no Dates—convert to ISO strings). Debounce already handled (1s) in `withStorage`—do NOT add another debounce upstream unless burst traffic is extreme.
|
||||||
|
4. Error Handling: Keep an `error` field in state for presentation; log via `logger()` at Warn/Error levels but do not store full Error object (serialize minimal fields: `message`, maybe `code`).
|
||||||
|
5. Loading Flags: Prefer a boolean `loading` OR a discriminated union `status: 'idle'|'loading'|'success'|'error'` for richer UI; avoid multiple booleans that can drift.
|
||||||
|
6. Computed Selectors: Name as `XComputed` or just semantic (e.g. `filteredItems`) using `computed(() => ...)` inside `withComputed`; never cause side-effects in a computed.
|
||||||
|
7. Resource Factory Pattern: For remote data needed in multiple components, create a factory function returning an object with `value`, `isLoading`, `error` signals plus a `reload()` method; see remission `resources/` directory.
|
||||||
|
|
||||||
|
Store Lifecycle Hooks:
|
||||||
|
|
||||||
|
- Use `withHooks({ onInit() { ... }, onDestroy() { ... } })` for restoration, websockets, or timers. Pair cleanups explicitly.
|
||||||
|
|
||||||
|
Persistence Feature (`withStorage`):
|
||||||
|
|
||||||
|
- Implementation: Debounced `storeState` rxMethod listens to any state change, saves hashed user‑scoped key (see `hash.utils.ts`). On init it calls `restoreState()`.
|
||||||
|
- Extending: If you need to blacklist transient fields from persistence, add a method wrapping `getState` and remove keys before `storage.set` (extend feature locally rather than editing shared code unless broadly needed).
|
||||||
|
|
||||||
|
Typical Store Template:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// feature-x.store.ts
|
||||||
|
import {
|
||||||
|
signalStore,
|
||||||
|
withState,
|
||||||
|
withComputed,
|
||||||
|
withMethods,
|
||||||
|
withHooks,
|
||||||
|
} from '@ngrx/signals';
|
||||||
|
import { rxMethod } from '@ngrx/signals/rxjs-interop';
|
||||||
|
import { debounceTime, switchMap, tap, catchError, of } from 'rxjs';
|
||||||
|
import { withStorage } from '@isa/core/storage';
|
||||||
|
import { logger } from '@isa/core/logging';
|
||||||
|
|
||||||
|
interface FeatureXState {
|
||||||
|
items: ReadonlyArray<Item>;
|
||||||
|
query: string;
|
||||||
|
loading: boolean;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState: FeatureXState = { items: [], query: '', loading: false };
|
||||||
|
|
||||||
|
export const FeatureXStore = signalStore(
|
||||||
|
withState(initialState),
|
||||||
|
withProps((store, logger = logger(() => ({ store: 'FeatureX' }))) => ({
|
||||||
|
_logger: logger,
|
||||||
|
})),
|
||||||
|
withComputed(({ items, query }) => ({
|
||||||
|
filtered: computed(() => items().filter((i) => i.name.includes(query()))),
|
||||||
|
hasError: computed(() => !!query() && !items().length),
|
||||||
|
})),
|
||||||
|
withMethods((store) => ({
|
||||||
|
setQuery: (q: string) => patchState(store, { query: q }),
|
||||||
|
// rxMethod side effect to load items
|
||||||
|
loadItems: rxMethod<string | void>(
|
||||||
|
pipe(
|
||||||
|
debounceTime(150),
|
||||||
|
tap(() => patchState(store, { loading: true, error: undefined })),
|
||||||
|
switchMap(() =>
|
||||||
|
fetchItems(store.query()).pipe(
|
||||||
|
tap((items) => patchState(store, { items, loading: false })),
|
||||||
|
catchError((err) => {
|
||||||
|
store._logger.error('Load failed', err as Error, () => ({
|
||||||
|
query: store.query(),
|
||||||
|
}));
|
||||||
|
patchState(store, {
|
||||||
|
loading: false,
|
||||||
|
error: (err as Error).message,
|
||||||
|
});
|
||||||
|
return of([]);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
})),
|
||||||
|
withHooks((store) => ({
|
||||||
|
onInit() {
|
||||||
|
store.loadItems();
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
withStorage('feature-x', LocalStorageProvider),
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
Testing Signal Stores (Vitest or Jest):
|
||||||
|
|
||||||
|
- Use `runInInjectionContext(TestBed.inject(Injector), () => FeatureXStore)` or instantiate via exported factory if provided.
|
||||||
|
- For async rxMethod flows, flush microtasks (`await vi.runAllTimersAsync()` if timers used) or rely on returned observable completion when you subscribe inside the test harness.
|
||||||
|
- Snapshot only primitive slices (avoid full object snapshots with volatile ordering).
|
||||||
|
|
||||||
|
Migration Tips:
|
||||||
|
|
||||||
|
- Converting legacy NgRx reducers: Start by lifting static initial state + selectors into `withState` + `withComputed`; replace effects with `rxMethod` maintaining cancellation semantics (`switchMap` mirrors effect flattening strategy).
|
||||||
|
- Keep action names only if externally observed (analytics, logging). Otherwise remove ceremony—call store methods directly.
|
||||||
|
|
||||||
|
Anti-Patterns to Avoid:
|
||||||
|
|
||||||
|
- Writing to signals inside a computed or inside another signal setter (causes cascading updates).
|
||||||
|
- Storing large unnormalized arrays and then repeatedly filtering/sorting in multiple components—centralize that in computed selectors.
|
||||||
|
- Persisting secrets or PII directly; hash keys already user-scoped but content still plain—sanitize if needed.
|
||||||
|
- Returning raw subscriptions from store methods; expose signals or idempotent methods only.
|
||||||
|
|
||||||
|
#### 7.b Prefer Signals over Observables (Practical Rules)
|
||||||
|
|
||||||
|
Default to signals for all in-memory UI & derived state; keep Observables only at I/O edges.
|
||||||
|
|
||||||
|
Use Observables for:
|
||||||
|
|
||||||
|
- HTTP / WebSocket / SignalR streams at the boundary.
|
||||||
|
- Timer / interval / external event sources.
|
||||||
|
- Interop with legacy NgRx store pieces not yet migrated.
|
||||||
|
|
||||||
|
Immediately convert inbound Observables to signals:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// Legacy service returning Observable<Item[]>
|
||||||
|
items$ = http.get<Item[]>(url);
|
||||||
|
// New pattern
|
||||||
|
const items = toSignal(http.get<Item[]>(url), { initialValue: [] });
|
||||||
|
```
|
||||||
|
|
||||||
|
Expose signals from stores & services:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// BAD (forces template async pipe + subscription mgmt)
|
||||||
|
getItems(): Observable<Item[]> { return this.http.get(...); }
|
||||||
|
|
||||||
|
// GOOD
|
||||||
|
items = toSignal(this.http.get<Item[]>(url), { initialValue: [] });
|
||||||
|
```
|
||||||
|
|
||||||
|
Bridge when needed:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// Signal -> Observable (rare):
|
||||||
|
const queryChanges$ = fromSignal(query, { requireSync: true });
|
||||||
|
|
||||||
|
// Observable -> Signal (preferred):
|
||||||
|
const data = toSignal(data$, { initialValue: undefined });
|
||||||
|
```
|
||||||
|
|
||||||
|
Side-effects: never subscribe manually—wrap in `rxMethod` (cancels stale work via `switchMap`).
|
||||||
|
|
||||||
|
```ts
|
||||||
|
loadData: rxMethod<void>(
|
||||||
|
pipe(
|
||||||
|
switchMap(() =>
|
||||||
|
this.api.fetch().pipe(tap((r) => patchState(store, { data: r }))),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
Template usage: reference signals directly (`{{ item.name }}`) or in control flow; no `| async` needed.
|
||||||
|
|
||||||
|
Replacing combineLatest / map chains:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// Before (Observable)
|
||||||
|
vm$ = combineLatest([a$, b$]).pipe(map(([a, b]) => buildVm(a, b)));
|
||||||
|
|
||||||
|
// After (Signals)
|
||||||
|
const vm = computed(() => buildVm(a(), b()));
|
||||||
|
```
|
||||||
|
|
||||||
|
Debounce / throttle user input:
|
||||||
|
Keep raw form value as a signal; create an rxMethod for debounced fetch instead of debouncing inside a computed.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
search = signal('');
|
||||||
|
runSearch: rxMethod<string>(
|
||||||
|
pipe(
|
||||||
|
debounceTime(300),
|
||||||
|
switchMap((term) =>
|
||||||
|
this.api
|
||||||
|
.search(term)
|
||||||
|
.pipe(tap((results) => patchState(store, { results }))),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
effect(() => {
|
||||||
|
runSearch(this.search());
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Avoid converting a signal back to an Observable just to use a single RxJS operator; prefer inline signal `computed` or small helper.
|
||||||
|
|
||||||
|
Migration heuristic:
|
||||||
|
|
||||||
|
1. Identify component `foo$` fields used only in template -> convert to signal via `toSignal`.
|
||||||
|
2. Collapse chains of `combineLatest` + `map` into `computed`.
|
||||||
|
3. Replace imperative `subscribe` side-effects with `rxMethod` + `patchState`.
|
||||||
|
4. Add persistence last via `withStorage` if state must survive reload.
|
||||||
|
|
||||||
|
Performance tip: heavy derived computations (sorting large arrays) belong in a memoized `computed`; if expensive & infrequently needed, gate behind another signal flag.
|
||||||
|
|
||||||
|
### 8. Scanner Integration (Scandit)
|
||||||
|
|
||||||
|
- Barcode scanning encapsulated in `@isa/shared/scanner` (`scanner.service.ts`). Use provided injection tokens for license & defaults (override via DI if needed). Service auto-configures once; `ready` signal triggers `configure()` lazily.
|
||||||
|
- Always catch and log errors with proper context; platform gating throws `PlatformNotSupportedError` which is downgraded to warn.
|
||||||
|
|
||||||
|
### 9. Styling
|
||||||
|
|
||||||
|
- Tailwind with custom semantic tokens (`tailwind.config.js`). Prefer design tokens like `text-isa-neutral-700`, spacing utilities with custom `px-*` scales rather than ad‑hoc raw values.
|
||||||
|
- Global overlays rely on CDK classes; retain `@angular/cdk/overlay-prebuilt.css` in style arrays when creating new entrypoints or Storybook stories.
|
||||||
|
|
||||||
|
### 10. Library Conventions
|
||||||
|
|
||||||
|
- File naming: kebab-case; feature first then type (e.g. `return-receipt-list.component.ts`).
|
||||||
|
- Provide public API via each lib `src/index.ts`. Export only stable symbols; keep internal utilities in subfolders not re-exported.
|
||||||
|
- Add `project.json` with `test` & `lint` targets; for new Vitest libs include `vite.config.mts` and adjust `tsconfig.spec.json` references to vitest types.
|
||||||
|
|
||||||
|
### 11. Adding / Modifying Tests
|
||||||
|
|
||||||
|
- For Jest libs: standard `*.spec.ts` with `TestBed`. Spectator may appear in legacy code—do not introduce Spectator in new tests; use Angular Testing Utilities.
|
||||||
|
- For Vitest libs: ensure `vite.config.mts` includes `setupFiles`. Use `describe/it` from `vitest` and Angular TestBed (see remission resource spec for pattern of using `runInInjectionContext`).
|
||||||
|
- Prefer resource-style factories returning signals for async state (pattern in `createSupplierResource`).
|
||||||
|
|
||||||
|
### 12. Performance & Safety
|
||||||
|
|
||||||
|
- Logging: rely on lazy context function; avoid `JSON.stringify()` unless behind a dev guard.
|
||||||
|
- Storage: hashing keys (see `hash.utils.ts`) ensures stable key space; do not bypass if you need consistent per-user scoping.
|
||||||
|
- Scanner overlay: always clean up overlay + event listeners (follow existing `open` implementation for pattern).
|
||||||
|
|
||||||
|
### 13. CI / Coverage / Artifacts
|
||||||
|
|
||||||
|
- JUnit XML placed in `testresults/` (Jest configured with `jest-junit`). Keep filename stability for pipeline consumption; do not rename those outputs.
|
||||||
|
- Coverage output under `coverage/libs/...`; respect Nx caching—avoid side effects outside project roots.
|
||||||
|
|
||||||
|
### 14. When Unsure
|
||||||
|
|
||||||
|
- Search existing domain folder for analogous implementation (e.g. new feature under remission: inspect sibling feature libs for structure).
|
||||||
|
- Preserve existing DI token patterns instead of introducing new global singletons.
|
||||||
|
|
||||||
|
### 15. Quick Examples
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// New feature logger usage
|
||||||
|
const log = logger(() => ({ feature: 'ReturnReceipt', action: 'init' }));
|
||||||
|
log.info('Mount');
|
||||||
|
|
||||||
|
// Persisting a signal store slice
|
||||||
|
export const FeatureStore = signalStore(
|
||||||
|
withState(initState),
|
||||||
|
withStorage('return:filters', LocalStorageProvider),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Fetch config value safely
|
||||||
|
const apiBase = inject(Config).get('api.baseUrl', z.string().url());
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Let me know if any area (e.g. auth flow, NgRx usage, Swagger generation details) needs deeper coverage and I can extend this file.
|
||||||
|
|||||||
189
.github/prompts/plan.prompt.md
vendored
Normal file
189
.github/prompts/plan.prompt.md
vendored
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
---
|
||||||
|
mode: agent
|
||||||
|
tools: ['edit', 'search', 'usages', 'vscodeAPI', 'problems', 'changes', 'fetch', 'githubRepo', 'Nx Mcp Server', 'context7']
|
||||||
|
description: Plan Mode - Research and create a detailed implementation plan before making any changes.
|
||||||
|
model: Gemini 2.5 Pro (copilot)
|
||||||
|
---
|
||||||
|
|
||||||
|
# Plan Mode
|
||||||
|
|
||||||
|
You are now operating in **Plan Mode** - a research and planning phase that ensures thorough analysis before implementation. Plan mode is **ALWAYS ACTIVE** when using this prompt. You must follow these strict guidelines for every request:
|
||||||
|
|
||||||
|
## Phase 1: Research & Analysis (MANDATORY)
|
||||||
|
|
||||||
|
### ALLOWED Operations:
|
||||||
|
|
||||||
|
- ✅ Read files using Read, Glob, Grep tools
|
||||||
|
- ✅ Search documentation and codebases
|
||||||
|
- ✅ Analyze existing patterns and structures
|
||||||
|
- ✅ Use WebFetch for documentation research
|
||||||
|
- ✅ List and explore project structure
|
||||||
|
- ✅ Use Nx/Angular/Context7 MCP tools for workspace analysis
|
||||||
|
- ✅ Review dependencies and configurations
|
||||||
|
|
||||||
|
### FORBIDDEN Operations:
|
||||||
|
|
||||||
|
- ❌ **NEVER** create, edit, or modify any files
|
||||||
|
- ❌ **NEVER** run commands that change system state
|
||||||
|
- ❌ **NEVER** make commits or push changes
|
||||||
|
- ❌ **NEVER** install packages or modify configurations
|
||||||
|
- ❌ **NEVER** run build/test commands during planning
|
||||||
|
|
||||||
|
## Phase 2: Plan Presentation (REQUIRED FORMAT)
|
||||||
|
|
||||||
|
After thorough research, present your plan using this exact structure:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## 📋 Implementation Plan
|
||||||
|
|
||||||
|
### 🎯 Objective
|
||||||
|
|
||||||
|
[Clear statement of what will be accomplished]
|
||||||
|
|
||||||
|
### 🔍 Research Summary
|
||||||
|
|
||||||
|
- **Current State**: [What exists now]
|
||||||
|
- **Requirements**: [What needs to be built/changed]
|
||||||
|
- **Constraints**: [Limitations and considerations]
|
||||||
|
|
||||||
|
### 📁 Files to be Modified/Created
|
||||||
|
|
||||||
|
1. **File**: `path/to/file.ts`
|
||||||
|
|
||||||
|
- **Action**: Create/Modify/Delete
|
||||||
|
- **Purpose**: [Why this file needs changes]
|
||||||
|
- **Key Changes**: [Specific modifications planned]
|
||||||
|
|
||||||
|
2. **File**: `path/to/another-file.ts`
|
||||||
|
- **Action**: Create/Modify/Delete
|
||||||
|
- **Purpose**: [Why this file needs changes]
|
||||||
|
- **Key Changes**: [Specific modifications planned]
|
||||||
|
|
||||||
|
### 🏗️ Implementation Steps
|
||||||
|
|
||||||
|
1. **Step 1**: [Detailed description]
|
||||||
|
|
||||||
|
- Files affected: `file1.ts`, `file2.ts`
|
||||||
|
- Rationale: [Why this step is necessary]
|
||||||
|
|
||||||
|
2. **Step 2**: [Detailed description]
|
||||||
|
|
||||||
|
- Files affected: `file3.ts`
|
||||||
|
- Rationale: [Why this step is necessary]
|
||||||
|
|
||||||
|
3. **Step N**: [Continue numbering...]
|
||||||
|
|
||||||
|
### ⚠️ Risks & Considerations
|
||||||
|
|
||||||
|
- **Risk 1**: [Potential issue and mitigation]
|
||||||
|
- **Risk 2**: [Potential issue and mitigation]
|
||||||
|
|
||||||
|
### 🧪 Testing Strategy
|
||||||
|
|
||||||
|
- [How the changes will be tested]
|
||||||
|
- [Specific test files or approaches]
|
||||||
|
|
||||||
|
### 📚 Architecture Decisions
|
||||||
|
|
||||||
|
- **Pattern Used**: [Which architectural pattern will be followed]
|
||||||
|
- **Libraries/Dependencies**: [What will be used and why]
|
||||||
|
- **Integration Points**: [How this fits with existing code]
|
||||||
|
|
||||||
|
### ✅ Success Criteria
|
||||||
|
|
||||||
|
- [ ] Criterion 1
|
||||||
|
- [ ] Criterion 2
|
||||||
|
- [ ] All tests pass
|
||||||
|
- [ ] No lint errors
|
||||||
|
```
|
||||||
|
|
||||||
|
## Phase 3: Await Approval
|
||||||
|
|
||||||
|
After presenting the plan:
|
||||||
|
|
||||||
|
1. **STOP** all implementation activities
|
||||||
|
2. **WAIT** for explicit user approval
|
||||||
|
3. **DO NOT** proceed with any file changes
|
||||||
|
4. **RESPOND** to questions or plan modifications
|
||||||
|
5. **EXIT PLAN MODE** only when user explicitly says "execute", "implement", "go ahead", "approved", or similar approval language
|
||||||
|
|
||||||
|
## Phase 4: Implementation (After Exiting Plan Mode)
|
||||||
|
|
||||||
|
Once the user explicitly approves and you exit plan mode:
|
||||||
|
|
||||||
|
1. **PLAN MODE IS NOW DISABLED** - you can proceed with normal implementation
|
||||||
|
2. Use TodoWrite to create implementation todos
|
||||||
|
3. Follow the plan step-by-step
|
||||||
|
4. Update todos as you progress
|
||||||
|
5. Run tests and lint checks as specified
|
||||||
|
6. Provide progress updates
|
||||||
|
|
||||||
|
## Key Behavioral Rules
|
||||||
|
|
||||||
|
### Research Thoroughly
|
||||||
|
|
||||||
|
- Spend significant time understanding the codebase
|
||||||
|
- Look for existing patterns to follow
|
||||||
|
- Identify all dependencies and integration points
|
||||||
|
- Consider edge cases and error scenarios
|
||||||
|
|
||||||
|
### Be Comprehensive
|
||||||
|
|
||||||
|
- Plans should be detailed enough for another developer to implement
|
||||||
|
- Include all necessary file changes
|
||||||
|
- Consider testing, documentation, and deployment
|
||||||
|
- Address potential conflicts or breaking changes
|
||||||
|
|
||||||
|
### Show Your Work
|
||||||
|
|
||||||
|
- Explain reasoning behind architectural decisions
|
||||||
|
- Reference existing code patterns when applicable
|
||||||
|
- Cite documentation or best practices
|
||||||
|
- Provide alternatives when multiple approaches exist
|
||||||
|
|
||||||
|
### Safety First
|
||||||
|
|
||||||
|
- Never make changes during planning phase
|
||||||
|
- Always wait for explicit approval
|
||||||
|
- Flag potentially risky changes
|
||||||
|
- Suggest incremental implementation when complex
|
||||||
|
|
||||||
|
## Example Interactions
|
||||||
|
|
||||||
|
### Good Plan Mode Behavior:
|
||||||
|
|
||||||
|
```
|
||||||
|
User: "Add a dark mode toggle to the settings page"
|
||||||
|
Assistant: I'll research the current theming system and create a comprehensive plan for implementing dark mode.
|
||||||
|
|
||||||
|
[Extensive research using Read, Grep, Glob tools]
|
||||||
|
|
||||||
|
## 📋 Implementation Plan
|
||||||
|
[Follows complete format above]
|
||||||
|
|
||||||
|
Ready to proceed? Please approve this plan before I begin implementation.
|
||||||
|
```
|
||||||
|
|
||||||
|
### What NOT to do:
|
||||||
|
|
||||||
|
```
|
||||||
|
User: "Add a dark mode toggle"
|
||||||
|
Assistant: I'll add that right away!
|
||||||
|
[Immediately starts editing files - WRONG!]
|
||||||
|
```
|
||||||
|
|
||||||
|
# <<<<<<< HEAD
|
||||||
|
|
||||||
|
## Integration with Existing Copilot Instructions
|
||||||
|
|
||||||
|
This plan mode respects all existing project patterns:
|
||||||
|
|
||||||
|
- Follows Angular + Nx workspace conventions
|
||||||
|
- Uses existing import path aliases
|
||||||
|
- Respects testing strategy (Jest/Vitest)
|
||||||
|
- Follows NgRx Signals patterns
|
||||||
|
- Adheres to logging and configuration patterns
|
||||||
|
- Maintains library conventions and file naming
|
||||||
|
|
||||||
|
> > > > > > > develop
|
||||||
|
> > > > > > > Remember: **RESEARCH FIRST, PLAN THOROUGHLY, WAIT FOR APPROVAL, THEN IMPLEMENT**
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -75,3 +75,4 @@ vitest.config.*.timestamp*
|
|||||||
.memory.json
|
.memory.json
|
||||||
|
|
||||||
nx.instructions.md
|
nx.instructions.md
|
||||||
|
CLAUDE.md
|
||||||
|
|||||||
192
.vscode/settings.json
vendored
192
.vscode/settings.json
vendored
@@ -1,92 +1,100 @@
|
|||||||
{
|
{
|
||||||
"editor.accessibilitySupport": "off",
|
"editor.accessibilitySupport": "off",
|
||||||
"typescript.tsdk": "node_modules/typescript/lib",
|
"typescript.tsdk": "node_modules/typescript/lib",
|
||||||
"exportall.config.exclude": [".test.", ".spec.", ".stories."],
|
"exportall.config.exclude": [".test.", ".spec.", ".stories."],
|
||||||
"editor.formatOnSave": true,
|
"editor.formatOnSave": true,
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
"eslint.validate": [
|
"eslint.validate": [
|
||||||
"json"
|
"json"
|
||||||
],
|
],
|
||||||
"[html]": {
|
"[html]": {
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
},
|
},
|
||||||
"[typescript]": {
|
"[typescript]": {
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
},
|
},
|
||||||
"[typescriptreact]": {
|
"[typescriptreact]": {
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
},
|
},
|
||||||
"exportall.config.folderListener": [
|
"[markdown]": {
|
||||||
"/libs/oms/data-access/src/lib/models",
|
"editor.formatOnSave": false
|
||||||
"/libs/oms/data-access/src/lib/schemas",
|
},
|
||||||
"/libs/catalogue/data-access/src/lib/models",
|
"exportall.config.folderListener": [
|
||||||
"/libs/common/data-access/src/lib/models",
|
"/libs/oms/data-access/src/lib/models",
|
||||||
"/libs/common/data-access/src/lib/error",
|
"/libs/oms/data-access/src/lib/schemas",
|
||||||
"/libs/oms/data-access/src/lib/errors/return-process"
|
"/libs/catalogue/data-access/src/lib/models",
|
||||||
],
|
"/libs/common/data-access/src/lib/models",
|
||||||
"github.copilot.chat.commitMessageGeneration.instructions": [
|
"/libs/common/data-access/src/lib/error",
|
||||||
{
|
"/libs/oms/data-access/src/lib/errors/return-process"
|
||||||
"file": ".github/commit-instructions.md"
|
],
|
||||||
}
|
"github.copilot.chat.commitMessageGeneration.instructions": [
|
||||||
],
|
{
|
||||||
"github.copilot.chat.codeGeneration.instructions": [
|
"file": ".github/commit-instructions.md"
|
||||||
{
|
}
|
||||||
"file": ".vscode/llms/angular.txt"
|
],
|
||||||
},
|
"github.copilot.chat.codeGeneration.instructions": [
|
||||||
{
|
{
|
||||||
"file": "docs/tech-stack.md"
|
"file": ".vscode/llms/angular.txt"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"file": "docs/guidelines/code-style.md"
|
"file": "docs/tech-stack.md"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"file": "docs/guidelines/project-structure.md"
|
"file": "docs/guidelines/code-style.md"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"file": "docs/guidelines/state-management.md"
|
"file": "docs/guidelines/project-structure.md"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"file": "docs/guidelines/testing.md"
|
"file": "docs/guidelines/state-management.md"
|
||||||
}
|
},
|
||||||
],
|
{
|
||||||
"github.copilot.chat.testGeneration.instructions": [
|
"file": "docs/guidelines/testing.md"
|
||||||
{
|
}
|
||||||
"file": ".github/testing-instructions.md"
|
],
|
||||||
},
|
"github.copilot.chat.testGeneration.instructions": [
|
||||||
{
|
{
|
||||||
"file": "docs/tech-stack.md"
|
"file": ".github/testing-instructions.md"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"file": "docs/guidelines/code-style.md"
|
"file": "docs/tech-stack.md"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"file": "docs/guidelines/testing.md"
|
"file": "docs/guidelines/code-style.md"
|
||||||
}
|
},
|
||||||
],
|
{
|
||||||
"github.copilot.chat.reviewSelection.instructions": [
|
"file": "docs/guidelines/testing.md"
|
||||||
{
|
}
|
||||||
"file": ".github/copilot-instructions.md"
|
],
|
||||||
},
|
"github.copilot.chat.reviewSelection.instructions": [
|
||||||
{
|
{
|
||||||
"file": ".github/review-instructions.md"
|
"file": ".github/copilot-instructions.md"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"file": "docs/tech-stack.md"
|
"file": ".github/review-instructions.md"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"file": "docs/guidelines/code-style.md"
|
"file": "docs/tech-stack.md"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"file": "docs/guidelines/project-structure.md"
|
"file": "docs/guidelines/code-style.md"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"file": "docs/guidelines/state-management.md"
|
"file": "docs/guidelines/project-structure.md"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"file": "docs/guidelines/testing.md"
|
"file": "docs/guidelines/state-management.md"
|
||||||
}
|
},
|
||||||
],
|
{
|
||||||
"nxConsole.generateAiAgentRules": true,
|
"file": "docs/guidelines/testing.md"
|
||||||
"chat.mcp.enabled": true,
|
}
|
||||||
"chat.mcp.discovery.enabled": true
|
],
|
||||||
}
|
"nxConsole.generateAiAgentRules": true,
|
||||||
|
"chat.mcp.discovery.enabled": {
|
||||||
|
"claude-desktop": true,
|
||||||
|
"windsurf": true,
|
||||||
|
"cursor-global": true,
|
||||||
|
"cursor-workspace": true
|
||||||
|
},
|
||||||
|
"chat.mcp.access": "all"
|
||||||
|
}
|
||||||
|
|||||||
148
CLAUDE.md
Normal file
148
CLAUDE.md
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
This is an Angular monorepo managed by Nx. The main application is `isa-app`, which appears to be an inventory and returns management system for retail/e-commerce.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Monorepo Structure
|
||||||
|
- **apps/isa-app**: Main Angular application
|
||||||
|
- **libs/**: Reusable libraries organized by domain and type
|
||||||
|
- **core/**: Core utilities (config, logging, storage, tabs)
|
||||||
|
- **common/**: Shared utilities (data-access, decorators, print)
|
||||||
|
- **ui/**: UI component libraries (buttons, dialogs, inputs, etc.)
|
||||||
|
- **shared/**: Shared domain components (filter, scanner, product components)
|
||||||
|
- **oms/**: Order Management System features and utilities
|
||||||
|
- **remission/**: Remission/returns management features
|
||||||
|
- **catalogue/**: Product catalogue functionality
|
||||||
|
- **utils/**: General utilities (validation, scroll position, parsing)
|
||||||
|
- **icons/**: Icon library
|
||||||
|
- **generated/swagger/**: Auto-generated API client code from OpenAPI specs
|
||||||
|
|
||||||
|
### Key Architectural Patterns
|
||||||
|
- **Standalone Components**: Project uses Angular standalone components
|
||||||
|
- **Feature Libraries**: Domain features organized as separate libraries (e.g., `oms-feature-return-search`)
|
||||||
|
- **Data Access Layer**: Separate data-access libraries for each domain (e.g., `oms-data-access`, `remission-data-access`)
|
||||||
|
- **Shared UI Components**: Reusable UI components in `libs/ui/`
|
||||||
|
- **Generated API Clients**: Swagger/OpenAPI clients auto-generated in `generated/swagger/`
|
||||||
|
|
||||||
|
## Common Development Commands
|
||||||
|
|
||||||
|
### Build Commands
|
||||||
|
```bash
|
||||||
|
# Build the main application (development)
|
||||||
|
npx nx build isa-app --configuration=development
|
||||||
|
|
||||||
|
# Build for production
|
||||||
|
npx nx build isa-app --configuration=production
|
||||||
|
|
||||||
|
# Serve the application with SSL
|
||||||
|
npx nx serve isa-app --ssl
|
||||||
|
```
|
||||||
|
|
||||||
|
### Testing Commands
|
||||||
|
```bash
|
||||||
|
# Run tests for a specific library (always use --skip-cache)
|
||||||
|
npx nx run <project-name>:test --skip-cache
|
||||||
|
# Example: npx nx run remission-data-access:test --skip-cache
|
||||||
|
|
||||||
|
# Run tests for all libraries except the main app
|
||||||
|
npx nx run-many -t test --exclude isa-app --skip-cache
|
||||||
|
|
||||||
|
# Run a single test file
|
||||||
|
npx nx run <project-name>:test --testFile=<path-to-test-file> --skip-cache
|
||||||
|
|
||||||
|
# Run tests with coverage
|
||||||
|
npx nx run <project-name>:test --code-coverage --skip-cache
|
||||||
|
|
||||||
|
# Run tests in watch mode
|
||||||
|
npx nx run <project-name>:test --watch
|
||||||
|
```
|
||||||
|
|
||||||
|
### Linting Commands
|
||||||
|
```bash
|
||||||
|
# Lint a specific project
|
||||||
|
npx nx lint <project-name>
|
||||||
|
# Example: npx nx lint remission-data-access
|
||||||
|
|
||||||
|
# Run linting for all projects
|
||||||
|
npx nx run-many -t lint
|
||||||
|
```
|
||||||
|
|
||||||
|
### Other Useful Commands
|
||||||
|
```bash
|
||||||
|
# Generate Swagger API clients
|
||||||
|
npm run generate:swagger
|
||||||
|
|
||||||
|
# Start Storybook
|
||||||
|
npx nx run isa-app:storybook
|
||||||
|
|
||||||
|
# Format code with Prettier
|
||||||
|
npm run prettier
|
||||||
|
|
||||||
|
# List all projects in the workspace
|
||||||
|
npx nx list
|
||||||
|
|
||||||
|
# Show project dependencies graph
|
||||||
|
npx nx graph
|
||||||
|
|
||||||
|
# Run affected tests (based on git changes)
|
||||||
|
npx nx affected:test
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing Framework
|
||||||
|
|
||||||
|
### Current Setup
|
||||||
|
- **Jest**: Primary test runner for existing libraries
|
||||||
|
- **Vitest**: Being adopted for new libraries (migration in progress)
|
||||||
|
- **Testing Utilities**:
|
||||||
|
- **Angular Testing Utilities** (TestBed, ComponentFixture): Use for new tests
|
||||||
|
- **Spectator**: Legacy testing utility for existing tests
|
||||||
|
- **ng-mocks**: For advanced mocking scenarios
|
||||||
|
|
||||||
|
### Test File Requirements
|
||||||
|
- Test files must end with `.spec.ts`
|
||||||
|
- Use AAA pattern (Arrange-Act-Assert)
|
||||||
|
- Include E2E testing attributes (`data-what`, `data-which`) in HTML templates
|
||||||
|
- Mock external dependencies and child components
|
||||||
|
|
||||||
|
## State Management
|
||||||
|
- **NgRx**: Store, Effects, Entity, Component Store, Signals
|
||||||
|
- **RxJS**: For reactive programming patterns
|
||||||
|
|
||||||
|
## Styling
|
||||||
|
- **Tailwind CSS**: Primary styling framework with custom configuration
|
||||||
|
- **SCSS**: For component-specific styles
|
||||||
|
- **Custom Tailwind plugins**: For buttons, inputs, menus, typography
|
||||||
|
|
||||||
|
## API Integration
|
||||||
|
- **Generated Swagger Clients**: Auto-generated TypeScript clients from OpenAPI specs
|
||||||
|
- **Available APIs**: availability, cat-search, checkout, crm, eis, inventory, isa, oms, print, wws
|
||||||
|
|
||||||
|
## Build Configuration
|
||||||
|
- **Angular 20.1.2**: Latest Angular version
|
||||||
|
- **TypeScript 5.8.3**: For type safety
|
||||||
|
- **Node.js >= 22.0.0**: Required Node version
|
||||||
|
- **npm >= 10.0.0**: Required npm version
|
||||||
|
|
||||||
|
## Important Conventions
|
||||||
|
- **Component Prefix**: Each library has its own prefix (e.g., `remi` for remission, `oms` for OMS)
|
||||||
|
- **Standalone Components**: All new components should be standalone
|
||||||
|
- **Path Aliases**: Use TypeScript path aliases defined in `tsconfig.base.json` (e.g., `@isa/core/config`)
|
||||||
|
- **Project Names**: Can be found in each library's `project.json` file
|
||||||
|
|
||||||
|
## Development Workflow Tips
|
||||||
|
- Always use `npx nx run` pattern for executing tasks
|
||||||
|
- Include `--skip-cache` flag when running tests to ensure fresh results
|
||||||
|
- Use Nx's affected commands to optimize CI/CD pipelines
|
||||||
|
- Project graph visualization helps understand dependencies: `npx nx graph`
|
||||||
|
|
||||||
|
## Development Notes
|
||||||
|
- Use start target to start the application. Only one project can be started: isa-app
|
||||||
|
- Make sure to have a look at @docs/guidelines/testing.md before writing tests
|
||||||
|
- Make sure to add e2e attributes to the html. Those are important for my colleagues writen e2e tests
|
||||||
|
- Guide for the e2e testing attributes can be found in the testing.md
|
||||||
|
- When reviewing code follow the instructions @.github/review-instructions.md
|
||||||
@@ -7,8 +7,9 @@ LABEL build.uniqueid="${BuildUniqueID:-1}"
|
|||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN umask 0022
|
RUN umask 0022
|
||||||
|
RUN npm install -g npm@11.6
|
||||||
RUN npm version ${SEMVERSION}
|
RUN npm version ${SEMVERSION}
|
||||||
RUN npm install --foreground-scripts
|
RUN npm ci --foreground-scripts
|
||||||
RUN if [ "${IS_PRODUCTION}" = "true" ] ; then npm run-script build-prod ; else npm run-script build ; fi
|
RUN if [ "${IS_PRODUCTION}" = "true" ] ; then npm run-script build-prod ; else npm run-script build ; fi
|
||||||
|
|
||||||
# stage final
|
# stage final
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { isDevMode, NgModule } from '@angular/core';
|
import { inject, isDevMode, NgModule } from '@angular/core';
|
||||||
import { RouterModule, Routes } from '@angular/router';
|
import { Location } from '@angular/common';
|
||||||
|
import { RouterModule, Routes, Router } from '@angular/router';
|
||||||
import {
|
import {
|
||||||
CanActivateCartGuard,
|
CanActivateCartGuard,
|
||||||
CanActivateCartWithProcessIdGuard,
|
CanActivateCartWithProcessIdGuard,
|
||||||
@@ -30,7 +31,12 @@ import {
|
|||||||
ActivateProcessIdWithConfigKeyGuard,
|
ActivateProcessIdWithConfigKeyGuard,
|
||||||
} from './guards/activate-process-id.guard';
|
} from './guards/activate-process-id.guard';
|
||||||
import { MatomoRouteData } from 'ngx-matomo-client';
|
import { MatomoRouteData } from 'ngx-matomo-client';
|
||||||
import { tabResolverFn } from '@isa/core/tabs';
|
import {
|
||||||
|
tabResolverFn,
|
||||||
|
TabService,
|
||||||
|
TabNavigationService,
|
||||||
|
processResolverFn,
|
||||||
|
} from '@isa/core/tabs';
|
||||||
import { provideScrollPositionRestoration } from '@isa/utils/scroll-position';
|
import { provideScrollPositionRestoration } from '@isa/utils/scroll-position';
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
@@ -182,9 +188,14 @@ const routes: Routes = [
|
|||||||
{
|
{
|
||||||
path: ':tabId',
|
path: ':tabId',
|
||||||
component: MainComponent,
|
component: MainComponent,
|
||||||
resolve: { process: tabResolverFn, tab: tabResolverFn },
|
resolve: { process: processResolverFn, tab: tabResolverFn },
|
||||||
canActivate: [IsAuthenticatedGuard],
|
canActivate: [IsAuthenticatedGuard],
|
||||||
children: [
|
children: [
|
||||||
|
{
|
||||||
|
path: 'reward',
|
||||||
|
loadChildren: () =>
|
||||||
|
import('@isa/checkout/feature/reward-catalog').then((m) => m.routes),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'return',
|
path: 'return',
|
||||||
loadChildren: () =>
|
loadChildren: () =>
|
||||||
@@ -222,10 +233,18 @@ if (isDevMode()) {
|
|||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
RouterModule.forRoot(routes, { bindToComponentInputs: true }),
|
RouterModule.forRoot(routes, {
|
||||||
|
bindToComponentInputs: true,
|
||||||
|
enableTracing: false,
|
||||||
|
}),
|
||||||
TokenLoginModule,
|
TokenLoginModule,
|
||||||
],
|
],
|
||||||
exports: [RouterModule],
|
exports: [RouterModule],
|
||||||
providers: [provideScrollPositionRestoration()],
|
providers: [provideScrollPositionRestoration()],
|
||||||
})
|
})
|
||||||
export class AppRoutingModule {}
|
export class AppRoutingModule {
|
||||||
|
constructor() {
|
||||||
|
// Loading TabNavigationService to ensure tab state is synced with tab location
|
||||||
|
inject(TabNavigationService);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,35 +1,34 @@
|
|||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { EffectsModule } from '@ngrx/effects';
|
import { EffectsModule } from '@ngrx/effects';
|
||||||
import { ActionReducer, MetaReducer, StoreModule } from '@ngrx/store';
|
import { ActionReducer, MetaReducer, StoreModule } from '@ngrx/store';
|
||||||
import { StoreDevtoolsModule } from '@ngrx/store-devtools';
|
import { StoreDevtoolsModule } from '@ngrx/store-devtools';
|
||||||
import packageInfo from 'packageJson';
|
import { environment } from '../environments/environment';
|
||||||
import { environment } from '../environments/environment';
|
import { rootReducer } from './store/root.reducer';
|
||||||
import { RootStateService } from './store/root-state.service';
|
import { RootState } from './store/root.state';
|
||||||
import { rootReducer } from './store/root.reducer';
|
|
||||||
import { RootState } from './store/root.state';
|
export function storeInLocalStorage(
|
||||||
|
reducer: ActionReducer<any>,
|
||||||
export function storeInLocalStorage(reducer: ActionReducer<any>): ActionReducer<any> {
|
): ActionReducer<any> {
|
||||||
return function (state, action) {
|
return function (state, action) {
|
||||||
if (action.type === 'HYDRATE') {
|
if (action.type === 'HYDRATE') {
|
||||||
const initialState = RootStateService.LoadFromLocalStorage();
|
return reducer(action['payload'], action);
|
||||||
|
}
|
||||||
if (initialState?.version === packageInfo.version) {
|
return reducer(state, action);
|
||||||
return reducer(initialState, action);
|
};
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return reducer(state, action);
|
export const metaReducers: MetaReducer<RootState>[] = !environment.production
|
||||||
};
|
? [storeInLocalStorage]
|
||||||
}
|
: [storeInLocalStorage];
|
||||||
|
|
||||||
export const metaReducers: MetaReducer<RootState>[] = !environment.production
|
@NgModule({
|
||||||
? [storeInLocalStorage]
|
imports: [
|
||||||
: [storeInLocalStorage];
|
StoreModule.forRoot(rootReducer, { metaReducers }),
|
||||||
|
EffectsModule.forRoot([]),
|
||||||
@NgModule({
|
StoreDevtoolsModule.instrument({
|
||||||
imports: [
|
name: 'ISA Ngrx Application Store',
|
||||||
StoreModule.forRoot(rootReducer, { metaReducers }),
|
connectInZone: true,
|
||||||
EffectsModule.forRoot([]),
|
}),
|
||||||
StoreDevtoolsModule.instrument({ name: 'ISA Ngrx Application Store', connectInZone: true }),
|
],
|
||||||
],
|
})
|
||||||
})
|
export class AppStoreModule {}
|
||||||
export class AppStoreModule {}
|
|
||||||
|
|||||||
@@ -1,238 +1,263 @@
|
|||||||
import {
|
import { version } from '../../../../package.json';
|
||||||
HTTP_INTERCEPTORS,
|
import {
|
||||||
provideHttpClient,
|
HTTP_INTERCEPTORS,
|
||||||
withInterceptorsFromDi,
|
provideHttpClient,
|
||||||
} from '@angular/common/http';
|
withInterceptorsFromDi,
|
||||||
import {
|
} from '@angular/common/http';
|
||||||
DEFAULT_CURRENCY_CODE,
|
import {
|
||||||
ErrorHandler,
|
DEFAULT_CURRENCY_CODE,
|
||||||
Injector,
|
ErrorHandler,
|
||||||
LOCALE_ID,
|
Injector,
|
||||||
NgModule,
|
LOCALE_ID,
|
||||||
inject,
|
NgModule,
|
||||||
provideAppInitializer,
|
inject,
|
||||||
} from '@angular/core';
|
provideAppInitializer,
|
||||||
import { BrowserModule } from '@angular/platform-browser';
|
} from '@angular/core';
|
||||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
import { BrowserModule } from '@angular/platform-browser';
|
||||||
import { PlatformModule } from '@angular/cdk/platform';
|
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
|
import { PlatformModule } from '@angular/cdk/platform';
|
||||||
import { Config } from '@core/config';
|
|
||||||
import { AuthModule, AuthService, LoginStrategy } from '@core/auth';
|
import { Config } from '@core/config';
|
||||||
import { CoreCommandModule } from '@core/command';
|
import { AuthModule, AuthService, LoginStrategy } from '@core/auth';
|
||||||
|
import { CoreCommandModule } from '@core/command';
|
||||||
import { AppRoutingModule } from './app-routing.module';
|
|
||||||
import { AppComponent } from './app.component';
|
import { AppRoutingModule } from './app-routing.module';
|
||||||
import { CoreApplicationModule } from '@core/application';
|
import { AppComponent } from './app.component';
|
||||||
import { AppStoreModule } from './app-store.module';
|
import {
|
||||||
import { ServiceWorkerModule } from '@angular/service-worker';
|
ApplicationService,
|
||||||
import { environment } from '../environments/environment';
|
ApplicationServiceAdapter,
|
||||||
import { AppSwaggerModule } from './app-swagger.module';
|
CoreApplicationModule,
|
||||||
import { AppDomainModule } from './app-domain.module';
|
} from '@core/application';
|
||||||
import { UiModalModule } from '@ui/modal';
|
import { AppStoreModule } from './app-store.module';
|
||||||
import {
|
import { ServiceWorkerModule } from '@angular/service-worker';
|
||||||
NotificationsHubModule,
|
import { environment } from '../environments/environment';
|
||||||
NOTIFICATIONS_HUB_OPTIONS,
|
import { AppSwaggerModule } from './app-swagger.module';
|
||||||
} from '@hub/notifications';
|
import { AppDomainModule } from './app-domain.module';
|
||||||
import { SignalRHubOptions } from '@core/signalr';
|
import { UiModalModule } from '@ui/modal';
|
||||||
import { CoreBreadcrumbModule } from '@core/breadcrumb';
|
import {
|
||||||
import { UiCommonModule } from '@ui/common';
|
NotificationsHubModule,
|
||||||
import { registerLocaleData } from '@angular/common';
|
NOTIFICATIONS_HUB_OPTIONS,
|
||||||
|
} from '@hub/notifications';
|
||||||
import localeDe from '@angular/common/locales/de';
|
import { SignalRHubOptions } from '@core/signalr';
|
||||||
import localeDeExtra from '@angular/common/locales/extra/de';
|
import { CoreBreadcrumbModule } from '@core/breadcrumb';
|
||||||
import { HttpErrorInterceptor } from './interceptors';
|
import { UiCommonModule } from '@ui/common';
|
||||||
import { CoreLoggerModule, LOG_PROVIDER } from '@core/logger';
|
import { registerLocaleData } from '@angular/common';
|
||||||
import { IsaLogProvider } from './providers';
|
|
||||||
import { IsaErrorHandler } from './providers/isa.error-handler';
|
import localeDe from '@angular/common/locales/de';
|
||||||
import {
|
import localeDeExtra from '@angular/common/locales/extra/de';
|
||||||
ScanAdapterModule,
|
import { HttpErrorInterceptor } from './interceptors';
|
||||||
ScanAdapterService,
|
import { CoreLoggerModule, LOG_PROVIDER } from '@core/logger';
|
||||||
ScanditScanAdapterModule,
|
import { IsaLogProvider } from './providers';
|
||||||
} from '@adapter/scan';
|
import { IsaErrorHandler } from './providers/isa.error-handler';
|
||||||
import { RootStateService } from './store/root-state.service';
|
import {
|
||||||
import * as Commands from './commands';
|
ScanAdapterModule,
|
||||||
import { PreviewComponent } from './preview';
|
ScanAdapterService,
|
||||||
import { NativeContainerService } from '@external/native-container';
|
ScanditScanAdapterModule,
|
||||||
import { ShellModule } from '@shared/shell';
|
} from '@adapter/scan';
|
||||||
import { MainComponent } from './main.component';
|
import * as Commands from './commands';
|
||||||
import { IconModule } from '@shared/components/icon';
|
import { PreviewComponent } from './preview';
|
||||||
import { NgIconsModule } from '@ng-icons/core';
|
import { NativeContainerService } from '@external/native-container';
|
||||||
import {
|
import { ShellModule } from '@shared/shell';
|
||||||
matClose,
|
import { MainComponent } from './main.component';
|
||||||
matWifi,
|
import { IconModule } from '@shared/components/icon';
|
||||||
matWifiOff,
|
import { NgIconsModule } from '@ng-icons/core';
|
||||||
} from '@ng-icons/material-icons/baseline';
|
import {
|
||||||
import { NetworkStatusService } from './services/network-status.service';
|
matClose,
|
||||||
import { firstValueFrom } from 'rxjs';
|
matWifi,
|
||||||
import { provideMatomo } from 'ngx-matomo-client';
|
matWifiOff,
|
||||||
import { withRouter, withRouteData } from 'ngx-matomo-client';
|
} from '@ng-icons/material-icons/baseline';
|
||||||
import {
|
import { NetworkStatusService } from './services/network-status.service';
|
||||||
provideLogging,
|
import { debounceTime, firstValueFrom } from 'rxjs';
|
||||||
withLogLevel,
|
import { provideMatomo } from 'ngx-matomo-client';
|
||||||
LogLevel,
|
import { withRouter, withRouteData } from 'ngx-matomo-client';
|
||||||
withSink,
|
import {
|
||||||
ConsoleLogSink,
|
provideLogging,
|
||||||
} from '@isa/core/logging';
|
withLogLevel,
|
||||||
|
LogLevel,
|
||||||
registerLocaleData(localeDe, localeDeExtra);
|
withSink,
|
||||||
registerLocaleData(localeDe, 'de', localeDeExtra);
|
ConsoleLogSink,
|
||||||
|
} from '@isa/core/logging';
|
||||||
export function _appInitializerFactory(config: Config, injector: Injector) {
|
import { IDBStorageProvider, UserStorageProvider } from '@isa/core/storage';
|
||||||
return async () => {
|
import { Store } from '@ngrx/store';
|
||||||
const statusElement = document.querySelector('#init-status');
|
|
||||||
const laoderElement = document.querySelector('#init-loader');
|
registerLocaleData(localeDe, localeDeExtra);
|
||||||
|
registerLocaleData(localeDe, 'de', localeDeExtra);
|
||||||
try {
|
|
||||||
let online = false;
|
export function _appInitializerFactory(config: Config, injector: Injector) {
|
||||||
const networkStatus = injector.get(NetworkStatusService);
|
return async () => {
|
||||||
while (!online) {
|
const statusElement = document.querySelector('#init-status');
|
||||||
online = await firstValueFrom(networkStatus.online$);
|
const laoderElement = document.querySelector('#init-loader');
|
||||||
|
|
||||||
if (!online) {
|
try {
|
||||||
statusElement.innerHTML =
|
let online = false;
|
||||||
'<b>Warte auf Netzwerkverbindung (WLAN)</b><br><br>Bitte prüfen Sie die Netzwerkverbindung (WLAN).<br>Sobald eine Netzwerkverbindung besteht, wird die App automatisch neu geladen.';
|
const networkStatus = injector.get(NetworkStatusService);
|
||||||
await new Promise((resolve) => setTimeout(resolve, 250));
|
while (!online) {
|
||||||
}
|
online = await firstValueFrom(networkStatus.online$);
|
||||||
}
|
|
||||||
|
if (!online) {
|
||||||
statusElement.innerHTML = 'Konfigurationen werden geladen...';
|
statusElement.innerHTML =
|
||||||
|
'<b>Warte auf Netzwerkverbindung (WLAN)</b><br><br>Bitte prüfen Sie die Netzwerkverbindung (WLAN).<br>Sobald eine Netzwerkverbindung besteht, wird die App automatisch neu geladen.';
|
||||||
statusElement.innerHTML = 'Scanner wird initialisiert...';
|
await new Promise((resolve) => setTimeout(resolve, 250));
|
||||||
const scanAdapter = injector.get(ScanAdapterService);
|
}
|
||||||
await scanAdapter.init();
|
}
|
||||||
|
|
||||||
statusElement.innerHTML = 'Authentifizierung wird geprüft...';
|
statusElement.innerHTML = 'Konfigurationen werden geladen...';
|
||||||
|
|
||||||
const auth = injector.get(AuthService);
|
statusElement.innerHTML = 'Scanner wird initialisiert...';
|
||||||
try {
|
const scanAdapter = injector.get(ScanAdapterService);
|
||||||
await auth.init();
|
await scanAdapter.init();
|
||||||
} catch (error) {
|
|
||||||
statusElement.innerHTML = 'Authentifizierung wird durchgeführt...';
|
statusElement.innerHTML = 'Authentifizierung wird geprüft...';
|
||||||
const strategy = injector.get(LoginStrategy);
|
|
||||||
await strategy.login();
|
const auth = injector.get(AuthService);
|
||||||
}
|
try {
|
||||||
|
await auth.init();
|
||||||
statusElement.innerHTML = 'App wird initialisiert...';
|
} catch (error) {
|
||||||
const state = injector.get(RootStateService);
|
statusElement.innerHTML = 'Authentifizierung wird durchgeführt...';
|
||||||
await state.init();
|
const strategy = injector.get(LoginStrategy);
|
||||||
|
await strategy.login();
|
||||||
statusElement.innerHTML = 'Native Container wird initialisiert...';
|
}
|
||||||
const nativeContainer = injector.get(NativeContainerService);
|
|
||||||
await nativeContainer.init();
|
statusElement.innerHTML = 'Native Container wird initialisiert...';
|
||||||
} catch (error) {
|
const nativeContainer = injector.get(NativeContainerService);
|
||||||
laoderElement.remove();
|
await nativeContainer.init();
|
||||||
statusElement.classList.add('text-xl');
|
|
||||||
statusElement.innerHTML +=
|
statusElement.innerHTML = 'Datenbank wird initialisiert...';
|
||||||
'⚡<br><br><b>Fehler bei der Initialisierung</b><br><br>Bitte prüfen Sie die Netzwerkverbindung (WLAN).<br><br>';
|
await injector.get(IDBStorageProvider).init();
|
||||||
|
|
||||||
const reload = document.createElement('button');
|
statusElement.innerHTML = 'Benutzerzustand wird geladen...';
|
||||||
reload.classList.add(
|
const userStorage = injector.get(UserStorageProvider);
|
||||||
'bg-brand',
|
await userStorage.init();
|
||||||
'text-white',
|
|
||||||
'p-2',
|
const store = injector.get(Store);
|
||||||
'rounded',
|
// Hydrate Ngrx Store
|
||||||
'cursor-pointer',
|
const state = userStorage.get('store');
|
||||||
);
|
if (state && state['version'] === version) {
|
||||||
reload.innerHTML = 'App neu laden';
|
store.dispatch({ type: 'HYDRATE', payload: userStorage.get('store') });
|
||||||
reload.onclick = () => window.location.reload();
|
}
|
||||||
statusElement.appendChild(reload);
|
// Subscribe on Store changes and save to user storage
|
||||||
|
store.pipe(debounceTime(1000)).subscribe((state) => {
|
||||||
const preLabel = document.createElement('div');
|
userStorage.set('store', state);
|
||||||
preLabel.classList.add('mt-12');
|
});
|
||||||
preLabel.innerHTML = 'Fehlermeldung:';
|
} catch (error) {
|
||||||
|
console.error('Error during app initialization', error);
|
||||||
statusElement.appendChild(preLabel);
|
laoderElement.remove();
|
||||||
|
statusElement.classList.add('text-xl');
|
||||||
const pre = document.createElement('pre');
|
statusElement.innerHTML +=
|
||||||
pre.classList.add('mt-4', 'text-wrap');
|
'⚡<br><br><b>Fehler bei der Initialisierung</b><br><br>Bitte prüfen Sie die Netzwerkverbindung (WLAN).<br><br>';
|
||||||
pre.innerHTML = error.message;
|
|
||||||
|
const reload = document.createElement('button');
|
||||||
statusElement.appendChild(pre);
|
reload.classList.add(
|
||||||
|
'bg-brand',
|
||||||
console.error('Error during app initialization', error);
|
'text-white',
|
||||||
throw error;
|
'p-2',
|
||||||
}
|
'rounded',
|
||||||
};
|
'cursor-pointer',
|
||||||
}
|
);
|
||||||
|
reload.innerHTML = 'App neu laden';
|
||||||
export function _notificationsHubOptionsFactory(
|
reload.onclick = () => window.location.reload();
|
||||||
config: Config,
|
statusElement.appendChild(reload);
|
||||||
auth: AuthService,
|
|
||||||
): SignalRHubOptions {
|
const preLabel = document.createElement('div');
|
||||||
const options = { ...config.get('hubs').notifications };
|
preLabel.classList.add('mt-12');
|
||||||
options.httpOptions.accessTokenFactory = () => auth.getToken();
|
preLabel.innerHTML = 'Fehlermeldung:';
|
||||||
return options;
|
|
||||||
}
|
statusElement.appendChild(preLabel);
|
||||||
|
|
||||||
@NgModule({
|
const pre = document.createElement('pre');
|
||||||
declarations: [AppComponent, MainComponent],
|
pre.classList.add('mt-4', 'text-wrap');
|
||||||
bootstrap: [AppComponent],
|
pre.innerHTML = error.message;
|
||||||
imports: [
|
|
||||||
BrowserModule,
|
statusElement.appendChild(pre);
|
||||||
BrowserAnimationsModule,
|
|
||||||
ShellModule.forRoot(),
|
console.error('Error during app initialization', error);
|
||||||
AppRoutingModule,
|
throw error;
|
||||||
AppSwaggerModule,
|
}
|
||||||
AppDomainModule,
|
};
|
||||||
CoreBreadcrumbModule.forRoot(),
|
}
|
||||||
CoreCommandModule.forRoot(Object.values(Commands)),
|
|
||||||
CoreLoggerModule.forRoot(),
|
export function _notificationsHubOptionsFactory(
|
||||||
AppStoreModule,
|
config: Config,
|
||||||
PreviewComponent,
|
auth: AuthService,
|
||||||
AuthModule.forRoot(),
|
): SignalRHubOptions {
|
||||||
CoreApplicationModule.forRoot(),
|
const options = { ...config.get('hubs').notifications };
|
||||||
UiModalModule.forRoot(),
|
options.httpOptions.accessTokenFactory = () => auth.getToken();
|
||||||
UiCommonModule.forRoot(),
|
return options;
|
||||||
NotificationsHubModule.forRoot(),
|
}
|
||||||
ServiceWorkerModule.register('ngsw-worker.js', {
|
|
||||||
enabled: environment.production,
|
@NgModule({
|
||||||
registrationStrategy: 'registerWhenStable:30000',
|
declarations: [AppComponent, MainComponent],
|
||||||
}),
|
bootstrap: [AppComponent],
|
||||||
ScanAdapterModule.forRoot(),
|
imports: [
|
||||||
ScanditScanAdapterModule.forRoot(),
|
BrowserModule,
|
||||||
PlatformModule,
|
BrowserAnimationsModule,
|
||||||
IconModule.forRoot(),
|
ShellModule.forRoot(),
|
||||||
NgIconsModule.withIcons({ matWifiOff, matClose, matWifi }),
|
AppRoutingModule,
|
||||||
],
|
AppSwaggerModule,
|
||||||
providers: [
|
AppDomainModule,
|
||||||
provideAppInitializer(() => {
|
CoreBreadcrumbModule.forRoot(),
|
||||||
const initializerFn = _appInitializerFactory(
|
CoreCommandModule.forRoot(Object.values(Commands)),
|
||||||
inject(Config),
|
CoreLoggerModule.forRoot(),
|
||||||
inject(Injector),
|
AppStoreModule,
|
||||||
);
|
PreviewComponent,
|
||||||
return initializerFn();
|
AuthModule.forRoot(),
|
||||||
}),
|
CoreApplicationModule.forRoot(),
|
||||||
{
|
UiModalModule.forRoot(),
|
||||||
provide: NOTIFICATIONS_HUB_OPTIONS,
|
UiCommonModule.forRoot(),
|
||||||
useFactory: _notificationsHubOptionsFactory,
|
NotificationsHubModule.forRoot(),
|
||||||
deps: [Config, AuthService],
|
ServiceWorkerModule.register('ngsw-worker.js', {
|
||||||
},
|
enabled: environment.production,
|
||||||
{
|
registrationStrategy: 'registerWhenStable:30000',
|
||||||
provide: HTTP_INTERCEPTORS,
|
}),
|
||||||
useClass: HttpErrorInterceptor,
|
ScanAdapterModule.forRoot(),
|
||||||
multi: true,
|
ScanditScanAdapterModule.forRoot(),
|
||||||
},
|
PlatformModule,
|
||||||
{
|
IconModule.forRoot(),
|
||||||
provide: LOG_PROVIDER,
|
NgIconsModule.withIcons({ matWifiOff, matClose, matWifi }),
|
||||||
useClass: IsaLogProvider,
|
],
|
||||||
multi: true,
|
providers: [
|
||||||
},
|
provideAppInitializer(() => {
|
||||||
{
|
const initializerFn = _appInitializerFactory(
|
||||||
provide: ErrorHandler,
|
inject(Config),
|
||||||
useClass: IsaErrorHandler,
|
inject(Injector),
|
||||||
},
|
);
|
||||||
{ provide: LOCALE_ID, useValue: 'de-DE' },
|
return initializerFn();
|
||||||
provideHttpClient(withInterceptorsFromDi()),
|
}),
|
||||||
provideMatomo(
|
{
|
||||||
{ trackerUrl: 'https://matomo.paragon-data.net', siteId: '1' },
|
provide: NOTIFICATIONS_HUB_OPTIONS,
|
||||||
withRouter(),
|
useFactory: _notificationsHubOptionsFactory,
|
||||||
withRouteData(),
|
deps: [Config, AuthService],
|
||||||
),
|
},
|
||||||
provideLogging(withLogLevel(LogLevel.Debug), withSink(ConsoleLogSink)),
|
{
|
||||||
{
|
provide: HTTP_INTERCEPTORS,
|
||||||
provide: DEFAULT_CURRENCY_CODE,
|
useClass: HttpErrorInterceptor,
|
||||||
useValue: 'EUR',
|
multi: true,
|
||||||
},
|
},
|
||||||
],
|
{
|
||||||
})
|
provide: LOG_PROVIDER,
|
||||||
export class AppModule {}
|
useClass: IsaLogProvider,
|
||||||
|
multi: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: ErrorHandler,
|
||||||
|
useClass: IsaErrorHandler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: ApplicationService,
|
||||||
|
useClass: ApplicationServiceAdapter,
|
||||||
|
},
|
||||||
|
{ provide: LOCALE_ID, useValue: 'de-DE' },
|
||||||
|
provideHttpClient(withInterceptorsFromDi()),
|
||||||
|
provideMatomo(
|
||||||
|
{ trackerUrl: 'https://matomo.paragon-data.net', siteId: '1' },
|
||||||
|
withRouter(),
|
||||||
|
withRouteData(),
|
||||||
|
),
|
||||||
|
provideLogging(withLogLevel(LogLevel.Debug), withSink(ConsoleLogSink)),
|
||||||
|
{
|
||||||
|
provide: DEFAULT_CURRENCY_CODE,
|
||||||
|
useValue: 'EUR',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class AppModule {}
|
||||||
|
|||||||
@@ -1,67 +1,74 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
|
import { ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
|
||||||
import { ApplicationProcess, ApplicationService } from '@core/application';
|
import { ApplicationProcess, ApplicationService } from '@core/application';
|
||||||
import { BreadcrumbService } from '@core/breadcrumb';
|
import { BreadcrumbService } from '@core/breadcrumb';
|
||||||
import { first } from 'rxjs/operators';
|
import { first } from 'rxjs/operators';
|
||||||
|
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class CanActivateProductWithProcessIdGuard {
|
export class CanActivateProductWithProcessIdGuard {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly _applicationService: ApplicationService,
|
private readonly _applicationService: ApplicationService,
|
||||||
private readonly _breadcrumbService: BreadcrumbService,
|
private readonly _breadcrumbService: BreadcrumbService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
|
async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
|
||||||
const process = await this._applicationService.getProcessById$(+route.params.processId).pipe(first()).toPromise();
|
const processId = +route.params.processId;
|
||||||
|
const process = await this._applicationService
|
||||||
// if (!(process?.type === 'cart')) {
|
.getProcessById$(processId)
|
||||||
// // TODO:
|
.pipe(first())
|
||||||
// // Anderer Prozesstyp mit gleicher Id - Was soll gemacht werden?
|
.toPromise();
|
||||||
// return false;
|
|
||||||
// }
|
// if (!(process?.type === 'cart')) {
|
||||||
|
// // TODO:
|
||||||
if (!process) {
|
// // Anderer Prozesstyp mit gleicher Id - Was soll gemacht werden?
|
||||||
const processes = await this._applicationService.getProcesses$('customer').pipe(first()).toPromise();
|
// return false;
|
||||||
await this._applicationService.createProcess({
|
// }
|
||||||
id: +route.params.processId,
|
|
||||||
type: 'cart',
|
if (!process) {
|
||||||
section: 'customer',
|
await this._applicationService.createCustomerProcess(processId);
|
||||||
name: `Vorgang ${this.processNumber(processes.filter((process) => process.type === 'cart'))}`,
|
}
|
||||||
});
|
|
||||||
}
|
await this.removeBreadcrumbWithSameProcessId(route);
|
||||||
|
this._applicationService.activateProcess(+route.params.processId);
|
||||||
await this.removeBreadcrumbWithSameProcessId(route);
|
return true;
|
||||||
this._applicationService.activateProcess(+route.params.processId);
|
}
|
||||||
return true;
|
|
||||||
}
|
// Fix #3292: Alle Breadcrumbs die nichts mit dem aktuellen Prozess zu tun haben, müssen removed werden
|
||||||
|
async removeBreadcrumbWithSameProcessId(route: ActivatedRouteSnapshot) {
|
||||||
// Fix #3292: Alle Breadcrumbs die nichts mit dem aktuellen Prozess zu tun haben, müssen removed werden
|
const crumbs = await this._breadcrumbService
|
||||||
async removeBreadcrumbWithSameProcessId(route: ActivatedRouteSnapshot) {
|
.getBreadcrumbByKey$(+route.params.processId)
|
||||||
const crumbs = await this._breadcrumbService.getBreadcrumbByKey$(+route.params.processId).pipe(first()).toPromise();
|
.pipe(first())
|
||||||
|
.toPromise();
|
||||||
// Entferne alle Crumbs die nichts mit der Artikelsuche zu tun haben
|
|
||||||
if (crumbs.length > 1) {
|
// Entferne alle Crumbs die nichts mit der Artikelsuche zu tun haben
|
||||||
const crumbsToRemove = crumbs.filter((crumb) => crumb.tags.find((tag) => tag === 'catalog') === undefined);
|
if (crumbs.length > 1) {
|
||||||
for (const crumb of crumbsToRemove) {
|
const crumbsToRemove = crumbs.filter(
|
||||||
await this._breadcrumbService.removeBreadcrumb(crumb.id);
|
(crumb) => crumb.tags.find((tag) => tag === 'catalog') === undefined,
|
||||||
}
|
);
|
||||||
}
|
for (const crumb of crumbsToRemove) {
|
||||||
}
|
await this._breadcrumbService.removeBreadcrumb(crumb.id);
|
||||||
|
}
|
||||||
processNumber(processes: ApplicationProcess[]) {
|
}
|
||||||
const processNumbers = processes?.map((process) => Number(process?.name?.replace(/\D/g, '')));
|
}
|
||||||
return !!processNumbers && processNumbers.length > 0 ? this.findMissingNumber(processNumbers) : 1;
|
|
||||||
}
|
processNumber(processes: ApplicationProcess[]) {
|
||||||
|
const processNumbers = processes?.map((process) =>
|
||||||
findMissingNumber(processNumbers: number[]) {
|
Number(process?.name?.replace(/\D/g, '')),
|
||||||
// Ticket #3272 Bei Klick auf "+" bzw. neuen Prozess hinzufügen soll der neue Tab immer die höchste Nummer haben (wie aktuell im Produktiv)
|
);
|
||||||
// ----------------------------------------------------------------------------------------------------------------------------------------
|
return !!processNumbers && processNumbers.length > 0
|
||||||
|
? this.findMissingNumber(processNumbers)
|
||||||
// for (let missingNumber = 1; missingNumber < Math.max(...processNumbers); missingNumber++) {
|
: 1;
|
||||||
// if (!processNumbers.find((number) => number === missingNumber)) {
|
}
|
||||||
// return missingNumber;
|
|
||||||
// }
|
findMissingNumber(processNumbers: number[]) {
|
||||||
// }
|
// Ticket #3272 Bei Klick auf "+" bzw. neuen Prozess hinzufügen soll der neue Tab immer die höchste Nummer haben (wie aktuell im Produktiv)
|
||||||
return Math.max(...processNumbers) + 1;
|
// ----------------------------------------------------------------------------------------------------------------------------------------
|
||||||
}
|
|
||||||
}
|
// for (let missingNumber = 1; missingNumber < Math.max(...processNumbers); missingNumber++) {
|
||||||
|
// if (!processNumbers.find((number) => number === missingNumber)) {
|
||||||
|
// return missingNumber;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
return Math.max(...processNumbers) + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ import {
|
|||||||
} from "@ui/modal";
|
} from "@ui/modal";
|
||||||
import { IsaLogProvider } from "./isa.log-provider";
|
import { IsaLogProvider } from "./isa.log-provider";
|
||||||
import { LogLevel } from "@core/logger";
|
import { LogLevel } from "@core/logger";
|
||||||
|
import { ZodError } from "zod";
|
||||||
|
import { extractZodErrorMessage } from "@isa/common/data-access";
|
||||||
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
@Injectable({ providedIn: "root" })
|
@Injectable({ providedIn: "root" })
|
||||||
export class IsaErrorHandler implements ErrorHandler {
|
export class IsaErrorHandler implements ErrorHandler {
|
||||||
@@ -28,7 +31,7 @@ export class IsaErrorHandler implements ErrorHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (error instanceof HttpErrorResponse && error?.status === 401) {
|
if (error instanceof HttpErrorResponse && error?.status === 401) {
|
||||||
await this._modal
|
await firstValueFrom(this._modal
|
||||||
.open({
|
.open({
|
||||||
content: UiDialogModalComponent,
|
content: UiDialogModalComponent,
|
||||||
title: "Sitzung abgelaufen",
|
title: "Sitzung abgelaufen",
|
||||||
@@ -41,12 +44,33 @@ export class IsaErrorHandler implements ErrorHandler {
|
|||||||
],
|
],
|
||||||
} as DialogModel,
|
} as DialogModel,
|
||||||
})
|
})
|
||||||
.afterClosed$.toPromise();
|
.afterClosed$);
|
||||||
|
|
||||||
this._authService.logout();
|
this._authService.logout();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle Zod validation errors
|
||||||
|
if (error instanceof ZodError) {
|
||||||
|
const zodErrorMessage = extractZodErrorMessage(error);
|
||||||
|
|
||||||
|
await firstValueFrom(this._modal
|
||||||
|
.open({
|
||||||
|
content: UiDialogModalComponent,
|
||||||
|
title: "Validierungsfehler",
|
||||||
|
data: {
|
||||||
|
handleCommand: false,
|
||||||
|
content: `Die eingegebenen Daten sind ungültig:\n\n${zodErrorMessage}`,
|
||||||
|
actions: [
|
||||||
|
{ command: "CLOSE", selected: true, label: "OK" },
|
||||||
|
],
|
||||||
|
} as DialogModel,
|
||||||
|
})
|
||||||
|
.afterClosed$);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this._isaLogProvider.log(LogLevel.ERROR, "Client Error", error);
|
this._isaLogProvider.log(LogLevel.ERROR, "Client Error", error);
|
||||||
} catch (logError) {
|
} catch (logError) {
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { ActivatedRouteSnapshot } from '@angular/router';
|
import { ActivatedRouteSnapshot } from '@angular/router';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
|
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class ProcessIdResolver {
|
export class ProcessIdResolver {
|
||||||
resolve(route: ActivatedRouteSnapshot): Observable<number> | Promise<number> | number {
|
resolve(
|
||||||
return route.params.processId;
|
route: ActivatedRouteSnapshot,
|
||||||
}
|
): Observable<number> | Promise<number> | number {
|
||||||
}
|
return route.params.processId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,128 +0,0 @@
|
|||||||
import { Injectable } from "@angular/core";
|
|
||||||
import { Logger, LogLevel } from "@core/logger";
|
|
||||||
import { Store } from "@ngrx/store";
|
|
||||||
import { debounceTime, switchMap, takeUntil } from "rxjs/operators";
|
|
||||||
import { RootState } from "./root.state";
|
|
||||||
import packageInfo from "packageJson";
|
|
||||||
import { environment } from "../../environments/environment";
|
|
||||||
import { Subject } from "rxjs";
|
|
||||||
import { AuthService } from "@core/auth";
|
|
||||||
import { injectStorage, UserStorageProvider } from "@isa/core/storage";
|
|
||||||
import { isEqual } from "lodash";
|
|
||||||
|
|
||||||
@Injectable({ providedIn: "root" })
|
|
||||||
export class RootStateService {
|
|
||||||
static LOCAL_STORAGE_KEY = "ISA_APP_INITIALSTATE";
|
|
||||||
|
|
||||||
#storage = injectStorage(UserStorageProvider);
|
|
||||||
|
|
||||||
private _cancelSave = new Subject<void>();
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private readonly _authService: AuthService,
|
|
||||||
private _logger: Logger,
|
|
||||||
private _store: Store,
|
|
||||||
) {
|
|
||||||
if (!environment.production) {
|
|
||||||
console.log(
|
|
||||||
'Die UserState kann in der Konsole mit der Funktion "clearUserState()" geleert werden.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
window["clearUserState"] = () => {
|
|
||||||
this.clear();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async init() {
|
|
||||||
await this.load();
|
|
||||||
this._store.dispatch({
|
|
||||||
type: "HYDRATE",
|
|
||||||
payload: RootStateService.LoadFromLocalStorage(),
|
|
||||||
});
|
|
||||||
this.initSave();
|
|
||||||
}
|
|
||||||
|
|
||||||
initSave() {
|
|
||||||
this._store
|
|
||||||
.select((state) => state)
|
|
||||||
.pipe(
|
|
||||||
takeUntil(this._cancelSave),
|
|
||||||
debounceTime(1000),
|
|
||||||
switchMap((state) => {
|
|
||||||
const data = {
|
|
||||||
...state,
|
|
||||||
version: packageInfo.version,
|
|
||||||
sub: this._authService.getClaimByKey("sub"),
|
|
||||||
};
|
|
||||||
RootStateService.SaveToLocalStorageRaw(JSON.stringify(data));
|
|
||||||
return this.#storage.set("state", data);
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.subscribe();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Loads the initial state from local storage and returns true/false if state was changed
|
|
||||||
*/
|
|
||||||
async load(): Promise<boolean> {
|
|
||||||
try {
|
|
||||||
const res = await this.#storage.get("state");
|
|
||||||
|
|
||||||
const storageContent = RootStateService.LoadFromLocalStorageRaw();
|
|
||||||
|
|
||||||
if (res) {
|
|
||||||
RootStateService.SaveToLocalStorageRaw(JSON.stringify(res));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isEqual(res, storageContent)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
this._logger.log(LogLevel.ERROR, error);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
async clear() {
|
|
||||||
try {
|
|
||||||
this._cancelSave.next();
|
|
||||||
await this.#storage.clear("state");
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
||||||
RootStateService.RemoveFromLocalStorage();
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
||||||
window.location.reload();
|
|
||||||
} catch (error) {
|
|
||||||
this._logger.log(LogLevel.ERROR, error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static SaveToLocalStorage(state: RootState) {
|
|
||||||
RootStateService.SaveToLocalStorageRaw(JSON.stringify(state));
|
|
||||||
}
|
|
||||||
|
|
||||||
static SaveToLocalStorageRaw(state: string) {
|
|
||||||
localStorage.setItem(RootStateService.LOCAL_STORAGE_KEY, state);
|
|
||||||
}
|
|
||||||
|
|
||||||
static LoadFromLocalStorage(): RootState {
|
|
||||||
const raw = RootStateService.LoadFromLocalStorageRaw();
|
|
||||||
if (raw) {
|
|
||||||
try {
|
|
||||||
return JSON.parse(raw);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error parsing local storage:", error);
|
|
||||||
this.RemoveFromLocalStorage();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
static LoadFromLocalStorageRaw(): string {
|
|
||||||
return localStorage.getItem(RootStateService.LOCAL_STORAGE_KEY);
|
|
||||||
}
|
|
||||||
|
|
||||||
static RemoveFromLocalStorage() {
|
|
||||||
localStorage.removeItem(RootStateService.LOCAL_STORAGE_KEY);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
338
apps/isa-app/src/core/application/application.service-adapter.ts
Normal file
338
apps/isa-app/src/core/application/application.service-adapter.ts
Normal file
@@ -0,0 +1,338 @@
|
|||||||
|
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,4 +1,5 @@
|
|||||||
export * from './application.module';
|
export * from './application.module';
|
||||||
export * from './application.service';
|
export * from './application.service';
|
||||||
|
export * from './application.service-adapter';
|
||||||
export * from './defs';
|
export * from './defs';
|
||||||
export * from './store';
|
export * from './store';
|
||||||
|
|||||||
@@ -1,55 +1,55 @@
|
|||||||
import {
|
import {
|
||||||
ChangeDetectionStrategy,
|
ChangeDetectionStrategy,
|
||||||
Component,
|
Component,
|
||||||
EventEmitter,
|
EventEmitter,
|
||||||
Input,
|
Input,
|
||||||
Output,
|
Output,
|
||||||
inject,
|
inject,
|
||||||
linkedSignal,
|
linkedSignal,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { PickupShelfInNavigationService } from '@shared/services/navigation';
|
import { PickupShelfInNavigationService } from '@shared/services/navigation';
|
||||||
import { UiFilter } from '@ui/filter';
|
import { UiFilter } from '@ui/filter';
|
||||||
import { MessageBoardItemDTO } from '@hub/notifications';
|
import { MessageBoardItemDTO } from '@hub/notifications';
|
||||||
import { TabService } from '@isa/core/tabs';
|
import { TabService } from '@isa/core/tabs';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'modal-notifications-remission-group',
|
selector: 'modal-notifications-remission-group',
|
||||||
templateUrl: 'notifications-remission-group.component.html',
|
templateUrl: 'notifications-remission-group.component.html',
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
standalone: false,
|
standalone: false,
|
||||||
})
|
})
|
||||||
export class ModalNotificationsRemissionGroupComponent {
|
export class ModalNotificationsRemissionGroupComponent {
|
||||||
tabService = inject(TabService);
|
tabService = inject(TabService);
|
||||||
private _pickupShelfInNavigationService = inject(
|
private _pickupShelfInNavigationService = inject(
|
||||||
PickupShelfInNavigationService,
|
PickupShelfInNavigationService,
|
||||||
);
|
);
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
notifications: MessageBoardItemDTO[];
|
notifications: MessageBoardItemDTO[];
|
||||||
|
|
||||||
@Output()
|
@Output()
|
||||||
navigated = new EventEmitter<void>();
|
navigated = new EventEmitter<void>();
|
||||||
|
|
||||||
remissionPath = linkedSignal(() => [
|
remissionPath = linkedSignal(() => [
|
||||||
'/',
|
'/',
|
||||||
this.tabService.activatedTab()?.id || this.tabService.nextId(),
|
this.tabService.activatedTab()?.id || Date.now(),
|
||||||
'remission',
|
'remission',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
constructor(private _router: Router) {}
|
constructor(private _router: Router) {}
|
||||||
|
|
||||||
itemSelected(item: MessageBoardItemDTO) {
|
itemSelected(item: MessageBoardItemDTO) {
|
||||||
const defaultNav = this._pickupShelfInNavigationService.listRoute();
|
const defaultNav = this._pickupShelfInNavigationService.listRoute();
|
||||||
const queryParams = UiFilter.getQueryParamsFromQueryTokenDTO(
|
const queryParams = UiFilter.getQueryParamsFromQueryTokenDTO(
|
||||||
item.queryToken,
|
item.queryToken,
|
||||||
);
|
);
|
||||||
this._router.navigate(defaultNav.path, {
|
this._router.navigate(defaultNav.path, {
|
||||||
queryParams: {
|
queryParams: {
|
||||||
...defaultNav.queryParams,
|
...defaultNav.queryParams,
|
||||||
...queryParams,
|
...queryParams,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
this.navigated.emit();
|
this.navigated.emit();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<ng-container *ifRole="'Store'">
|
<!-- <ng-container *ifRole="'Store'">
|
||||||
@if (customerType !== 'b2b') {
|
@if (customerType !== 'b2b') {
|
||||||
<shared-checkbox
|
<shared-checkbox
|
||||||
[ngModel]="p4mUser"
|
[ngModel]="p4mUser"
|
||||||
@@ -8,15 +8,17 @@
|
|||||||
Kundenkarte
|
Kundenkarte
|
||||||
</shared-checkbox>
|
</shared-checkbox>
|
||||||
}
|
}
|
||||||
</ng-container>
|
</ng-container> -->
|
||||||
@for (option of filteredOptions$ | async; track option) {
|
@for (option of filteredOptions$ | async; track option) {
|
||||||
@if (option?.enabled !== false) {
|
@if (option?.enabled !== false) {
|
||||||
<shared-checkbox
|
<shared-checkbox
|
||||||
[ngModel]="option.value === customerType"
|
[ngModel]="option.value === customerType"
|
||||||
(ngModelChange)="setValue({ customerType: $event ? option.value : undefined })"
|
(ngModelChange)="
|
||||||
|
setValue({ customerType: $event ? option.value : undefined })
|
||||||
|
"
|
||||||
[disabled]="isOptionDisabled(option)"
|
[disabled]="isOptionDisabled(option)"
|
||||||
[name]="option.value"
|
[name]="option.value"
|
||||||
>
|
>
|
||||||
{{ option.label }}
|
{{ option.label }}
|
||||||
</shared-checkbox>
|
</shared-checkbox>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,13 @@ import { OptionDTO } from '@generated/swagger/checkout-api';
|
|||||||
import { UiCheckboxComponent } from '@ui/checkbox';
|
import { UiCheckboxComponent } from '@ui/checkbox';
|
||||||
import { first, isBoolean, isString } from 'lodash';
|
import { first, isBoolean, isString } from 'lodash';
|
||||||
import { combineLatest, Observable, Subject } from 'rxjs';
|
import { combineLatest, Observable, Subject } from 'rxjs';
|
||||||
import { distinctUntilChanged, filter, map, shareReplay, switchMap } from 'rxjs/operators';
|
import {
|
||||||
|
distinctUntilChanged,
|
||||||
|
filter,
|
||||||
|
map,
|
||||||
|
shareReplay,
|
||||||
|
switchMap,
|
||||||
|
} from 'rxjs/operators';
|
||||||
|
|
||||||
export interface CustomerTypeSelectorState {
|
export interface CustomerTypeSelectorState {
|
||||||
processId: number;
|
processId: number;
|
||||||
@@ -58,18 +64,18 @@ export class CustomerTypeSelectorComponent
|
|||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
get value() {
|
get value() {
|
||||||
if (this.p4mUser) {
|
// if (this.p4mUser) {
|
||||||
return `${this.customerType}-p4m`;
|
// return `${this.customerType}-p4m`;
|
||||||
}
|
// }
|
||||||
return this.customerType;
|
return this.customerType;
|
||||||
}
|
}
|
||||||
set value(value: string) {
|
set value(value: string) {
|
||||||
if (value.includes('-p4m')) {
|
// if (value.includes('-p4m')) {
|
||||||
this.p4mUser = true;
|
// this.p4mUser = true;
|
||||||
this.customerType = value.replace('-p4m', '');
|
// this.customerType = value.replace('-p4m', '');
|
||||||
} else {
|
// } else {
|
||||||
this.customerType = value;
|
this.customerType = value;
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Output()
|
@Output()
|
||||||
@@ -111,29 +117,36 @@ export class CustomerTypeSelectorComponent
|
|||||||
get filteredOptions$() {
|
get filteredOptions$() {
|
||||||
const options$ = this.select((s) => s.options).pipe(distinctUntilChanged());
|
const options$ = this.select((s) => s.options).pipe(distinctUntilChanged());
|
||||||
const p4mUser$ = this.select((s) => s.p4mUser).pipe(distinctUntilChanged());
|
const p4mUser$ = this.select((s) => s.p4mUser).pipe(distinctUntilChanged());
|
||||||
const customerType$ = this.select((s) => s.customerType).pipe(distinctUntilChanged());
|
const customerType$ = this.select((s) => s.customerType).pipe(
|
||||||
|
distinctUntilChanged(),
|
||||||
|
);
|
||||||
return combineLatest([options$, p4mUser$, customerType$]).pipe(
|
return combineLatest([options$, p4mUser$, customerType$]).pipe(
|
||||||
filter(([options]) => options?.length > 0),
|
filter(([options]) => options?.length > 0),
|
||||||
map(([options, p4mUser, customerType]) => {
|
map(([options, p4mUser, customerType]) => {
|
||||||
const initial = { p4mUser: this.p4mUser, customerType: this.customerType };
|
const initial = {
|
||||||
|
p4mUser: this.p4mUser,
|
||||||
|
customerType: this.customerType,
|
||||||
|
};
|
||||||
let result: OptionDTO[] = options;
|
let result: OptionDTO[] = options;
|
||||||
if (p4mUser) {
|
// if (p4mUser) {
|
||||||
result = result.filter((o) => o.value === 'store' || (o.value === 'webshop' && o.enabled !== false));
|
// result = result.filter((o) => o.value === 'store' || (o.value === 'webshop' && o.enabled !== false));
|
||||||
|
|
||||||
result = result.map((o) => {
|
// result = result.map((o) => {
|
||||||
if (o.value === 'store') {
|
// if (o.value === 'store') {
|
||||||
return { ...o, enabled: false };
|
// return { ...o, enabled: false };
|
||||||
}
|
// }
|
||||||
return o;
|
// return o;
|
||||||
});
|
// });
|
||||||
}
|
// }
|
||||||
|
|
||||||
if (customerType === 'b2b' && this.p4mUser) {
|
if (customerType === 'b2b' && this.p4mUser) {
|
||||||
this.p4mUser = false;
|
this.p4mUser = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (initial.p4mUser !== this.p4mUser || initial.customerType !== this.customerType) {
|
if (initial.customerType !== this.customerType) {
|
||||||
this.setValue({ customerType: this.customerType, p4mUser: this.p4mUser });
|
// if (initial.p4mUser !== this.p4mUser || initial.customerType !== this.customerType) {
|
||||||
|
// this.setValue({ customerType: this.customerType, p4mUser: this.p4mUser });
|
||||||
|
this.setValue({ customerType: this.customerType });
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
@@ -224,42 +237,51 @@ export class CustomerTypeSelectorComponent
|
|||||||
if (typeof value === 'string') {
|
if (typeof value === 'string') {
|
||||||
this.value = value;
|
this.value = value;
|
||||||
} else {
|
} else {
|
||||||
if (isBoolean(value.p4mUser)) {
|
// if (isBoolean(value.p4mUser)) {
|
||||||
this.p4mUser = value.p4mUser;
|
// this.p4mUser = value.p4mUser;
|
||||||
}
|
// }
|
||||||
if (isString(value.customerType)) {
|
if (isString(value.customerType)) {
|
||||||
this.customerType = value.customerType;
|
this.customerType = value.customerType;
|
||||||
} else if (this.p4mUser) {
|
}
|
||||||
// Implementierung wie im PBI #3467 beschrieben
|
// else if (this.p4mUser) {
|
||||||
// wenn customerType nicht gesetzt wird und p4mUser true ist,
|
// // Implementierung wie im PBI #3467 beschrieben
|
||||||
// dann customerType auf store setzen.
|
// // wenn customerType nicht gesetzt wird und p4mUser true ist,
|
||||||
// wenn dies nicht möglich ist da der Warenkob keinen store Kunden zulässt,
|
// // dann customerType auf store setzen.
|
||||||
// dann customerType auf webshop setzen.
|
// // wenn dies nicht möglich ist da der Warenkob keinen store Kunden zulässt,
|
||||||
// wenn dies nicht möglich ist da der Warenkob keinen webshop Kunden zulässt,
|
// // dann customerType auf webshop setzen.
|
||||||
// dann customerType auf den ersten verfügbaren setzen und p4mUser auf false setzen.
|
// // wenn dies nicht möglich ist da der Warenkob keinen webshop Kunden zulässt,
|
||||||
if (this.enabledOptions.some((o) => o.value === 'store')) {
|
// // dann customerType auf den ersten verfügbaren setzen und p4mUser auf false setzen.
|
||||||
this.customerType = 'store';
|
// if (this.enabledOptions.some((o) => o.value === 'store')) {
|
||||||
} else if (this.enabledOptions.some((o) => o.value === 'webshop')) {
|
// this.customerType = 'store';
|
||||||
this.customerType = 'webshop';
|
// } else if (this.enabledOptions.some((o) => o.value === 'webshop')) {
|
||||||
} else {
|
// this.customerType = 'webshop';
|
||||||
this.p4mUser = false;
|
// } else {
|
||||||
const includesGuest = this.enabledOptions.some((o) => o.value === 'guest');
|
// this.p4mUser = false;
|
||||||
this.customerType = includesGuest ? 'guest' : first(this.enabledOptions)?.value;
|
// const includesGuest = this.enabledOptions.some((o) => o.value === 'guest');
|
||||||
}
|
// this.customerType = includesGuest ? 'guest' : first(this.enabledOptions)?.value;
|
||||||
} else {
|
// }
|
||||||
|
// }
|
||||||
|
else {
|
||||||
// wenn customerType nicht gesetzt wird und p4mUser false ist,
|
// wenn customerType nicht gesetzt wird und p4mUser false ist,
|
||||||
// dann customerType auf den ersten verfügbaren setzen der nicht mit dem aktuellen customerType übereinstimmt.
|
// dann customerType auf den ersten verfügbaren setzen der nicht mit dem aktuellen customerType übereinstimmt.
|
||||||
this.customerType =
|
this.customerType =
|
||||||
first(this.enabledOptions.filter((o) => o.value === this.customerType))?.value ?? this.customerType;
|
first(
|
||||||
|
this.enabledOptions.filter((o) => o.value === this.customerType),
|
||||||
|
)?.value ?? this.customerType;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.customerType !== initial.customerType || this.p4mUser !== initial.p4mUser) {
|
if (
|
||||||
|
this.customerType !== initial.customerType ||
|
||||||
|
this.p4mUser !== initial.p4mUser
|
||||||
|
) {
|
||||||
this.onChange(this.value);
|
this.onChange(this.value);
|
||||||
this.onTouched();
|
this.onTouched();
|
||||||
this.valueChanges.emit(this.value);
|
this.valueChanges.emit(this.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.checkboxes?.find((c) => c.name === this.customerType)?.writeValue(true);
|
this.checkboxes
|
||||||
|
?.find((c) => c.name === this.customerType)
|
||||||
|
?.writeValue(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,6 @@ export * from './interests';
|
|||||||
export * from './name';
|
export * from './name';
|
||||||
export * from './newsletter';
|
export * from './newsletter';
|
||||||
export * from './organisation';
|
export * from './organisation';
|
||||||
export * from './p4m-number';
|
// export * from './p4m-number';
|
||||||
export * from './phone-numbers';
|
export * from './phone-numbers';
|
||||||
export * from './form-block';
|
export * from './form-block';
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// start:ng42.barrel
|
// // start:ng42.barrel
|
||||||
export * from './p4m-number-form-block.component';
|
// export * from './p4m-number-form-block.component';
|
||||||
export * from './p4m-number-form-block.module';
|
// export * from './p4m-number-form-block.module';
|
||||||
// end:ng42.barrel
|
// // end:ng42.barrel
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<shared-form-control label="Kundenkartencode" class="flex-grow">
|
<!-- <shared-form-control label="Kundenkartencode" class="flex-grow">
|
||||||
<input
|
<input
|
||||||
placeholder="Kundenkartencode"
|
placeholder="Kundenkartencode"
|
||||||
class="input-control"
|
class="input-control"
|
||||||
@@ -13,4 +13,4 @@
|
|||||||
<button type="button" (click)="scan()">
|
<button type="button" (click)="scan()">
|
||||||
<shared-icon icon="barcode-scan" [size]="32"></shared-icon>
|
<shared-icon icon="barcode-scan" [size]="32"></shared-icon>
|
||||||
</button>
|
</button>
|
||||||
}
|
} -->
|
||||||
|
|||||||
@@ -1,49 +1,49 @@
|
|||||||
import { Component, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
|
// import { Component, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
|
||||||
import { UntypedFormControl, Validators } from '@angular/forms';
|
// import { UntypedFormControl, Validators } from '@angular/forms';
|
||||||
import { FormBlockControl } from '../form-block';
|
// import { FormBlockControl } from '../form-block';
|
||||||
import { ScanAdapterService } from '@adapter/scan';
|
// import { ScanAdapterService } from '@adapter/scan';
|
||||||
|
|
||||||
@Component({
|
// @Component({
|
||||||
selector: 'app-p4m-number-form-block',
|
// selector: 'app-p4m-number-form-block',
|
||||||
templateUrl: 'p4m-number-form-block.component.html',
|
// templateUrl: 'p4m-number-form-block.component.html',
|
||||||
styleUrls: ['p4m-number-form-block.component.scss'],
|
// styleUrls: ['p4m-number-form-block.component.scss'],
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
// changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
standalone: false,
|
// standalone: false,
|
||||||
})
|
// })
|
||||||
export class P4mNumberFormBlockComponent extends FormBlockControl<string> {
|
// export class P4mNumberFormBlockComponent extends FormBlockControl<string> {
|
||||||
get tabIndexEnd() {
|
// get tabIndexEnd() {
|
||||||
return this.tabIndexStart;
|
// return this.tabIndexStart;
|
||||||
}
|
// }
|
||||||
|
|
||||||
constructor(
|
// constructor(
|
||||||
private scanAdapter: ScanAdapterService,
|
// private scanAdapter: ScanAdapterService,
|
||||||
private changeDetectorRef: ChangeDetectorRef,
|
// private changeDetectorRef: ChangeDetectorRef,
|
||||||
) {
|
// ) {
|
||||||
super();
|
// super();
|
||||||
}
|
// }
|
||||||
|
|
||||||
updateValidators(): void {
|
// updateValidators(): void {
|
||||||
this.control.setValidators([...this.getValidatorFn()]);
|
// this.control.setValidators([...this.getValidatorFn()]);
|
||||||
this.control.setAsyncValidators(this.getAsyncValidatorFn());
|
// this.control.setAsyncValidators(this.getAsyncValidatorFn());
|
||||||
this.control.updateValueAndValidity();
|
// this.control.updateValueAndValidity();
|
||||||
}
|
// }
|
||||||
|
|
||||||
initializeControl(data?: string): void {
|
// initializeControl(data?: string): void {
|
||||||
this.control = new UntypedFormControl(data ?? '', [Validators.required], this.getAsyncValidatorFn());
|
// this.control = new UntypedFormControl(data ?? '', [Validators.required], this.getAsyncValidatorFn());
|
||||||
}
|
// }
|
||||||
|
|
||||||
_patchValue(update: { previous: string; current: string }): void {
|
// _patchValue(update: { previous: string; current: string }): void {
|
||||||
this.control.patchValue(update.current);
|
// this.control.patchValue(update.current);
|
||||||
}
|
// }
|
||||||
|
|
||||||
scan() {
|
// scan() {
|
||||||
this.scanAdapter.scan().subscribe((result) => {
|
// this.scanAdapter.scan().subscribe((result) => {
|
||||||
this.control.patchValue(result);
|
// this.control.patchValue(result);
|
||||||
this.changeDetectorRef.markForCheck();
|
// this.changeDetectorRef.markForCheck();
|
||||||
});
|
// });
|
||||||
}
|
// }
|
||||||
|
|
||||||
canScan() {
|
// canScan() {
|
||||||
return this.scanAdapter.isReady();
|
// return this.scanAdapter.isReady();
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import { NgModule } from '@angular/core';
|
// import { NgModule } from '@angular/core';
|
||||||
import { CommonModule } from '@angular/common';
|
// import { CommonModule } from '@angular/common';
|
||||||
|
|
||||||
import { P4mNumberFormBlockComponent } from './p4m-number-form-block.component';
|
// import { P4mNumberFormBlockComponent } from './p4m-number-form-block.component';
|
||||||
import { ReactiveFormsModule } from '@angular/forms';
|
// import { ReactiveFormsModule } from '@angular/forms';
|
||||||
import { IconComponent } from '@shared/components/icon';
|
// import { IconComponent } from '@shared/components/icon';
|
||||||
import { FormControlComponent } from '@shared/components/form-control';
|
// import { FormControlComponent } from '@shared/components/form-control';
|
||||||
|
|
||||||
@NgModule({
|
// @NgModule({
|
||||||
imports: [CommonModule, ReactiveFormsModule, FormControlComponent, IconComponent],
|
// imports: [CommonModule, ReactiveFormsModule, FormControlComponent, IconComponent],
|
||||||
exports: [P4mNumberFormBlockComponent],
|
// exports: [P4mNumberFormBlockComponent],
|
||||||
declarations: [P4mNumberFormBlockComponent],
|
// declarations: [P4mNumberFormBlockComponent],
|
||||||
})
|
// })
|
||||||
export class P4mNumberFormBlockModule {}
|
// export class P4mNumberFormBlockModule {}
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
import { HttpErrorResponse } from '@angular/common/http';
|
import { HttpErrorResponse } from '@angular/common/http';
|
||||||
import { ChangeDetectorRef, Directive, OnDestroy, OnInit, ViewChild, inject } from '@angular/core';
|
import {
|
||||||
|
ChangeDetectorRef,
|
||||||
|
Directive,
|
||||||
|
OnDestroy,
|
||||||
|
OnInit,
|
||||||
|
ViewChild,
|
||||||
|
inject,
|
||||||
|
} from '@angular/core';
|
||||||
import {
|
import {
|
||||||
AbstractControl,
|
AbstractControl,
|
||||||
AsyncValidatorFn,
|
AsyncValidatorFn,
|
||||||
@@ -11,7 +18,12 @@ import {
|
|||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { BreadcrumbService } from '@core/breadcrumb';
|
import { BreadcrumbService } from '@core/breadcrumb';
|
||||||
import { CrmCustomerService } from '@domain/crm';
|
import { CrmCustomerService } from '@domain/crm';
|
||||||
import { AddressDTO, CustomerDTO, PayerDTO, ShippingAddressDTO } from '@generated/swagger/crm-api';
|
import {
|
||||||
|
AddressDTO,
|
||||||
|
CustomerDTO,
|
||||||
|
PayerDTO,
|
||||||
|
ShippingAddressDTO,
|
||||||
|
} from '@generated/swagger/crm-api';
|
||||||
import { UiErrorModalComponent, UiModalService } from '@ui/modal';
|
import { UiErrorModalComponent, UiModalService } from '@ui/modal';
|
||||||
import { UiValidators } from '@ui/validators';
|
import { UiValidators } from '@ui/validators';
|
||||||
import { isNull } from 'lodash';
|
import { isNull } from 'lodash';
|
||||||
@@ -42,7 +54,10 @@ import {
|
|||||||
mapCustomerInfoDtoToCustomerCreateFormData,
|
mapCustomerInfoDtoToCustomerCreateFormData,
|
||||||
} from './customer-create-form-data';
|
} from './customer-create-form-data';
|
||||||
import { AddressSelectionModalService } from '../modals';
|
import { AddressSelectionModalService } from '../modals';
|
||||||
import { CustomerCreateNavigation, CustomerSearchNavigation } from '@shared/services/navigation';
|
import {
|
||||||
|
CustomerCreateNavigation,
|
||||||
|
CustomerSearchNavigation,
|
||||||
|
} from '@shared/services/navigation';
|
||||||
|
|
||||||
@Directive()
|
@Directive()
|
||||||
export abstract class AbstractCreateCustomer implements OnInit, OnDestroy {
|
export abstract class AbstractCreateCustomer implements OnInit, OnDestroy {
|
||||||
@@ -104,7 +119,12 @@ export abstract class AbstractCreateCustomer implements OnInit, OnDestroy {
|
|||||||
);
|
);
|
||||||
|
|
||||||
this.processId$
|
this.processId$
|
||||||
.pipe(startWith(undefined), bufferCount(2, 1), takeUntil(this.onDestroy$), delay(100))
|
.pipe(
|
||||||
|
startWith(undefined),
|
||||||
|
bufferCount(2, 1),
|
||||||
|
takeUntil(this.onDestroy$),
|
||||||
|
delay(100),
|
||||||
|
)
|
||||||
.subscribe(async ([previous, current]) => {
|
.subscribe(async ([previous, current]) => {
|
||||||
if (previous === undefined) {
|
if (previous === undefined) {
|
||||||
await this._initFormData();
|
await this._initFormData();
|
||||||
@@ -155,7 +175,10 @@ export abstract class AbstractCreateCustomer implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async addOrUpdateBreadcrumb(processId: number, formData: CustomerCreateFormData) {
|
async addOrUpdateBreadcrumb(
|
||||||
|
processId: number,
|
||||||
|
formData: CustomerCreateFormData,
|
||||||
|
) {
|
||||||
await this.breadcrumb.addOrUpdateBreadcrumbIfNotExists({
|
await this.breadcrumb.addOrUpdateBreadcrumbIfNotExists({
|
||||||
key: processId,
|
key: processId,
|
||||||
name: 'Kundendaten erfassen',
|
name: 'Kundendaten erfassen',
|
||||||
@@ -195,7 +218,10 @@ export abstract class AbstractCreateCustomer implements OnInit, OnDestroy {
|
|||||||
console.log('customerTypeChanged', customerType);
|
console.log('customerTypeChanged', customerType);
|
||||||
}
|
}
|
||||||
|
|
||||||
addFormBlock(key: keyof CustomerCreateFormData, block: FormBlock<any, AbstractControl>) {
|
addFormBlock(
|
||||||
|
key: keyof CustomerCreateFormData,
|
||||||
|
block: FormBlock<any, AbstractControl>,
|
||||||
|
) {
|
||||||
this.form.addControl(key, block.control);
|
this.form.addControl(key, block.control);
|
||||||
this.cdr.markForCheck();
|
this.cdr.markForCheck();
|
||||||
}
|
}
|
||||||
@@ -232,7 +258,10 @@ export abstract class AbstractCreateCustomer implements OnInit, OnDestroy {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// Check Year + Month
|
// Check Year + Month
|
||||||
else if (inputDate.getFullYear() === minBirthDate.getFullYear() && inputDate.getMonth() < minBirthDate.getMonth()) {
|
else if (
|
||||||
|
inputDate.getFullYear() === minBirthDate.getFullYear() &&
|
||||||
|
inputDate.getMonth() < minBirthDate.getMonth()
|
||||||
|
) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// Check Year + Month + Day
|
// Check Year + Month + Day
|
||||||
@@ -279,70 +308,80 @@ export abstract class AbstractCreateCustomer implements OnInit, OnDestroy {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
checkLoyalityCardValidator: AsyncValidatorFn = (control) => {
|
// checkLoyalityCardValidator: AsyncValidatorFn = (control) => {
|
||||||
return of(control.value).pipe(
|
// return of(control.value).pipe(
|
||||||
delay(500),
|
// delay(500),
|
||||||
mergeMap((value) => {
|
// mergeMap((value) => {
|
||||||
const customerId = this.formData?._meta?.customerDto?.id ?? this.formData?._meta?.customerInfoDto?.id;
|
// const customerId = this.formData?._meta?.customerDto?.id ?? this.formData?._meta?.customerInfoDto?.id;
|
||||||
return this.customerService.checkLoyaltyCard({ loyaltyCardNumber: value, customerId }).pipe(
|
// return this.customerService.checkLoyaltyCard({ loyaltyCardNumber: value, customerId }).pipe(
|
||||||
map((response) => {
|
// map((response) => {
|
||||||
if (response.error) {
|
// if (response.error) {
|
||||||
throw response.message;
|
// throw response.message;
|
||||||
}
|
// }
|
||||||
|
|
||||||
/**
|
// /**
|
||||||
* #4485 Kubi // Verhalten mit angelegte aber nicht verknüpfte Kundenkartencode in Kundensuche und Kundendaten erfassen ist nicht gleich
|
// * #4485 Kubi // Verhalten mit angelegte aber nicht verknüpfte Kundenkartencode in Kundensuche und Kundendaten erfassen ist nicht gleich
|
||||||
* Fall1: Kundenkarte hat Daten in point4more:
|
// * Fall1: Kundenkarte hat Daten in point4more:
|
||||||
* Sobald Kundenkartencode in Feld "Kundenkartencode" reingegeben wird- werden die Daten von point4more in Formular "Kundendaten Erfassen" eingefügt und ersetzen (im Ganzen, nicht inkremental) die Daten in Felder, falls welche schon reingetippt werden.
|
// * Sobald Kundenkartencode in Feld "Kundenkartencode" reingegeben wird- werden die Daten von point4more in Formular "Kundendaten Erfassen" eingefügt und ersetzen (im Ganzen, nicht inkremental) die Daten in Felder, falls welche schon reingetippt werden.
|
||||||
* Fall2: Kundenkarte hat keine Daten in point4more:
|
// * Fall2: Kundenkarte hat keine Daten in point4more:
|
||||||
* Sobald Kundenkartencode in Feld "Kundenkartencode" reingegeben wird- bleiben die Daten in Formular "Kundendaten Erfassen" in Felder, falls welche schon reingetippt werden.
|
// * Sobald Kundenkartencode in Feld "Kundenkartencode" reingegeben wird- bleiben die Daten in Formular "Kundendaten Erfassen" in Felder, falls welche schon reingetippt werden.
|
||||||
*/
|
// */
|
||||||
if (response.result && response.result.customer) {
|
// if (response.result && response.result.customer) {
|
||||||
const customer = response.result.customer;
|
// const customer = response.result.customer;
|
||||||
const data = mapCustomerInfoDtoToCustomerCreateFormData(customer);
|
// const data = mapCustomerInfoDtoToCustomerCreateFormData(customer);
|
||||||
|
|
||||||
if (data.name.firstName && data.name.lastName) {
|
// if (data.name.firstName && data.name.lastName) {
|
||||||
// Fall1
|
// // Fall1
|
||||||
this._formData.next(data);
|
// this._formData.next(data);
|
||||||
} else {
|
// } else {
|
||||||
// Fall2 Hier müssen die Metadaten gesetzt werden um eine verknüfung zur kundenkarte zu ermöglichen.
|
// // Fall2 Hier müssen die Metadaten gesetzt werden um eine verknüfung zur kundenkarte zu ermöglichen.
|
||||||
const current = this.formData;
|
// const current = this.formData;
|
||||||
current._meta = data._meta;
|
// current._meta = data._meta;
|
||||||
current.p4m = data.p4m;
|
// current.p4m = data.p4m;
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
return null;
|
// return null;
|
||||||
}),
|
// }),
|
||||||
catchError((error) => {
|
// catchError((error) => {
|
||||||
if (error instanceof HttpErrorResponse) {
|
// if (error instanceof HttpErrorResponse) {
|
||||||
if (error?.error?.invalidProperties?.loyaltyCardNumber) {
|
// if (error?.error?.invalidProperties?.loyaltyCardNumber) {
|
||||||
return of({ invalid: error.error.invalidProperties.loyaltyCardNumber });
|
// return of({ invalid: error.error.invalidProperties.loyaltyCardNumber });
|
||||||
} else {
|
// } else {
|
||||||
return of({ invalid: 'Kundenkartencode ist ungültig' });
|
// return of({ invalid: 'Kundenkartencode ist ungültig' });
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}),
|
// }),
|
||||||
);
|
// );
|
||||||
}),
|
// }),
|
||||||
tap(() => {
|
// tap(() => {
|
||||||
control.markAsTouched();
|
// control.markAsTouched();
|
||||||
this.cdr.markForCheck();
|
// this.cdr.markForCheck();
|
||||||
}),
|
// }),
|
||||||
);
|
// );
|
||||||
};
|
// };
|
||||||
|
|
||||||
async navigateToCustomerDetails(customer: CustomerDTO) {
|
async navigateToCustomerDetails(customer: CustomerDTO) {
|
||||||
const processId = await this.processId$.pipe(first()).toPromise();
|
const processId = await this.processId$.pipe(first()).toPromise();
|
||||||
const route = this.customerSearchNavigation.detailsRoute({ processId, customerId: customer.id, customer });
|
const route = this.customerSearchNavigation.detailsRoute({
|
||||||
|
processId,
|
||||||
|
customerId: customer.id,
|
||||||
|
customer,
|
||||||
|
});
|
||||||
|
|
||||||
return this.router.navigate(route.path, { queryParams: route.urlTree.queryParams });
|
return this.router.navigate(route.path, {
|
||||||
|
queryParams: route.urlTree.queryParams,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async validateAddressData(address: AddressDTO): Promise<AddressDTO> {
|
async validateAddressData(address: AddressDTO): Promise<AddressDTO> {
|
||||||
const addressValidationResult = await this.addressVlidationModal.validateAddress(address);
|
const addressValidationResult =
|
||||||
|
await this.addressVlidationModal.validateAddress(address);
|
||||||
|
|
||||||
if (addressValidationResult !== undefined && (addressValidationResult as any) !== 'continue') {
|
if (
|
||||||
|
addressValidationResult !== undefined &&
|
||||||
|
(addressValidationResult as any) !== 'continue'
|
||||||
|
) {
|
||||||
address = addressValidationResult;
|
address = addressValidationResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -389,7 +428,9 @@ export abstract class AbstractCreateCustomer implements OnInit, OnDestroy {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.form.enable();
|
this.form.enable();
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.addressFormBlock.setAddressValidationError(error.error.invalidProperties);
|
this.addressFormBlock.setAddressValidationError(
|
||||||
|
error.error.invalidProperties,
|
||||||
|
);
|
||||||
}, 10);
|
}, 10);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
@@ -397,7 +438,10 @@ export abstract class AbstractCreateCustomer implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.birthDate && isNull(UiValidators.date(new UntypedFormControl(data.birthDate)))) {
|
if (
|
||||||
|
data.birthDate &&
|
||||||
|
isNull(UiValidators.date(new UntypedFormControl(data.birthDate)))
|
||||||
|
) {
|
||||||
customer.dateOfBirth = data.birthDate;
|
customer.dateOfBirth = data.birthDate;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -406,11 +450,15 @@ export abstract class AbstractCreateCustomer implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
if (this.validateShippingAddress) {
|
if (this.validateShippingAddress) {
|
||||||
try {
|
try {
|
||||||
billingAddress.address = await this.validateAddressData(billingAddress.address);
|
billingAddress.address = await this.validateAddressData(
|
||||||
|
billingAddress.address,
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.form.enable();
|
this.form.enable();
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.addressFormBlock.setAddressValidationError(error.error.invalidProperties);
|
this.addressFormBlock.setAddressValidationError(
|
||||||
|
error.error.invalidProperties,
|
||||||
|
);
|
||||||
}, 10);
|
}, 10);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
@@ -426,15 +474,21 @@ export abstract class AbstractCreateCustomer implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (data.deviatingDeliveryAddress?.deviatingAddress) {
|
if (data.deviatingDeliveryAddress?.deviatingAddress) {
|
||||||
const shippingAddress = this.mapToShippingAddress(data.deviatingDeliveryAddress);
|
const shippingAddress = this.mapToShippingAddress(
|
||||||
|
data.deviatingDeliveryAddress,
|
||||||
|
);
|
||||||
|
|
||||||
if (this.validateShippingAddress) {
|
if (this.validateShippingAddress) {
|
||||||
try {
|
try {
|
||||||
shippingAddress.address = await this.validateAddressData(shippingAddress.address);
|
shippingAddress.address = await this.validateAddressData(
|
||||||
|
shippingAddress.address,
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.form.enable();
|
this.form.enable();
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.deviatingDeliveryAddressFormBlock.setAddressValidationError(error.error.invalidProperties);
|
this.deviatingDeliveryAddressFormBlock.setAddressValidationError(
|
||||||
|
error.error.invalidProperties,
|
||||||
|
);
|
||||||
}, 10);
|
}, 10);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
@@ -474,7 +528,13 @@ export abstract class AbstractCreateCustomer implements OnInit, OnDestroy {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
mapToBillingAddress({ name, address, email, organisation, phoneNumbers }: DeviatingAddressFormBlockData): PayerDTO {
|
mapToBillingAddress({
|
||||||
|
name,
|
||||||
|
address,
|
||||||
|
email,
|
||||||
|
organisation,
|
||||||
|
phoneNumbers,
|
||||||
|
}: DeviatingAddressFormBlockData): PayerDTO {
|
||||||
return {
|
return {
|
||||||
gender: name?.gender,
|
gender: name?.gender,
|
||||||
title: name?.title,
|
title: name?.title,
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { CreateB2BCustomerModule } from './create-b2b-customer/create-b2b-customer.module';
|
import { CreateB2BCustomerModule } from './create-b2b-customer/create-b2b-customer.module';
|
||||||
import { CreateGuestCustomerModule } from './create-guest-customer';
|
import { CreateGuestCustomerModule } from './create-guest-customer';
|
||||||
import { CreateP4MCustomerModule } from './create-p4m-customer';
|
// import { CreateP4MCustomerModule } from './create-p4m-customer';
|
||||||
import { CreateStoreCustomerModule } from './create-store-customer/create-store-customer.module';
|
import { CreateStoreCustomerModule } from './create-store-customer/create-store-customer.module';
|
||||||
import { CreateWebshopCustomerModule } from './create-webshop-customer/create-webshop-customer.module';
|
import { CreateWebshopCustomerModule } from './create-webshop-customer/create-webshop-customer.module';
|
||||||
import { UpdateP4MWebshopCustomerModule } from './update-p4m-webshop-customer';
|
// import { UpdateP4MWebshopCustomerModule } from './update-p4m-webshop-customer';
|
||||||
import { CreateCustomerComponent } from './create-customer.component';
|
import { CreateCustomerComponent } from './create-customer.component';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
@@ -13,8 +13,8 @@ import { CreateCustomerComponent } from './create-customer.component';
|
|||||||
CreateGuestCustomerModule,
|
CreateGuestCustomerModule,
|
||||||
CreateStoreCustomerModule,
|
CreateStoreCustomerModule,
|
||||||
CreateWebshopCustomerModule,
|
CreateWebshopCustomerModule,
|
||||||
CreateP4MCustomerModule,
|
// CreateP4MCustomerModule,
|
||||||
UpdateP4MWebshopCustomerModule,
|
// UpdateP4MWebshopCustomerModule,
|
||||||
CreateCustomerComponent,
|
CreateCustomerComponent,
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
@@ -22,8 +22,8 @@ import { CreateCustomerComponent } from './create-customer.component';
|
|||||||
CreateGuestCustomerModule,
|
CreateGuestCustomerModule,
|
||||||
CreateStoreCustomerModule,
|
CreateStoreCustomerModule,
|
||||||
CreateWebshopCustomerModule,
|
CreateWebshopCustomerModule,
|
||||||
CreateP4MCustomerModule,
|
// CreateP4MCustomerModule,
|
||||||
UpdateP4MWebshopCustomerModule,
|
// UpdateP4MWebshopCustomerModule,
|
||||||
CreateCustomerComponent,
|
CreateCustomerComponent,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
@if (formData$ | async; as data) {
|
<!-- @if (formData$ | async; as data) {
|
||||||
<form (keydown.enter)="$event.preventDefault()">
|
<form (keydown.enter)="$event.preventDefault()">
|
||||||
<h1 class="title flex flex-row items-center justify-center">
|
<h1 class="title flex flex-row items-center justify-center">
|
||||||
Kundendaten erfassen
|
Kundendaten erfassen -->
|
||||||
<!-- <span
|
<!-- <span
|
||||||
class="rounded-full ml-4 h-8 w-8 text-xl text-center border-2 border-solid border-brand text-brand">i</span> -->
|
class="rounded-full ml-4 h-8 w-8 text-xl text-center border-2 border-solid border-brand text-brand">i</span> -->
|
||||||
</h1>
|
<!-- </h1>
|
||||||
<p class="description">
|
<p class="description">
|
||||||
Um Sie als Kunde beim nächsten
|
Um Sie als Kunde beim nächsten
|
||||||
<br />
|
<br />
|
||||||
@@ -135,4 +135,4 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
}
|
} -->
|
||||||
|
|||||||
@@ -1,292 +1,292 @@
|
|||||||
import { Component, ChangeDetectionStrategy, ViewChild, OnInit } from '@angular/core';
|
// import { Component, ChangeDetectionStrategy, ViewChild, OnInit } from '@angular/core';
|
||||||
import { AsyncValidatorFn, ValidatorFn, Validators } from '@angular/forms';
|
// import { AsyncValidatorFn, ValidatorFn, Validators } from '@angular/forms';
|
||||||
import { Result } from '@domain/defs';
|
// import { Result } from '@domain/defs';
|
||||||
import { CustomerDTO, CustomerInfoDTO, KeyValueDTOOfStringAndString } from '@generated/swagger/crm-api';
|
// import { CustomerDTO, CustomerInfoDTO, KeyValueDTOOfStringAndString } from '@generated/swagger/crm-api';
|
||||||
import { UiErrorModalComponent, UiModalResult } from '@ui/modal';
|
// import { UiErrorModalComponent, UiModalResult } from '@ui/modal';
|
||||||
import { NEVER, Observable, of } from 'rxjs';
|
// import { NEVER, Observable, of } from 'rxjs';
|
||||||
import { catchError, distinctUntilChanged, first, map, switchMap, takeUntil, withLatestFrom } from 'rxjs/operators';
|
// import { catchError, distinctUntilChanged, first, map, switchMap, takeUntil, withLatestFrom } from 'rxjs/operators';
|
||||||
import {
|
// import {
|
||||||
AddressFormBlockComponent,
|
// AddressFormBlockComponent,
|
||||||
AddressFormBlockData,
|
// AddressFormBlockData,
|
||||||
DeviatingAddressFormBlockComponent,
|
// DeviatingAddressFormBlockComponent,
|
||||||
} from '../../components/form-blocks';
|
// } from '../../components/form-blocks';
|
||||||
import { NameFormBlockData } from '../../components/form-blocks/name/name-form-block-data';
|
// import { NameFormBlockData } from '../../components/form-blocks/name/name-form-block-data';
|
||||||
import { WebshopCustomnerAlreadyExistsModalComponent, WebshopCustomnerAlreadyExistsModalData } from '../../modals';
|
// import { WebshopCustomnerAlreadyExistsModalComponent, WebshopCustomnerAlreadyExistsModalData } from '../../modals';
|
||||||
import { validateEmail } from '../../validators/email-validator';
|
// import { validateEmail } from '../../validators/email-validator';
|
||||||
import { AbstractCreateCustomer } from '../abstract-create-customer';
|
// import { AbstractCreateCustomer } from '../abstract-create-customer';
|
||||||
import { encodeFormData, mapCustomerDtoToCustomerCreateFormData } from '../customer-create-form-data';
|
// import { encodeFormData, mapCustomerDtoToCustomerCreateFormData } from '../customer-create-form-data';
|
||||||
import { zipCodeValidator } from '../../validators/zip-code-validator';
|
// import { zipCodeValidator } from '../../validators/zip-code-validator';
|
||||||
|
|
||||||
@Component({
|
// @Component({
|
||||||
selector: 'app-create-p4m-customer',
|
// selector: 'app-create-p4m-customer',
|
||||||
templateUrl: 'create-p4m-customer.component.html',
|
// templateUrl: 'create-p4m-customer.component.html',
|
||||||
styleUrls: ['../create-customer.scss', 'create-p4m-customer.component.scss'],
|
// styleUrls: ['../create-customer.scss', 'create-p4m-customer.component.scss'],
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
// changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
standalone: false,
|
// standalone: false,
|
||||||
})
|
// })
|
||||||
export class CreateP4MCustomerComponent extends AbstractCreateCustomer implements OnInit {
|
// export class CreateP4MCustomerComponent extends AbstractCreateCustomer implements OnInit {
|
||||||
validateAddress = true;
|
// validateAddress = true;
|
||||||
|
|
||||||
validateShippingAddress = true;
|
// validateShippingAddress = true;
|
||||||
|
|
||||||
get _customerType() {
|
// get _customerType() {
|
||||||
return this.activatedRoute.snapshot.data.customerType;
|
// return this.activatedRoute.snapshot.data.customerType;
|
||||||
}
|
// }
|
||||||
|
|
||||||
get customerType() {
|
// get customerType() {
|
||||||
return `${this._customerType}-p4m`;
|
// return `${this._customerType}-p4m`;
|
||||||
}
|
// }
|
||||||
|
|
||||||
nameRequiredMarks: (keyof NameFormBlockData)[] = ['gender', 'firstName', 'lastName'];
|
// nameRequiredMarks: (keyof NameFormBlockData)[] = ['gender', 'firstName', 'lastName'];
|
||||||
|
|
||||||
nameValidationFns: Record<keyof NameFormBlockData, ValidatorFn[]> = {
|
// nameValidationFns: Record<keyof NameFormBlockData, ValidatorFn[]> = {
|
||||||
firstName: [Validators.required],
|
// firstName: [Validators.required],
|
||||||
lastName: [Validators.required],
|
// lastName: [Validators.required],
|
||||||
gender: [Validators.required],
|
// gender: [Validators.required],
|
||||||
title: [],
|
// title: [],
|
||||||
};
|
// };
|
||||||
|
|
||||||
emailRequiredMark: boolean;
|
// emailRequiredMark: boolean;
|
||||||
|
|
||||||
emailValidatorFn: ValidatorFn[];
|
// emailValidatorFn: ValidatorFn[];
|
||||||
|
|
||||||
asyncEmailVlaidtorFn: AsyncValidatorFn[];
|
// asyncEmailVlaidtorFn: AsyncValidatorFn[];
|
||||||
|
|
||||||
asyncLoyaltyCardValidatorFn: AsyncValidatorFn[];
|
// asyncLoyaltyCardValidatorFn: AsyncValidatorFn[];
|
||||||
|
|
||||||
shippingAddressRequiredMarks: (keyof AddressFormBlockData)[] = [
|
// shippingAddressRequiredMarks: (keyof AddressFormBlockData)[] = [
|
||||||
'street',
|
// 'street',
|
||||||
'streetNumber',
|
// 'streetNumber',
|
||||||
'zipCode',
|
// 'zipCode',
|
||||||
'city',
|
// 'city',
|
||||||
'country',
|
// 'country',
|
||||||
];
|
// ];
|
||||||
|
|
||||||
shippingAddressValidators: Record<string, ValidatorFn[]> = {
|
// shippingAddressValidators: Record<string, ValidatorFn[]> = {
|
||||||
street: [Validators.required],
|
// street: [Validators.required],
|
||||||
streetNumber: [Validators.required],
|
// streetNumber: [Validators.required],
|
||||||
zipCode: [Validators.required, zipCodeValidator()],
|
// zipCode: [Validators.required, zipCodeValidator()],
|
||||||
city: [Validators.required],
|
// city: [Validators.required],
|
||||||
country: [Validators.required],
|
// country: [Validators.required],
|
||||||
};
|
// };
|
||||||
|
|
||||||
addressRequiredMarks: (keyof AddressFormBlockData)[];
|
// addressRequiredMarks: (keyof AddressFormBlockData)[];
|
||||||
|
|
||||||
addressValidatorFns: Record<string, ValidatorFn[]>;
|
// addressValidatorFns: Record<string, ValidatorFn[]>;
|
||||||
|
|
||||||
@ViewChild(AddressFormBlockComponent, { static: false })
|
// @ViewChild(AddressFormBlockComponent, { static: false })
|
||||||
addressFormBlock: AddressFormBlockComponent;
|
// addressFormBlock: AddressFormBlockComponent;
|
||||||
|
|
||||||
@ViewChild(DeviatingAddressFormBlockComponent, { static: false })
|
// @ViewChild(DeviatingAddressFormBlockComponent, { static: false })
|
||||||
deviatingDeliveryAddressFormBlock: DeviatingAddressFormBlockComponent;
|
// deviatingDeliveryAddressFormBlock: DeviatingAddressFormBlockComponent;
|
||||||
|
|
||||||
agbValidatorFns = [Validators.requiredTrue];
|
// agbValidatorFns = [Validators.requiredTrue];
|
||||||
|
|
||||||
birthDateValidatorFns = [];
|
// birthDateValidatorFns = [];
|
||||||
|
|
||||||
existingCustomer$: Observable<CustomerInfoDTO | CustomerDTO | null>;
|
// existingCustomer$: Observable<CustomerInfoDTO | CustomerDTO | null>;
|
||||||
|
|
||||||
ngOnInit(): void {
|
// ngOnInit(): void {
|
||||||
super.ngOnInit();
|
// super.ngOnInit();
|
||||||
this.initMarksAndValidators();
|
// this.initMarksAndValidators();
|
||||||
this.existingCustomer$ = this.customerExists$.pipe(
|
// this.existingCustomer$ = this.customerExists$.pipe(
|
||||||
distinctUntilChanged(),
|
// distinctUntilChanged(),
|
||||||
switchMap((exists) => {
|
// switchMap((exists) => {
|
||||||
if (exists) {
|
// if (exists) {
|
||||||
return this.fetchCustomerInfo();
|
// return this.fetchCustomerInfo();
|
||||||
}
|
// }
|
||||||
return of(null);
|
// return of(null);
|
||||||
}),
|
// }),
|
||||||
);
|
// );
|
||||||
|
|
||||||
this.existingCustomer$
|
// this.existingCustomer$
|
||||||
.pipe(
|
// .pipe(
|
||||||
takeUntil(this.onDestroy$),
|
// takeUntil(this.onDestroy$),
|
||||||
switchMap((info) => {
|
// switchMap((info) => {
|
||||||
if (info) {
|
// if (info) {
|
||||||
return this.customerService.getCustomer(info.id, 2).pipe(
|
// return this.customerService.getCustomer(info.id, 2).pipe(
|
||||||
map((res) => res.result),
|
// map((res) => res.result),
|
||||||
catchError((err) => NEVER),
|
// catchError((err) => NEVER),
|
||||||
);
|
// );
|
||||||
}
|
// }
|
||||||
return NEVER;
|
// return NEVER;
|
||||||
}),
|
// }),
|
||||||
withLatestFrom(this.processId$),
|
// withLatestFrom(this.processId$),
|
||||||
)
|
// )
|
||||||
.subscribe(([customer, processId]) => {
|
// .subscribe(([customer, processId]) => {
|
||||||
if (customer) {
|
// if (customer) {
|
||||||
this.modal
|
// this.modal
|
||||||
.open({
|
// .open({
|
||||||
content: WebshopCustomnerAlreadyExistsModalComponent,
|
// content: WebshopCustomnerAlreadyExistsModalComponent,
|
||||||
data: {
|
// data: {
|
||||||
customer,
|
// customer,
|
||||||
processId,
|
// processId,
|
||||||
} as WebshopCustomnerAlreadyExistsModalData,
|
// } as WebshopCustomnerAlreadyExistsModalData,
|
||||||
title: 'Es existiert bereits ein Onlinekonto mit dieser E-Mail-Adresse',
|
// title: 'Es existiert bereits ein Onlinekonto mit dieser E-Mail-Adresse',
|
||||||
})
|
// })
|
||||||
.afterClosed$.subscribe(async (result: UiModalResult<boolean>) => {
|
// .afterClosed$.subscribe(async (result: UiModalResult<boolean>) => {
|
||||||
if (result.data) {
|
// if (result.data) {
|
||||||
this.navigateToUpdatePage(customer);
|
// this.navigateToUpdatePage(customer);
|
||||||
} else {
|
// } else {
|
||||||
this.formData.email = '';
|
// this.formData.email = '';
|
||||||
this.cdr.markForCheck();
|
// this.cdr.markForCheck();
|
||||||
}
|
// }
|
||||||
});
|
// });
|
||||||
}
|
// }
|
||||||
});
|
// });
|
||||||
}
|
// }
|
||||||
|
|
||||||
async navigateToUpdatePage(customer: CustomerDTO) {
|
// async navigateToUpdatePage(customer: CustomerDTO) {
|
||||||
const processId = await this.processId$.pipe(first()).toPromise();
|
// const processId = await this.processId$.pipe(first()).toPromise();
|
||||||
this.router.navigate(['/kunde', processId, 'customer', 'create', 'webshop-p4m', 'update'], {
|
// this.router.navigate(['/kunde', processId, 'customer', 'create', 'webshop-p4m', 'update'], {
|
||||||
queryParams: {
|
// queryParams: {
|
||||||
formData: encodeFormData({
|
// formData: encodeFormData({
|
||||||
...mapCustomerDtoToCustomerCreateFormData(customer),
|
// ...mapCustomerDtoToCustomerCreateFormData(customer),
|
||||||
p4m: this.formData.p4m,
|
// p4m: this.formData.p4m,
|
||||||
}),
|
// }),
|
||||||
},
|
// },
|
||||||
});
|
// });
|
||||||
}
|
// }
|
||||||
|
|
||||||
initMarksAndValidators() {
|
// initMarksAndValidators() {
|
||||||
this.asyncLoyaltyCardValidatorFn = [this.checkLoyalityCardValidator];
|
// this.asyncLoyaltyCardValidatorFn = [this.checkLoyalityCardValidator];
|
||||||
this.birthDateValidatorFns = [Validators.required, this.minBirthDateValidator()];
|
// this.birthDateValidatorFns = [Validators.required, this.minBirthDateValidator()];
|
||||||
if (this._customerType === 'webshop') {
|
// if (this._customerType === 'webshop') {
|
||||||
this.emailRequiredMark = true;
|
// this.emailRequiredMark = true;
|
||||||
this.emailValidatorFn = [Validators.required, Validators.email, validateEmail];
|
// this.emailValidatorFn = [Validators.required, Validators.email, validateEmail];
|
||||||
this.asyncEmailVlaidtorFn = [this.emailExistsValidator];
|
// this.asyncEmailVlaidtorFn = [this.emailExistsValidator];
|
||||||
this.addressRequiredMarks = this.shippingAddressRequiredMarks;
|
// this.addressRequiredMarks = this.shippingAddressRequiredMarks;
|
||||||
this.addressValidatorFns = this.shippingAddressValidators;
|
// this.addressValidatorFns = this.shippingAddressValidators;
|
||||||
} else {
|
// } else {
|
||||||
this.emailRequiredMark = false;
|
// this.emailRequiredMark = false;
|
||||||
this.emailValidatorFn = [Validators.email, validateEmail];
|
// this.emailValidatorFn = [Validators.email, validateEmail];
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
fetchCustomerInfo(): Observable<CustomerDTO | null> {
|
// fetchCustomerInfo(): Observable<CustomerDTO | null> {
|
||||||
const email = this.formData.email;
|
// const email = this.formData.email;
|
||||||
return this.customerService.getOnlineCustomerByEmail(email).pipe(
|
// return this.customerService.getOnlineCustomerByEmail(email).pipe(
|
||||||
map((result) => {
|
// map((result) => {
|
||||||
if (result) {
|
// if (result) {
|
||||||
return result;
|
// return result;
|
||||||
}
|
// }
|
||||||
return null;
|
// return null;
|
||||||
}),
|
// }),
|
||||||
catchError((err) => {
|
// catchError((err) => {
|
||||||
this.modal.open({
|
// this.modal.open({
|
||||||
content: UiErrorModalComponent,
|
// content: UiErrorModalComponent,
|
||||||
data: err,
|
// data: err,
|
||||||
});
|
// });
|
||||||
return [null];
|
// return [null];
|
||||||
}),
|
// }),
|
||||||
);
|
// );
|
||||||
}
|
// }
|
||||||
|
|
||||||
getInterests(): KeyValueDTOOfStringAndString[] {
|
// getInterests(): KeyValueDTOOfStringAndString[] {
|
||||||
const interests: KeyValueDTOOfStringAndString[] = [];
|
// const interests: KeyValueDTOOfStringAndString[] = [];
|
||||||
|
|
||||||
for (const key in this.formData.interests) {
|
// for (const key in this.formData.interests) {
|
||||||
if (this.formData.interests[key]) {
|
// if (this.formData.interests[key]) {
|
||||||
interests.push({ key, group: 'KUBI_INTERESSEN' });
|
// interests.push({ key, group: 'KUBI_INTERESSEN' });
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
return interests;
|
// return interests;
|
||||||
}
|
// }
|
||||||
|
|
||||||
getNewsletter(): KeyValueDTOOfStringAndString | undefined {
|
// getNewsletter(): KeyValueDTOOfStringAndString | undefined {
|
||||||
if (this.formData.newsletter) {
|
// if (this.formData.newsletter) {
|
||||||
return { key: 'kubi_newsletter', group: 'KUBI_NEWSLETTER' };
|
// return { key: 'kubi_newsletter', group: 'KUBI_NEWSLETTER' };
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
static MapCustomerInfoDtoToCustomerDto(customerInfoDto: CustomerInfoDTO): CustomerDTO {
|
// static MapCustomerInfoDtoToCustomerDto(customerInfoDto: CustomerInfoDTO): CustomerDTO {
|
||||||
return {
|
// return {
|
||||||
address: customerInfoDto.address,
|
// address: customerInfoDto.address,
|
||||||
agentComment: customerInfoDto.agentComment,
|
// agentComment: customerInfoDto.agentComment,
|
||||||
bonusCard: customerInfoDto.bonusCard,
|
// bonusCard: customerInfoDto.bonusCard,
|
||||||
campaignCode: customerInfoDto.campaignCode,
|
// campaignCode: customerInfoDto.campaignCode,
|
||||||
communicationDetails: customerInfoDto.communicationDetails,
|
// communicationDetails: customerInfoDto.communicationDetails,
|
||||||
createdInBranch: customerInfoDto.createdInBranch,
|
// createdInBranch: customerInfoDto.createdInBranch,
|
||||||
customerGroup: customerInfoDto.customerGroup,
|
// customerGroup: customerInfoDto.customerGroup,
|
||||||
customerNumber: customerInfoDto.customerNumber,
|
// customerNumber: customerInfoDto.customerNumber,
|
||||||
customerStatus: customerInfoDto.customerStatus,
|
// customerStatus: customerInfoDto.customerStatus,
|
||||||
customerType: customerInfoDto.customerType,
|
// customerType: customerInfoDto.customerType,
|
||||||
dateOfBirth: customerInfoDto.dateOfBirth,
|
// dateOfBirth: customerInfoDto.dateOfBirth,
|
||||||
features: customerInfoDto.features,
|
// features: customerInfoDto.features,
|
||||||
firstName: customerInfoDto.firstName,
|
// firstName: customerInfoDto.firstName,
|
||||||
lastName: customerInfoDto.lastName,
|
// lastName: customerInfoDto.lastName,
|
||||||
gender: customerInfoDto.gender,
|
// gender: customerInfoDto.gender,
|
||||||
hasOnlineAccount: customerInfoDto.hasOnlineAccount,
|
// hasOnlineAccount: customerInfoDto.hasOnlineAccount,
|
||||||
isGuestAccount: customerInfoDto.isGuestAccount,
|
// isGuestAccount: customerInfoDto.isGuestAccount,
|
||||||
label: customerInfoDto.label,
|
// label: customerInfoDto.label,
|
||||||
notificationChannels: customerInfoDto.notificationChannels,
|
// notificationChannels: customerInfoDto.notificationChannels,
|
||||||
organisation: customerInfoDto.organisation,
|
// organisation: customerInfoDto.organisation,
|
||||||
title: customerInfoDto.title,
|
// title: customerInfoDto.title,
|
||||||
id: customerInfoDto.id,
|
// id: customerInfoDto.id,
|
||||||
pId: customerInfoDto.pId,
|
// pId: customerInfoDto.pId,
|
||||||
};
|
// };
|
||||||
}
|
// }
|
||||||
|
|
||||||
async saveCustomer(customer: CustomerDTO): Promise<CustomerDTO> {
|
// async saveCustomer(customer: CustomerDTO): Promise<CustomerDTO> {
|
||||||
const isWebshop = this._customerType === 'webshop';
|
// const isWebshop = this._customerType === 'webshop';
|
||||||
let res: Result<CustomerDTO>;
|
// let res: Result<CustomerDTO>;
|
||||||
|
|
||||||
const { customerDto, customerInfoDto } = this.formData?._meta ?? {};
|
// const { customerDto, customerInfoDto } = this.formData?._meta ?? {};
|
||||||
|
|
||||||
if (customerDto) {
|
// if (customerDto) {
|
||||||
customer = { ...customerDto, ...customer };
|
// customer = { ...customerDto, ...customer };
|
||||||
} else if (customerInfoDto) {
|
// } else if (customerInfoDto) {
|
||||||
customer = { ...CreateP4MCustomerComponent.MapCustomerInfoDtoToCustomerDto(customerInfoDto), ...customer };
|
// customer = { ...CreateP4MCustomerComponent.MapCustomerInfoDtoToCustomerDto(customerInfoDto), ...customer };
|
||||||
}
|
// }
|
||||||
|
|
||||||
const p4mFeature = customer.features?.find((attr) => attr.key === 'p4mUser');
|
// const p4mFeature = customer.features?.find((attr) => attr.key === 'p4mUser');
|
||||||
if (p4mFeature) {
|
// if (p4mFeature) {
|
||||||
p4mFeature.value = this.formData.p4m;
|
// p4mFeature.value = this.formData.p4m;
|
||||||
} else {
|
// } else {
|
||||||
customer.features.push({
|
// customer.features.push({
|
||||||
key: 'p4mUser',
|
// key: 'p4mUser',
|
||||||
value: this.formData.p4m,
|
// value: this.formData.p4m,
|
||||||
});
|
// });
|
||||||
}
|
// }
|
||||||
|
|
||||||
const interests = this.getInterests();
|
// const interests = this.getInterests();
|
||||||
|
|
||||||
if (interests.length > 0) {
|
// if (interests.length > 0) {
|
||||||
customer.features?.push(...interests);
|
// customer.features?.push(...interests);
|
||||||
// TODO: Klärung wie Interessen zukünftig gespeichert werden
|
// // TODO: Klärung wie Interessen zukünftig gespeichert werden
|
||||||
// await this._loyaltyCardService
|
// // await this._loyaltyCardService
|
||||||
// .LoyaltyCardSaveInteressen({
|
// // .LoyaltyCardSaveInteressen({
|
||||||
// customerId: res.result.id,
|
// // customerId: res.result.id,
|
||||||
// interessen: this.getInterests(),
|
// // interessen: this.getInterests(),
|
||||||
// })
|
// // })
|
||||||
// .toPromise();
|
// // .toPromise();
|
||||||
}
|
// }
|
||||||
|
|
||||||
const newsletter = this.getNewsletter();
|
// const newsletter = this.getNewsletter();
|
||||||
|
|
||||||
if (newsletter) {
|
// if (newsletter) {
|
||||||
customer.features.push(newsletter);
|
// customer.features.push(newsletter);
|
||||||
} else {
|
// } else {
|
||||||
customer.features = customer.features.filter(
|
// customer.features = customer.features.filter(
|
||||||
(feature) => feature.key !== 'kubi_newsletter' && feature.group !== 'KUBI_NEWSLETTER',
|
// (feature) => feature.key !== 'kubi_newsletter' && feature.group !== 'KUBI_NEWSLETTER',
|
||||||
);
|
// );
|
||||||
}
|
// }
|
||||||
|
|
||||||
if (isWebshop) {
|
// if (isWebshop) {
|
||||||
if (customer.id > 0) {
|
// if (customer.id > 0) {
|
||||||
if (this.formData?._meta?.hasLocalityCard) {
|
// if (this.formData?._meta?.hasLocalityCard) {
|
||||||
res = await this.customerService.updateStoreP4MToWebshopP4M(customer);
|
// res = await this.customerService.updateStoreP4MToWebshopP4M(customer);
|
||||||
} else {
|
// } else {
|
||||||
res = await this.customerService.updateToP4MOnlineCustomer(customer);
|
// res = await this.customerService.updateToP4MOnlineCustomer(customer);
|
||||||
}
|
// }
|
||||||
} else {
|
// } else {
|
||||||
res = await this.customerService.createOnlineCustomer(customer).toPromise();
|
// res = await this.customerService.createOnlineCustomer(customer).toPromise();
|
||||||
}
|
// }
|
||||||
} else {
|
// } else {
|
||||||
res = await this.customerService.createStoreCustomer(customer).toPromise();
|
// res = await this.customerService.createStoreCustomer(customer).toPromise();
|
||||||
}
|
// }
|
||||||
|
|
||||||
return res.result;
|
// return res.result;
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|||||||
@@ -1,45 +1,45 @@
|
|||||||
import { NgModule } from '@angular/core';
|
// import { NgModule } from '@angular/core';
|
||||||
import { CommonModule } from '@angular/common';
|
// import { CommonModule } from '@angular/common';
|
||||||
|
|
||||||
import { CreateP4MCustomerComponent } from './create-p4m-customer.component';
|
// import { CreateP4MCustomerComponent } from './create-p4m-customer.component';
|
||||||
import {
|
// import {
|
||||||
AddressFormBlockModule,
|
// AddressFormBlockModule,
|
||||||
BirthDateFormBlockModule,
|
// BirthDateFormBlockModule,
|
||||||
InterestsFormBlockModule,
|
// InterestsFormBlockModule,
|
||||||
NameFormBlockModule,
|
// NameFormBlockModule,
|
||||||
OrganisationFormBlockModule,
|
// OrganisationFormBlockModule,
|
||||||
P4mNumberFormBlockModule,
|
// P4mNumberFormBlockModule,
|
||||||
NewsletterFormBlockModule,
|
// NewsletterFormBlockModule,
|
||||||
DeviatingAddressFormBlockComponentModule,
|
// DeviatingAddressFormBlockComponentModule,
|
||||||
AcceptAGBFormBlockModule,
|
// AcceptAGBFormBlockModule,
|
||||||
EmailFormBlockModule,
|
// EmailFormBlockModule,
|
||||||
PhoneNumbersFormBlockModule,
|
// PhoneNumbersFormBlockModule,
|
||||||
} from '../../components/form-blocks';
|
// } from '../../components/form-blocks';
|
||||||
import { CustomerTypeSelectorModule } from '../../components/customer-type-selector';
|
// import { CustomerTypeSelectorModule } from '../../components/customer-type-selector';
|
||||||
import { UiSpinnerModule } from '@ui/spinner';
|
// import { UiSpinnerModule } from '@ui/spinner';
|
||||||
import { UiIconModule } from '@ui/icon';
|
// import { UiIconModule } from '@ui/icon';
|
||||||
import { RouterModule } from '@angular/router';
|
// import { RouterModule } from '@angular/router';
|
||||||
|
|
||||||
@NgModule({
|
// @NgModule({
|
||||||
imports: [
|
// imports: [
|
||||||
CommonModule,
|
// CommonModule,
|
||||||
CustomerTypeSelectorModule,
|
// CustomerTypeSelectorModule,
|
||||||
AddressFormBlockModule,
|
// AddressFormBlockModule,
|
||||||
BirthDateFormBlockModule,
|
// BirthDateFormBlockModule,
|
||||||
InterestsFormBlockModule,
|
// InterestsFormBlockModule,
|
||||||
NameFormBlockModule,
|
// NameFormBlockModule,
|
||||||
OrganisationFormBlockModule,
|
// OrganisationFormBlockModule,
|
||||||
P4mNumberFormBlockModule,
|
// P4mNumberFormBlockModule,
|
||||||
NewsletterFormBlockModule,
|
// NewsletterFormBlockModule,
|
||||||
DeviatingAddressFormBlockComponentModule,
|
// DeviatingAddressFormBlockComponentModule,
|
||||||
AcceptAGBFormBlockModule,
|
// AcceptAGBFormBlockModule,
|
||||||
EmailFormBlockModule,
|
// EmailFormBlockModule,
|
||||||
PhoneNumbersFormBlockModule,
|
// PhoneNumbersFormBlockModule,
|
||||||
UiSpinnerModule,
|
// UiSpinnerModule,
|
||||||
UiIconModule,
|
// UiIconModule,
|
||||||
RouterModule,
|
// RouterModule,
|
||||||
],
|
// ],
|
||||||
exports: [CreateP4MCustomerComponent],
|
// exports: [CreateP4MCustomerComponent],
|
||||||
declarations: [CreateP4MCustomerComponent],
|
// declarations: [CreateP4MCustomerComponent],
|
||||||
})
|
// })
|
||||||
export class CreateP4MCustomerModule {}
|
// export class CreateP4MCustomerModule {}
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
export * from './create-p4m-customer.component';
|
// export * from './create-p4m-customer.component';
|
||||||
export * from './create-p4m-customer.module';
|
// export * from './create-p4m-customer.module';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Component, ChangeDetectionStrategy, ViewChild } from '@angular/core';
|
import { Component, ChangeDetectionStrategy, ViewChild } from '@angular/core';
|
||||||
import { ValidatorFn, Validators } from '@angular/forms';
|
import { ValidatorFn, Validators } from '@angular/forms';
|
||||||
import { CustomerDTO } from '@generated/swagger/crm-api';
|
import { CustomerDTO, CustomerInfoDTO } from '@generated/swagger/crm-api';
|
||||||
import { map } from 'rxjs/operators';
|
import { map } from 'rxjs/operators';
|
||||||
import {
|
import {
|
||||||
AddressFormBlockComponent,
|
AddressFormBlockComponent,
|
||||||
@@ -10,13 +10,16 @@ import {
|
|||||||
import { NameFormBlockData } from '../../components/form-blocks/name/name-form-block-data';
|
import { NameFormBlockData } from '../../components/form-blocks/name/name-form-block-data';
|
||||||
import { validateEmail } from '../../validators/email-validator';
|
import { validateEmail } from '../../validators/email-validator';
|
||||||
import { AbstractCreateCustomer } from '../abstract-create-customer';
|
import { AbstractCreateCustomer } from '../abstract-create-customer';
|
||||||
import { CreateP4MCustomerComponent } from '../create-p4m-customer';
|
// import { CreateP4MCustomerComponent } from '../create-p4m-customer';
|
||||||
import { zipCodeValidator } from '../../validators/zip-code-validator';
|
import { zipCodeValidator } from '../../validators/zip-code-validator';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-create-webshop-customer',
|
selector: 'app-create-webshop-customer',
|
||||||
templateUrl: 'create-webshop-customer.component.html',
|
templateUrl: 'create-webshop-customer.component.html',
|
||||||
styleUrls: ['../create-customer.scss', 'create-webshop-customer.component.scss'],
|
styleUrls: [
|
||||||
|
'../create-customer.scss',
|
||||||
|
'create-webshop-customer.component.scss',
|
||||||
|
],
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
standalone: false,
|
standalone: false,
|
||||||
})
|
})
|
||||||
@@ -26,7 +29,11 @@ export class CreateWebshopCustomerComponent extends AbstractCreateCustomer {
|
|||||||
validateAddress = true;
|
validateAddress = true;
|
||||||
validateShippingAddress = true;
|
validateShippingAddress = true;
|
||||||
|
|
||||||
nameRequiredMarks: (keyof NameFormBlockData)[] = ['gender', 'firstName', 'lastName'];
|
nameRequiredMarks: (keyof NameFormBlockData)[] = [
|
||||||
|
'gender',
|
||||||
|
'firstName',
|
||||||
|
'lastName',
|
||||||
|
];
|
||||||
|
|
||||||
nameValidationFns: Record<keyof NameFormBlockData, ValidatorFn[]> = {
|
nameValidationFns: Record<keyof NameFormBlockData, ValidatorFn[]> = {
|
||||||
firstName: [Validators.required],
|
firstName: [Validators.required],
|
||||||
@@ -35,7 +42,13 @@ export class CreateWebshopCustomerComponent extends AbstractCreateCustomer {
|
|||||||
title: [],
|
title: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
addressRequiredMarks: (keyof AddressFormBlockData)[] = ['street', 'streetNumber', 'zipCode', 'city', 'country'];
|
addressRequiredMarks: (keyof AddressFormBlockData)[] = [
|
||||||
|
'street',
|
||||||
|
'streetNumber',
|
||||||
|
'zipCode',
|
||||||
|
'city',
|
||||||
|
'country',
|
||||||
|
];
|
||||||
|
|
||||||
addressValidators: Record<string, ValidatorFn[]> = {
|
addressValidators: Record<string, ValidatorFn[]> = {
|
||||||
street: [Validators.required],
|
street: [Validators.required],
|
||||||
@@ -68,7 +81,11 @@ export class CreateWebshopCustomerComponent extends AbstractCreateCustomer {
|
|||||||
if (customerDto) {
|
if (customerDto) {
|
||||||
customer = { ...customerDto, ...customer };
|
customer = { ...customerDto, ...customer };
|
||||||
} else if (customerInfoDto) {
|
} else if (customerInfoDto) {
|
||||||
customer = { ...CreateP4MCustomerComponent.MapCustomerInfoDtoToCustomerDto(customerInfoDto), ...customer };
|
customer = {
|
||||||
|
// ...CreateP4MCustomerComponent.MapCustomerInfoDtoToCustomerDto(customerInfoDto),
|
||||||
|
...this.mapCustomerInfoDtoToCustomerDto(customerInfoDto),
|
||||||
|
...customer,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await this.customerService.updateToOnlineCustomer(customer);
|
const res = await this.customerService.updateToOnlineCustomer(customer);
|
||||||
@@ -80,4 +97,34 @@ export class CreateWebshopCustomerComponent extends AbstractCreateCustomer {
|
|||||||
.toPromise();
|
.toPromise();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mapCustomerInfoDtoToCustomerDto(
|
||||||
|
customerInfoDto: CustomerInfoDTO,
|
||||||
|
): CustomerDTO {
|
||||||
|
return {
|
||||||
|
address: customerInfoDto.address,
|
||||||
|
agentComment: customerInfoDto.agentComment,
|
||||||
|
bonusCard: customerInfoDto.bonusCard,
|
||||||
|
campaignCode: customerInfoDto.campaignCode,
|
||||||
|
communicationDetails: customerInfoDto.communicationDetails,
|
||||||
|
createdInBranch: customerInfoDto.createdInBranch,
|
||||||
|
customerGroup: customerInfoDto.customerGroup,
|
||||||
|
customerNumber: customerInfoDto.customerNumber,
|
||||||
|
customerStatus: customerInfoDto.customerStatus,
|
||||||
|
customerType: customerInfoDto.customerType,
|
||||||
|
dateOfBirth: customerInfoDto.dateOfBirth,
|
||||||
|
features: customerInfoDto.features,
|
||||||
|
firstName: customerInfoDto.firstName,
|
||||||
|
lastName: customerInfoDto.lastName,
|
||||||
|
gender: customerInfoDto.gender,
|
||||||
|
hasOnlineAccount: customerInfoDto.hasOnlineAccount,
|
||||||
|
isGuestAccount: customerInfoDto.isGuestAccount,
|
||||||
|
label: customerInfoDto.label,
|
||||||
|
notificationChannels: customerInfoDto.notificationChannels,
|
||||||
|
organisation: customerInfoDto.organisation,
|
||||||
|
title: customerInfoDto.title,
|
||||||
|
id: customerInfoDto.id,
|
||||||
|
pId: customerInfoDto.pId,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { CustomerDTO, Gender } from '@generated/swagger/crm-api';
|
import { CustomerDTO, Gender } from '@generated/swagger/crm-api';
|
||||||
|
|
||||||
export interface CreateCustomerQueryParams {
|
export interface CreateCustomerQueryParams {
|
||||||
p4mNumber?: string;
|
// p4mNumber?: string;
|
||||||
customerId?: number;
|
customerId?: number;
|
||||||
gender?: Gender;
|
gender?: Gender;
|
||||||
title?: string;
|
title?: string;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
export * from './create-b2b-customer';
|
export * from './create-b2b-customer';
|
||||||
export * from './create-guest-customer';
|
export * from './create-guest-customer';
|
||||||
export * from './create-p4m-customer';
|
// export * from './create-p4m-customer';
|
||||||
export * from './create-store-customer';
|
export * from './create-store-customer';
|
||||||
export * from './create-webshop-customer';
|
export * from './create-webshop-customer';
|
||||||
export * from './defs';
|
export * from './defs';
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@if (formData$ | async; as data) {
|
<!-- @if (formData$ | async; as data) {
|
||||||
<form (keydown.enter)="$event.preventDefault()">
|
<form (keydown.enter)="$event.preventDefault()">
|
||||||
<h1 class="title flex flex-row items-center justify-center">Kundenkartendaten erfasen</h1>
|
<h1 class="title flex flex-row items-center justify-center">Kundenkartendaten erfasen</h1>
|
||||||
<p class="description">Bitte erfassen Sie die Kundenkarte</p>
|
<p class="description">Bitte erfassen Sie die Kundenkarte</p>
|
||||||
@@ -106,4 +106,4 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
}
|
} -->
|
||||||
|
|||||||
@@ -1,156 +1,156 @@
|
|||||||
import { Component, ChangeDetectionStrategy, OnInit } from '@angular/core';
|
// import { Component, ChangeDetectionStrategy, OnInit } from '@angular/core';
|
||||||
import { AsyncValidatorFn, ValidatorFn, Validators } from '@angular/forms';
|
// import { AsyncValidatorFn, ValidatorFn, Validators } from '@angular/forms';
|
||||||
import { Result } from '@domain/defs';
|
// import { Result } from '@domain/defs';
|
||||||
import { CustomerDTO, KeyValueDTOOfStringAndString, PayerDTO } from '@generated/swagger/crm-api';
|
// import { CustomerDTO, KeyValueDTOOfStringAndString, PayerDTO } from '@generated/swagger/crm-api';
|
||||||
import { AddressFormBlockData } from '../../components/form-blocks';
|
// import { AddressFormBlockData } from '../../components/form-blocks';
|
||||||
import { NameFormBlockData } from '../../components/form-blocks/name/name-form-block-data';
|
// import { NameFormBlockData } from '../../components/form-blocks/name/name-form-block-data';
|
||||||
import { AbstractCreateCustomer } from '../abstract-create-customer';
|
// import { AbstractCreateCustomer } from '../abstract-create-customer';
|
||||||
import { CreateP4MCustomerComponent } from '../create-p4m-customer';
|
// import { CreateP4MCustomerComponent } from '../create-p4m-customer';
|
||||||
|
|
||||||
@Component({
|
// @Component({
|
||||||
selector: 'page-update-p4m-webshop-customer',
|
// selector: 'page-update-p4m-webshop-customer',
|
||||||
templateUrl: 'update-p4m-webshop-customer.component.html',
|
// templateUrl: 'update-p4m-webshop-customer.component.html',
|
||||||
styleUrls: ['../create-customer.scss', 'update-p4m-webshop-customer.component.scss'],
|
// styleUrls: ['../create-customer.scss', 'update-p4m-webshop-customer.component.scss'],
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
// changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
standalone: false,
|
// standalone: false,
|
||||||
})
|
// })
|
||||||
export class UpdateP4MWebshopCustomerComponent extends AbstractCreateCustomer implements OnInit {
|
// export class UpdateP4MWebshopCustomerComponent extends AbstractCreateCustomer implements OnInit {
|
||||||
customerType = 'webshop-p4m/update';
|
// customerType = 'webshop-p4m/update';
|
||||||
|
|
||||||
validateAddress = true;
|
// validateAddress = true;
|
||||||
|
|
||||||
validateShippingAddress = true;
|
// validateShippingAddress = true;
|
||||||
|
|
||||||
agbValidatorFns = [Validators.requiredTrue];
|
// agbValidatorFns = [Validators.requiredTrue];
|
||||||
|
|
||||||
birthDateValidatorFns = [Validators.required, this.minBirthDateValidator()];
|
// birthDateValidatorFns = [Validators.required, this.minBirthDateValidator()];
|
||||||
|
|
||||||
nameRequiredMarks: (keyof NameFormBlockData)[] = ['gender', 'firstName', 'lastName'];
|
// nameRequiredMarks: (keyof NameFormBlockData)[] = ['gender', 'firstName', 'lastName'];
|
||||||
|
|
||||||
nameValidationFns: Record<keyof NameFormBlockData, ValidatorFn[]> = {
|
// nameValidationFns: Record<keyof NameFormBlockData, ValidatorFn[]> = {
|
||||||
firstName: [Validators.required],
|
// firstName: [Validators.required],
|
||||||
lastName: [Validators.required],
|
// lastName: [Validators.required],
|
||||||
gender: [Validators.required],
|
// gender: [Validators.required],
|
||||||
title: [],
|
// title: [],
|
||||||
};
|
// };
|
||||||
|
|
||||||
addressRequiredMarks: (keyof AddressFormBlockData)[] = ['street', 'streetNumber', 'zipCode', 'city', 'country'];
|
// addressRequiredMarks: (keyof AddressFormBlockData)[] = ['street', 'streetNumber', 'zipCode', 'city', 'country'];
|
||||||
|
|
||||||
addressValidatorFns: Record<string, ValidatorFn[]> = {
|
// addressValidatorFns: Record<string, ValidatorFn[]> = {
|
||||||
street: [Validators.required],
|
// street: [Validators.required],
|
||||||
streetNumber: [Validators.required],
|
// streetNumber: [Validators.required],
|
||||||
zipCode: [Validators.required],
|
// zipCode: [Validators.required],
|
||||||
city: [Validators.required],
|
// city: [Validators.required],
|
||||||
country: [Validators.required],
|
// country: [Validators.required],
|
||||||
};
|
// };
|
||||||
|
|
||||||
asyncLoyaltyCardValidatorFn: AsyncValidatorFn[] = [this.checkLoyalityCardValidator];
|
// asyncLoyaltyCardValidatorFn: AsyncValidatorFn[] = [this.checkLoyalityCardValidator];
|
||||||
|
|
||||||
get billingAddress(): PayerDTO | undefined {
|
// get billingAddress(): PayerDTO | undefined {
|
||||||
const payers = this.formData?._meta?.customerDto?.payers;
|
// const payers = this.formData?._meta?.customerDto?.payers;
|
||||||
|
|
||||||
if (!payers || payers.length === 0) {
|
// if (!payers || payers.length === 0) {
|
||||||
return undefined;
|
// return undefined;
|
||||||
}
|
// }
|
||||||
|
|
||||||
// the default payer is the payer with the latest isDefault(Date) value
|
// // the default payer is the payer with the latest isDefault(Date) value
|
||||||
const defaultPayer = payers.reduce((prev, curr) =>
|
// const defaultPayer = payers.reduce((prev, curr) =>
|
||||||
new Date(prev.isDefault) > new Date(curr.isDefault) ? prev : curr,
|
// new Date(prev.isDefault) > new Date(curr.isDefault) ? prev : curr,
|
||||||
);
|
// );
|
||||||
|
|
||||||
return defaultPayer.payer.data;
|
// return defaultPayer.payer.data;
|
||||||
}
|
// }
|
||||||
|
|
||||||
get shippingAddress() {
|
// get shippingAddress() {
|
||||||
const shippingAddresses = this.formData?._meta?.customerDto?.shippingAddresses;
|
// const shippingAddresses = this.formData?._meta?.customerDto?.shippingAddresses;
|
||||||
|
|
||||||
if (!shippingAddresses || shippingAddresses.length === 0) {
|
// if (!shippingAddresses || shippingAddresses.length === 0) {
|
||||||
return undefined;
|
// return undefined;
|
||||||
}
|
// }
|
||||||
|
|
||||||
// the default shipping address is the shipping address with the latest isDefault(Date) value
|
// // the default shipping address is the shipping address with the latest isDefault(Date) value
|
||||||
const defaultShippingAddress = shippingAddresses.reduce((prev, curr) =>
|
// const defaultShippingAddress = shippingAddresses.reduce((prev, curr) =>
|
||||||
new Date(prev.data.isDefault) > new Date(curr.data.isDefault) ? prev : curr,
|
// new Date(prev.data.isDefault) > new Date(curr.data.isDefault) ? prev : curr,
|
||||||
);
|
// );
|
||||||
|
|
||||||
return defaultShippingAddress.data;
|
// return defaultShippingAddress.data;
|
||||||
}
|
// }
|
||||||
|
|
||||||
ngOnInit() {
|
// ngOnInit() {
|
||||||
super.ngOnInit();
|
// super.ngOnInit();
|
||||||
}
|
// }
|
||||||
|
|
||||||
getInterests(): KeyValueDTOOfStringAndString[] {
|
// getInterests(): KeyValueDTOOfStringAndString[] {
|
||||||
const interests: KeyValueDTOOfStringAndString[] = [];
|
// const interests: KeyValueDTOOfStringAndString[] = [];
|
||||||
|
|
||||||
for (const key in this.formData.interests) {
|
// for (const key in this.formData.interests) {
|
||||||
if (this.formData.interests[key]) {
|
// if (this.formData.interests[key]) {
|
||||||
interests.push({ key, group: 'KUBI_INTERESSEN' });
|
// interests.push({ key, group: 'KUBI_INTERESSEN' });
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
return interests;
|
// return interests;
|
||||||
}
|
// }
|
||||||
|
|
||||||
getNewsletter(): KeyValueDTOOfStringAndString | undefined {
|
// getNewsletter(): KeyValueDTOOfStringAndString | undefined {
|
||||||
if (this.formData.newsletter) {
|
// if (this.formData.newsletter) {
|
||||||
return { key: 'kubi_newsletter', group: 'KUBI_NEWSLETTER' };
|
// return { key: 'kubi_newsletter', group: 'KUBI_NEWSLETTER' };
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
async saveCustomer(customer: CustomerDTO): Promise<CustomerDTO> {
|
// async saveCustomer(customer: CustomerDTO): Promise<CustomerDTO> {
|
||||||
let res: Result<CustomerDTO>;
|
// let res: Result<CustomerDTO>;
|
||||||
|
|
||||||
const { customerDto, customerInfoDto } = this.formData?._meta ?? {};
|
// const { customerDto, customerInfoDto } = this.formData?._meta ?? {};
|
||||||
|
|
||||||
if (customerDto) {
|
// if (customerDto) {
|
||||||
customer = { ...customerDto, shippingAddresses: [], payers: [], ...customer };
|
// customer = { ...customerDto, shippingAddresses: [], payers: [], ...customer };
|
||||||
|
|
||||||
if (customerDto.shippingAddresses?.length) {
|
// if (customerDto.shippingAddresses?.length) {
|
||||||
customer.shippingAddresses.unshift(...customerDto.shippingAddresses);
|
// customer.shippingAddresses.unshift(...customerDto.shippingAddresses);
|
||||||
}
|
// }
|
||||||
if (customerDto.payers?.length) {
|
// if (customerDto.payers?.length) {
|
||||||
customer.payers.unshift(...customerDto.payers);
|
// customer.payers.unshift(...customerDto.payers);
|
||||||
}
|
// }
|
||||||
} else if (customerInfoDto) {
|
// } else if (customerInfoDto) {
|
||||||
customer = { ...CreateP4MCustomerComponent.MapCustomerInfoDtoToCustomerDto(customerInfoDto), ...customer };
|
// customer = { ...CreateP4MCustomerComponent.MapCustomerInfoDtoToCustomerDto(customerInfoDto), ...customer };
|
||||||
}
|
// }
|
||||||
|
|
||||||
const p4mFeature = customer.features?.find((attr) => attr.key === 'p4mUser');
|
// const p4mFeature = customer.features?.find((attr) => attr.key === 'p4mUser');
|
||||||
if (p4mFeature) {
|
// if (p4mFeature) {
|
||||||
p4mFeature.value = this.formData.p4m;
|
// p4mFeature.value = this.formData.p4m;
|
||||||
} else {
|
// } else {
|
||||||
customer.features.push({
|
// customer.features.push({
|
||||||
key: 'p4mUser',
|
// key: 'p4mUser',
|
||||||
value: this.formData.p4m,
|
// value: this.formData.p4m,
|
||||||
});
|
// });
|
||||||
}
|
// }
|
||||||
|
|
||||||
const interests = this.getInterests();
|
// const interests = this.getInterests();
|
||||||
|
|
||||||
if (interests.length > 0) {
|
// if (interests.length > 0) {
|
||||||
customer.features?.push(...interests);
|
// customer.features?.push(...interests);
|
||||||
// TODO: Klärung wie Interessen zukünftig gespeichert werden
|
// // TODO: Klärung wie Interessen zukünftig gespeichert werden
|
||||||
// await this._loyaltyCardService
|
// // await this._loyaltyCardService
|
||||||
// .LoyaltyCardSaveInteressen({
|
// // .LoyaltyCardSaveInteressen({
|
||||||
// customerId: res.result.id,
|
// // customerId: res.result.id,
|
||||||
// interessen: this.getInterests(),
|
// // interessen: this.getInterests(),
|
||||||
// })
|
// // })
|
||||||
// .toPromise();
|
// // .toPromise();
|
||||||
}
|
// }
|
||||||
|
|
||||||
const newsletter = this.getNewsletter();
|
// const newsletter = this.getNewsletter();
|
||||||
|
|
||||||
if (newsletter) {
|
// if (newsletter) {
|
||||||
customer.features.push(newsletter);
|
// customer.features.push(newsletter);
|
||||||
} else {
|
// } else {
|
||||||
customer.features = customer.features.filter(
|
// customer.features = customer.features.filter(
|
||||||
(feature) => feature.key !== 'kubi_newsletter' && feature.group !== 'KUBI_NEWSLETTER',
|
// (feature) => feature.key !== 'kubi_newsletter' && feature.group !== 'KUBI_NEWSLETTER',
|
||||||
);
|
// );
|
||||||
}
|
// }
|
||||||
|
|
||||||
res = await this.customerService.updateToP4MOnlineCustomer(customer);
|
// res = await this.customerService.updateToP4MOnlineCustomer(customer);
|
||||||
|
|
||||||
return res.result;
|
// return res.result;
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|||||||
@@ -1,48 +1,48 @@
|
|||||||
import { NgModule } from '@angular/core';
|
// import { NgModule } from '@angular/core';
|
||||||
import { CommonModule } from '@angular/common';
|
// import { CommonModule } from '@angular/common';
|
||||||
|
|
||||||
import { UpdateP4MWebshopCustomerComponent } from './update-p4m-webshop-customer.component';
|
// import { UpdateP4MWebshopCustomerComponent } from './update-p4m-webshop-customer.component';
|
||||||
|
|
||||||
import {
|
// import {
|
||||||
AddressFormBlockModule,
|
// AddressFormBlockModule,
|
||||||
BirthDateFormBlockModule,
|
// BirthDateFormBlockModule,
|
||||||
InterestsFormBlockModule,
|
// InterestsFormBlockModule,
|
||||||
NameFormBlockModule,
|
// NameFormBlockModule,
|
||||||
OrganisationFormBlockModule,
|
// OrganisationFormBlockModule,
|
||||||
P4mNumberFormBlockModule,
|
// P4mNumberFormBlockModule,
|
||||||
NewsletterFormBlockModule,
|
// NewsletterFormBlockModule,
|
||||||
DeviatingAddressFormBlockComponentModule,
|
// DeviatingAddressFormBlockComponentModule,
|
||||||
AcceptAGBFormBlockModule,
|
// AcceptAGBFormBlockModule,
|
||||||
EmailFormBlockModule,
|
// EmailFormBlockModule,
|
||||||
PhoneNumbersFormBlockModule,
|
// PhoneNumbersFormBlockModule,
|
||||||
} from '../../components/form-blocks';
|
// } from '../../components/form-blocks';
|
||||||
import { UiFormControlModule } from '@ui/form-control';
|
// import { UiFormControlModule } from '@ui/form-control';
|
||||||
import { UiInputModule } from '@ui/input';
|
// import { UiInputModule } from '@ui/input';
|
||||||
import { CustomerPipesModule } from '@shared/pipes/customer';
|
// import { CustomerPipesModule } from '@shared/pipes/customer';
|
||||||
import { CustomerTypeSelectorModule } from '../../components/customer-type-selector';
|
// import { CustomerTypeSelectorModule } from '../../components/customer-type-selector';
|
||||||
import { UiSpinnerModule } from '@ui/spinner';
|
// import { UiSpinnerModule } from '@ui/spinner';
|
||||||
|
|
||||||
@NgModule({
|
// @NgModule({
|
||||||
imports: [
|
// imports: [
|
||||||
CommonModule,
|
// CommonModule,
|
||||||
CustomerTypeSelectorModule,
|
// CustomerTypeSelectorModule,
|
||||||
AddressFormBlockModule,
|
// AddressFormBlockModule,
|
||||||
BirthDateFormBlockModule,
|
// BirthDateFormBlockModule,
|
||||||
InterestsFormBlockModule,
|
// InterestsFormBlockModule,
|
||||||
NameFormBlockModule,
|
// NameFormBlockModule,
|
||||||
OrganisationFormBlockModule,
|
// OrganisationFormBlockModule,
|
||||||
P4mNumberFormBlockModule,
|
// P4mNumberFormBlockModule,
|
||||||
NewsletterFormBlockModule,
|
// NewsletterFormBlockModule,
|
||||||
DeviatingAddressFormBlockComponentModule,
|
// DeviatingAddressFormBlockComponentModule,
|
||||||
AcceptAGBFormBlockModule,
|
// AcceptAGBFormBlockModule,
|
||||||
EmailFormBlockModule,
|
// EmailFormBlockModule,
|
||||||
PhoneNumbersFormBlockModule,
|
// PhoneNumbersFormBlockModule,
|
||||||
UiFormControlModule,
|
// UiFormControlModule,
|
||||||
UiInputModule,
|
// UiInputModule,
|
||||||
CustomerPipesModule,
|
// CustomerPipesModule,
|
||||||
UiSpinnerModule,
|
// UiSpinnerModule,
|
||||||
],
|
// ],
|
||||||
exports: [UpdateP4MWebshopCustomerComponent],
|
// exports: [UpdateP4MWebshopCustomerComponent],
|
||||||
declarations: [UpdateP4MWebshopCustomerComponent],
|
// declarations: [UpdateP4MWebshopCustomerComponent],
|
||||||
})
|
// })
|
||||||
export class UpdateP4MWebshopCustomerModule {}
|
// export class UpdateP4MWebshopCustomerModule {}
|
||||||
|
|||||||
@@ -1,15 +1,33 @@
|
|||||||
import { Component, ChangeDetectionStrategy, OnInit, OnDestroy, inject, effect, untracked } from '@angular/core';
|
import {
|
||||||
|
Component,
|
||||||
|
ChangeDetectionStrategy,
|
||||||
|
OnInit,
|
||||||
|
OnDestroy,
|
||||||
|
inject,
|
||||||
|
effect,
|
||||||
|
untracked,
|
||||||
|
} from '@angular/core';
|
||||||
import { ActivatedRoute, NavigationEnd, Router } from '@angular/router';
|
import { ActivatedRoute, NavigationEnd, Router } from '@angular/router';
|
||||||
import { BehaviorSubject, Subject, Subscription, fromEvent } from 'rxjs';
|
import {
|
||||||
|
BehaviorSubject,
|
||||||
|
Subject,
|
||||||
|
Subscription,
|
||||||
|
firstValueFrom,
|
||||||
|
fromEvent,
|
||||||
|
} from 'rxjs';
|
||||||
import { CustomerSearchStore } from './store/customer-search.store';
|
import { CustomerSearchStore } from './store/customer-search.store';
|
||||||
import { provideComponentStore } from '@ngrx/component-store';
|
import { provideComponentStore } from '@ngrx/component-store';
|
||||||
import { Breadcrumb, BreadcrumbService } from '@core/breadcrumb';
|
import { Breadcrumb, BreadcrumbService } from '@core/breadcrumb';
|
||||||
import { delay, filter, first, switchMap, takeUntil } from 'rxjs/operators';
|
import { delay, filter, first, switchMap, takeUntil } from 'rxjs/operators';
|
||||||
import { CustomerCreateNavigation, CustomerSearchNavigation } from '@shared/services/navigation';
|
import {
|
||||||
|
CustomerCreateNavigation,
|
||||||
|
CustomerSearchNavigation,
|
||||||
|
} from '@shared/services/navigation';
|
||||||
import { CustomerSearchMainAutocompleteProvider } from './providers/customer-search-main-autocomplete.provider';
|
import { CustomerSearchMainAutocompleteProvider } from './providers/customer-search-main-autocomplete.provider';
|
||||||
import { FilterAutocompleteProvider } from '@shared/components/filter';
|
import { FilterAutocompleteProvider } from '@shared/components/filter';
|
||||||
import { toSignal } from '@angular/core/rxjs-interop';
|
import { toSignal } from '@angular/core/rxjs-interop';
|
||||||
import { provideCancelSearchSubject } from '@shared/services/cancel-subject';
|
import { provideCancelSearchSubject } from '@shared/services/cancel-subject';
|
||||||
|
import { injectFeedbackErrorDialog } from '@isa/ui/dialog';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'page-customer-search',
|
selector: 'page-customer-search',
|
||||||
@@ -28,6 +46,7 @@ import { provideCancelSearchSubject } from '@shared/services/cancel-subject';
|
|||||||
standalone: false,
|
standalone: false,
|
||||||
})
|
})
|
||||||
export class CustomerSearchComponent implements OnInit, OnDestroy {
|
export class CustomerSearchComponent implements OnInit, OnDestroy {
|
||||||
|
#errorFeedbackDialog = injectFeedbackErrorDialog();
|
||||||
private _store = inject(CustomerSearchStore);
|
private _store = inject(CustomerSearchStore);
|
||||||
private _activatedRoute = inject(ActivatedRoute);
|
private _activatedRoute = inject(ActivatedRoute);
|
||||||
private _router = inject(Router);
|
private _router = inject(Router);
|
||||||
@@ -37,7 +56,11 @@ export class CustomerSearchComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
private searchStore = inject(CustomerSearchStore);
|
private searchStore = inject(CustomerSearchStore);
|
||||||
|
|
||||||
keyEscPressed = toSignal(fromEvent(document, 'keydown').pipe(filter((e: KeyboardEvent) => e.key === 'Escape')));
|
keyEscPressed = toSignal(
|
||||||
|
fromEvent(document, 'keydown').pipe(
|
||||||
|
filter((e: KeyboardEvent) => e.key === 'Escape'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
get breadcrumb() {
|
get breadcrumb() {
|
||||||
let breadcrumb: string;
|
let breadcrumb: string;
|
||||||
@@ -53,7 +76,9 @@ export class CustomerSearchComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
private _breadcrumbs$ = this._store.processId$.pipe(
|
private _breadcrumbs$ = this._store.processId$.pipe(
|
||||||
filter((id) => !!id),
|
filter((id) => !!id),
|
||||||
switchMap((id) => this._breadcrumbService.getBreadcrumbsByKeyAndTag$(id, 'customer')),
|
switchMap((id) =>
|
||||||
|
this._breadcrumbService.getBreadcrumbsByKeyAndTag$(id, 'customer'),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
side$ = new BehaviorSubject<string | undefined>(undefined);
|
side$ = new BehaviorSubject<string | undefined>(undefined);
|
||||||
@@ -97,53 +122,77 @@ export class CustomerSearchComponent implements OnInit, OnDestroy {
|
|||||||
this.checkDetailsBreadcrumb();
|
this.checkDetailsBreadcrumb();
|
||||||
});
|
});
|
||||||
|
|
||||||
this._eventsSubscription = this._router.events.pipe(takeUntil(this._onDestroy$)).subscribe((event) => {
|
this._eventsSubscription = this._router.events
|
||||||
if (event instanceof NavigationEnd) {
|
|
||||||
this.checkAndUpdateProcessId();
|
|
||||||
this.checkAndUpdateSide();
|
|
||||||
this.checkAndUpdateCustomerId();
|
|
||||||
this.checkBreadcrumbs();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this._store.customerListResponse$
|
|
||||||
.pipe(takeUntil(this._onDestroy$))
|
.pipe(takeUntil(this._onDestroy$))
|
||||||
.subscribe(async ([response, filter, processId, restored, skipNavigation]) => {
|
.subscribe((event) => {
|
||||||
if (this._store.processId === processId) {
|
if (event instanceof NavigationEnd) {
|
||||||
if (skipNavigation) {
|
this.checkAndUpdateProcessId();
|
||||||
return;
|
this.checkAndUpdateSide();
|
||||||
}
|
this.checkAndUpdateCustomerId();
|
||||||
|
|
||||||
if (response.hits === 1) {
|
|
||||||
// Navigate to details page
|
|
||||||
const customer = response.result[0];
|
|
||||||
|
|
||||||
if (customer.id < 0) {
|
|
||||||
// navigate to create customer
|
|
||||||
const route = this._createNavigation.upgradeCustomerRoute({ processId, customerInfo: customer });
|
|
||||||
await this._router.navigate(route.path, { queryParams: route.queryParams });
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
const route = this._navigation.detailsRoute({ processId, customerId: customer.id });
|
|
||||||
await this._router.navigate(route.path, { queryParams: filter.getQueryParams() });
|
|
||||||
}
|
|
||||||
} else if (response.hits > 1) {
|
|
||||||
const route = this._navigation.listRoute({ processId, filter });
|
|
||||||
|
|
||||||
if (
|
|
||||||
(['details'].includes(this.breadcrumb) &&
|
|
||||||
response?.result?.some((c) => c.id === this._store.customerId)) ||
|
|
||||||
restored
|
|
||||||
) {
|
|
||||||
await this._router.navigate([], { queryParams: route.queryParams });
|
|
||||||
} else {
|
|
||||||
await this._router.navigate(route.path, { queryParams: route.queryParams });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.checkBreadcrumbs();
|
this.checkBreadcrumbs();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this._store.customerListResponse$
|
||||||
|
.pipe(takeUntil(this._onDestroy$))
|
||||||
|
.subscribe(
|
||||||
|
async ([response, filter, processId, restored, skipNavigation]) => {
|
||||||
|
if (this._store.processId === processId) {
|
||||||
|
if (skipNavigation) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.hits === 1) {
|
||||||
|
// Navigate to details page
|
||||||
|
const customer = response.result[0];
|
||||||
|
|
||||||
|
if (customer.id < 0) {
|
||||||
|
// #5375 - Zusätzlich soll bei Kunden bei denen ein Upgrade möglich ist ein Dialog angezeigt werden, dass Kundenneuanlage mit Kundenkarte nicht möglich ist
|
||||||
|
await firstValueFrom(
|
||||||
|
this.#errorFeedbackDialog({
|
||||||
|
data: {
|
||||||
|
errorMessage:
|
||||||
|
'Kundenneuanlage mit Kundenkarte nicht möglich',
|
||||||
|
},
|
||||||
|
}).closed,
|
||||||
|
);
|
||||||
|
// navigate to create customer
|
||||||
|
// const route = this._createNavigation.upgradeCustomerRoute({ processId, customerInfo: customer });
|
||||||
|
// await this._router.navigate(route.path, { queryParams: route.queryParams });
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
const route = this._navigation.detailsRoute({
|
||||||
|
processId,
|
||||||
|
customerId: customer.id,
|
||||||
|
});
|
||||||
|
await this._router.navigate(route.path, {
|
||||||
|
queryParams: filter.getQueryParams(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (response.hits > 1) {
|
||||||
|
const route = this._navigation.listRoute({ processId, filter });
|
||||||
|
|
||||||
|
if (
|
||||||
|
(['details'].includes(this.breadcrumb) &&
|
||||||
|
response?.result?.some(
|
||||||
|
(c) => c.id === this._store.customerId,
|
||||||
|
)) ||
|
||||||
|
restored
|
||||||
|
) {
|
||||||
|
await this._router.navigate([], {
|
||||||
|
queryParams: route.queryParams,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await this._router.navigate(route.path, {
|
||||||
|
queryParams: route.queryParams,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.checkBreadcrumbs();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
@@ -169,7 +218,11 @@ export class CustomerSearchComponent implements OnInit, OnDestroy {
|
|||||||
this._store.setProcessId(processId);
|
this._store.setProcessId(processId);
|
||||||
this._store.reset(this._activatedRoute.snapshot.queryParams);
|
this._store.reset(this._activatedRoute.snapshot.queryParams);
|
||||||
if (!['main', 'filter'].some((s) => s === this.breadcrumb)) {
|
if (!['main', 'filter'].some((s) => s === this.breadcrumb)) {
|
||||||
const skipNavigation = ['orders', 'order-details', 'order-details-history'].includes(this.breadcrumb);
|
const skipNavigation = [
|
||||||
|
'orders',
|
||||||
|
'order-details',
|
||||||
|
'order-details-history',
|
||||||
|
].includes(this.breadcrumb);
|
||||||
this._store.search({ skipNavigation });
|
this._store.search({ skipNavigation });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -229,7 +282,9 @@ export class CustomerSearchComponent implements OnInit, OnDestroy {
|
|||||||
const mainBreadcrumb = await this.getMainBreadcrumb();
|
const mainBreadcrumb = await this.getMainBreadcrumb();
|
||||||
|
|
||||||
if (!mainBreadcrumb) {
|
if (!mainBreadcrumb) {
|
||||||
const navigation = this._navigation.defaultRoute({ processId: this._store.processId });
|
const navigation = this._navigation.defaultRoute({
|
||||||
|
processId: this._store.processId,
|
||||||
|
});
|
||||||
const breadcrumb: Breadcrumb = {
|
const breadcrumb: Breadcrumb = {
|
||||||
key: this._store.processId,
|
key: this._store.processId,
|
||||||
tags: ['customer', 'search', 'main'],
|
tags: ['customer', 'search', 'main'],
|
||||||
@@ -242,14 +297,19 @@ export class CustomerSearchComponent implements OnInit, OnDestroy {
|
|||||||
this._breadcrumbService.addBreadcrumb(breadcrumb);
|
this._breadcrumbService.addBreadcrumb(breadcrumb);
|
||||||
} else {
|
} else {
|
||||||
this._breadcrumbService.patchBreadcrumb(mainBreadcrumb.id, {
|
this._breadcrumbService.patchBreadcrumb(mainBreadcrumb.id, {
|
||||||
params: { ...this.snapshot.queryParams, ...(mainBreadcrumb.params ?? {}) },
|
params: {
|
||||||
|
...this.snapshot.queryParams,
|
||||||
|
...(mainBreadcrumb.params ?? {}),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getCreateCustomerBreadcrumb(): Promise<Breadcrumb | undefined> {
|
async getCreateCustomerBreadcrumb(): Promise<Breadcrumb | undefined> {
|
||||||
const breadcrumbs = await this.getBreadcrumbs();
|
const breadcrumbs = await this.getBreadcrumbs();
|
||||||
return breadcrumbs.find((b) => b.tags.includes('create') && b.tags.includes('customer'));
|
return breadcrumbs.find(
|
||||||
|
(b) => b.tags.includes('create') && b.tags.includes('customer'),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async checkCreateCustomerBreadcrumb() {
|
async checkCreateCustomerBreadcrumb() {
|
||||||
@@ -262,7 +322,9 @@ export class CustomerSearchComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
async getSearchBreadcrumb(): Promise<Breadcrumb | undefined> {
|
async getSearchBreadcrumb(): Promise<Breadcrumb | undefined> {
|
||||||
const breadcrumbs = await this.getBreadcrumbs();
|
const breadcrumbs = await this.getBreadcrumbs();
|
||||||
return breadcrumbs.find((b) => b.tags.includes('list') && b.tags.includes('search'));
|
return breadcrumbs.find(
|
||||||
|
(b) => b.tags.includes('list') && b.tags.includes('search'),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async checkSearchBreadcrumb() {
|
async checkSearchBreadcrumb() {
|
||||||
@@ -288,7 +350,9 @@ export class CustomerSearchComponent implements OnInit, OnDestroy {
|
|||||||
const name = this._store.queryParams?.main_qs || 'Suche';
|
const name = this._store.queryParams?.main_qs || 'Suche';
|
||||||
|
|
||||||
if (!searchBreadcrumb) {
|
if (!searchBreadcrumb) {
|
||||||
const navigation = this._navigation.listRoute({ processId: this._store.processId });
|
const navigation = this._navigation.listRoute({
|
||||||
|
processId: this._store.processId,
|
||||||
|
});
|
||||||
const breadcrumb: Breadcrumb = {
|
const breadcrumb: Breadcrumb = {
|
||||||
key: this._store.processId,
|
key: this._store.processId,
|
||||||
tags: ['customer', 'search', 'list'],
|
tags: ['customer', 'search', 'list'],
|
||||||
@@ -300,7 +364,10 @@ export class CustomerSearchComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
this._breadcrumbService.addBreadcrumb(breadcrumb);
|
this._breadcrumbService.addBreadcrumb(breadcrumb);
|
||||||
} else {
|
} else {
|
||||||
this._breadcrumbService.patchBreadcrumb(searchBreadcrumb.id, { params: this.snapshot.queryParams, name });
|
this._breadcrumbService.patchBreadcrumb(searchBreadcrumb.id, {
|
||||||
|
params: this.snapshot.queryParams,
|
||||||
|
name,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (searchBreadcrumb) {
|
if (searchBreadcrumb) {
|
||||||
@@ -311,7 +378,9 @@ export class CustomerSearchComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
async getDetailsBreadcrumb(): Promise<Breadcrumb | undefined> {
|
async getDetailsBreadcrumb(): Promise<Breadcrumb | undefined> {
|
||||||
const breadcrumbs = await this.getBreadcrumbs();
|
const breadcrumbs = await this.getBreadcrumbs();
|
||||||
return breadcrumbs.find((b) => b.tags.includes('details') && b.tags.includes('search'));
|
return breadcrumbs.find(
|
||||||
|
(b) => b.tags.includes('details') && b.tags.includes('search'),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async checkDetailsBreadcrumb() {
|
async checkDetailsBreadcrumb() {
|
||||||
@@ -333,7 +402,8 @@ export class CustomerSearchComponent implements OnInit, OnDestroy {
|
|||||||
].includes(this.breadcrumb)
|
].includes(this.breadcrumb)
|
||||||
) {
|
) {
|
||||||
const customer = this._store.customer;
|
const customer = this._store.customer;
|
||||||
const fullName = `${customer?.firstName ?? ''} ${customer?.lastName ?? ''}`.trim();
|
const fullName =
|
||||||
|
`${customer?.firstName ?? ''} ${customer?.lastName ?? ''}`.trim();
|
||||||
|
|
||||||
if (!detailsBreadcrumb) {
|
if (!detailsBreadcrumb) {
|
||||||
const navigation = this._navigation.detailsRoute({
|
const navigation = this._navigation.detailsRoute({
|
||||||
@@ -515,7 +585,10 @@ export class CustomerSearchComponent implements OnInit, OnDestroy {
|
|||||||
async checkOrderDetailsBreadcrumb() {
|
async checkOrderDetailsBreadcrumb() {
|
||||||
const orderDetailsBreadcrumb = await this.getOrderDetailsBreadcrumb();
|
const orderDetailsBreadcrumb = await this.getOrderDetailsBreadcrumb();
|
||||||
|
|
||||||
if (this.breadcrumb === 'order-details' || this.breadcrumb === 'order-details-history') {
|
if (
|
||||||
|
this.breadcrumb === 'order-details' ||
|
||||||
|
this.breadcrumb === 'order-details-history'
|
||||||
|
) {
|
||||||
if (!orderDetailsBreadcrumb) {
|
if (!orderDetailsBreadcrumb) {
|
||||||
const navigation = this._navigation.orderDetialsRoute({
|
const navigation = this._navigation.orderDetialsRoute({
|
||||||
processId: this._store.processId,
|
processId: this._store.processId,
|
||||||
@@ -546,7 +619,8 @@ export class CustomerSearchComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async checkOrderDetailsHistoryBreadcrumb() {
|
async checkOrderDetailsHistoryBreadcrumb() {
|
||||||
const orderDetailsHistoryBreadcrumb = await this.getOrderDetailsHistoryBreadcrumb();
|
const orderDetailsHistoryBreadcrumb =
|
||||||
|
await this.getOrderDetailsHistoryBreadcrumb();
|
||||||
|
|
||||||
if (this.breadcrumb === 'order-details-history') {
|
if (this.breadcrumb === 'order-details-history') {
|
||||||
if (!orderDetailsHistoryBreadcrumb) {
|
if (!orderDetailsHistoryBreadcrumb) {
|
||||||
@@ -569,7 +643,9 @@ export class CustomerSearchComponent implements OnInit, OnDestroy {
|
|||||||
this._breadcrumbService.addBreadcrumb(breadcrumb);
|
this._breadcrumbService.addBreadcrumb(breadcrumb);
|
||||||
}
|
}
|
||||||
} else if (orderDetailsHistoryBreadcrumb) {
|
} else if (orderDetailsHistoryBreadcrumb) {
|
||||||
this._breadcrumbService.removeBreadcrumb(orderDetailsHistoryBreadcrumb.id);
|
this._breadcrumbService.removeBreadcrumb(
|
||||||
|
orderDetailsHistoryBreadcrumb.id,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
import { inject, Injectable } from '@angular/core';
|
import { inject, Injectable } from '@angular/core';
|
||||||
import { ActivatedRouteSnapshot, Params, Router, RouterStateSnapshot } from '@angular/router';
|
import {
|
||||||
|
ActivatedRouteSnapshot,
|
||||||
|
Params,
|
||||||
|
Router,
|
||||||
|
RouterStateSnapshot,
|
||||||
|
} from '@angular/router';
|
||||||
import { DomainCheckoutService } from '@domain/checkout';
|
import { DomainCheckoutService } from '@domain/checkout';
|
||||||
import { CustomerCreateFormData, decodeFormData } from '../create-customer';
|
import { CustomerCreateFormData, decodeFormData } from '../create-customer';
|
||||||
import { CustomerCreateNavigation } from '@shared/services/navigation';
|
import { CustomerCreateNavigation } from '@shared/services/navigation';
|
||||||
@@ -9,7 +14,10 @@ export class CustomerCreateGuard {
|
|||||||
private checkoutService = inject(DomainCheckoutService);
|
private checkoutService = inject(DomainCheckoutService);
|
||||||
private customerCreateNavigation = inject(CustomerCreateNavigation);
|
private customerCreateNavigation = inject(CustomerCreateNavigation);
|
||||||
|
|
||||||
async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<boolean> {
|
async canActivate(
|
||||||
|
route: ActivatedRouteSnapshot,
|
||||||
|
state: RouterStateSnapshot,
|
||||||
|
): Promise<boolean> {
|
||||||
// exit with true if canActivateChild will be called
|
// exit with true if canActivateChild will be called
|
||||||
if (route.firstChild) {
|
if (route.firstChild) {
|
||||||
return true;
|
return true;
|
||||||
@@ -19,10 +27,15 @@ export class CustomerCreateGuard {
|
|||||||
|
|
||||||
const processId = this.getProcessId(route);
|
const processId = this.getProcessId(route);
|
||||||
const formData = this.getFormData(route);
|
const formData = this.getFormData(route);
|
||||||
const canActivateCustomerType = await this.setableCustomerTypes(processId, formData);
|
const canActivateCustomerType = await this.setableCustomerTypes(
|
||||||
|
processId,
|
||||||
|
formData,
|
||||||
|
);
|
||||||
|
|
||||||
if (canActivateCustomerType[customerType] !== true) {
|
if (canActivateCustomerType[customerType] !== true) {
|
||||||
customerType = Object.keys(canActivateCustomerType).find((key) => canActivateCustomerType[key]);
|
customerType = Object.keys(canActivateCustomerType).find(
|
||||||
|
(key) => canActivateCustomerType[key],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.navigate(processId, customerType, route.queryParams);
|
await this.navigate(processId, customerType, route.queryParams);
|
||||||
@@ -30,9 +43,14 @@ export class CustomerCreateGuard {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async canActivateChild(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<boolean> {
|
async canActivateChild(
|
||||||
|
route: ActivatedRouteSnapshot,
|
||||||
|
state: RouterStateSnapshot,
|
||||||
|
): Promise<boolean> {
|
||||||
const processId = this.getProcessId(route);
|
const processId = this.getProcessId(route);
|
||||||
const customerType = route.routeConfig.path?.replace('create/', '')?.replace('/update', '');
|
const customerType = route.routeConfig.path
|
||||||
|
?.replace('create/', '')
|
||||||
|
?.replace('/update', '');
|
||||||
|
|
||||||
if (customerType === 'create-customer-main') {
|
if (customerType === 'create-customer-main') {
|
||||||
return true;
|
return true;
|
||||||
@@ -40,29 +58,39 @@ export class CustomerCreateGuard {
|
|||||||
|
|
||||||
const formData = this.getFormData(route);
|
const formData = this.getFormData(route);
|
||||||
|
|
||||||
const canActivateCustomerType = await this.setableCustomerTypes(processId, formData);
|
const canActivateCustomerType = await this.setableCustomerTypes(
|
||||||
|
processId,
|
||||||
|
formData,
|
||||||
|
);
|
||||||
|
|
||||||
if (canActivateCustomerType[customerType]) {
|
if (canActivateCustomerType[customerType]) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const activatableCustomerType = Object.keys(canActivateCustomerType)?.find((key) => canActivateCustomerType[key]);
|
const activatableCustomerType = Object.keys(canActivateCustomerType)?.find(
|
||||||
|
(key) => canActivateCustomerType[key],
|
||||||
|
);
|
||||||
|
|
||||||
await this.navigate(processId, activatableCustomerType, route.queryParams);
|
await this.navigate(processId, activatableCustomerType, route.queryParams);
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async setableCustomerTypes(processId: number, formData: CustomerCreateFormData): Promise<Record<string, boolean>> {
|
async setableCustomerTypes(
|
||||||
const res = await this.checkoutService.getSetableCustomerTypes(processId).toPromise();
|
processId: number,
|
||||||
|
formData: CustomerCreateFormData,
|
||||||
|
): Promise<Record<string, boolean>> {
|
||||||
|
const res = await this.checkoutService
|
||||||
|
.getSetableCustomerTypes(processId)
|
||||||
|
.toPromise();
|
||||||
|
|
||||||
if (res.store) {
|
// if (res.store) {
|
||||||
res['store-p4m'] = true;
|
// res['store-p4m'] = true;
|
||||||
}
|
// }
|
||||||
|
|
||||||
if (res.webshop) {
|
// if (res.webshop) {
|
||||||
res['webshop-p4m'] = true;
|
// res['webshop-p4m'] = true;
|
||||||
}
|
// }
|
||||||
|
|
||||||
if (formData?._meta) {
|
if (formData?._meta) {
|
||||||
const customerType = formData._meta.customerType;
|
const customerType = formData._meta.customerType;
|
||||||
@@ -107,7 +135,11 @@ export class CustomerCreateGuard {
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
navigate(processId: number, customerType: string, queryParams: Params): Promise<boolean> {
|
navigate(
|
||||||
|
processId: number,
|
||||||
|
customerType: string,
|
||||||
|
queryParams: Params,
|
||||||
|
): Promise<boolean> {
|
||||||
const path = this.customerCreateNavigation.createCustomerRoute({
|
const path = this.customerCreateNavigation.createCustomerRoute({
|
||||||
customerType,
|
customerType,
|
||||||
processId,
|
processId,
|
||||||
|
|||||||
@@ -31,7 +31,9 @@ export class CantAddCustomerToCartModalComponent {
|
|||||||
get option() {
|
get option() {
|
||||||
return (
|
return (
|
||||||
this.ref.data.upgradeableTo?.options.values.find((upgradeOption) =>
|
this.ref.data.upgradeableTo?.options.values.find((upgradeOption) =>
|
||||||
this.ref.data.required.options.values.some((requiredOption) => upgradeOption.key === requiredOption.key),
|
this.ref.data.required.options.values.some(
|
||||||
|
(requiredOption) => upgradeOption.key === requiredOption.key,
|
||||||
|
),
|
||||||
) || { value: this.queryParams }
|
) || { value: this.queryParams }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -39,7 +41,9 @@ export class CantAddCustomerToCartModalComponent {
|
|||||||
get queryParams() {
|
get queryParams() {
|
||||||
let option = this.ref.data.required?.options.values.find((f) => f.selected);
|
let option = this.ref.data.required?.options.values.find((f) => f.selected);
|
||||||
if (!option) {
|
if (!option) {
|
||||||
option = this.ref.data.required?.options.values.find((f) => (isBoolean(f.enabled) ? f.enabled : true));
|
option = this.ref.data.required?.options.values.find((f) =>
|
||||||
|
isBoolean(f.enabled) ? f.enabled : true,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return option ? { customertype: option.value } : {};
|
return option ? { customertype: option.value } : {};
|
||||||
}
|
}
|
||||||
@@ -57,27 +61,29 @@ export class CantAddCustomerToCartModalComponent {
|
|||||||
const queryParams: Record<string, string> = {};
|
const queryParams: Record<string, string> = {};
|
||||||
|
|
||||||
if (customer) {
|
if (customer) {
|
||||||
queryParams['formData'] = encodeFormData(mapCustomerDtoToCustomerCreateFormData(customer));
|
queryParams['formData'] = encodeFormData(
|
||||||
|
mapCustomerDtoToCustomerCreateFormData(customer),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (option === 'webshop' && attributes.some((a) => a.key === 'p4mUser')) {
|
// if (option === 'webshop' && attributes.some((a) => a.key === 'p4mUser')) {
|
||||||
const nav = this.customerCreateNavigation.createCustomerRoute({
|
// const nav = this.customerCreateNavigation.createCustomerRoute({
|
||||||
processId: this.applicationService.activatedProcessId,
|
// processId: this.applicationService.activatedProcessId,
|
||||||
customerType: 'webshop-p4m',
|
// customerType: 'webshop-p4m',
|
||||||
});
|
// });
|
||||||
this.router.navigate(nav.path, {
|
// this.router.navigate(nav.path, {
|
||||||
queryParams: { ...nav.queryParams, ...queryParams },
|
// queryParams: { ...nav.queryParams, ...queryParams },
|
||||||
});
|
// });
|
||||||
} else {
|
// } else {
|
||||||
const nav = this.customerCreateNavigation.createCustomerRoute({
|
const nav = this.customerCreateNavigation.createCustomerRoute({
|
||||||
processId: this.applicationService.activatedProcessId,
|
processId: this.applicationService.activatedProcessId,
|
||||||
customerType: option as any,
|
customerType: option as any,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.router.navigate(nav.path, {
|
this.router.navigate(nav.path, {
|
||||||
queryParams: { ...nav.queryParams, ...queryParams },
|
queryParams: { ...nav.queryParams, ...queryParams },
|
||||||
});
|
});
|
||||||
}
|
// }
|
||||||
|
|
||||||
this.ref.close();
|
this.ref.close();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
<div class="font-bold text-center border-t border-b border-solid border-disabled-customer -mx-4 py-4">
|
<div
|
||||||
|
class="font-bold text-center border-t border-b border-solid border-disabled-customer -mx-4 py-4"
|
||||||
|
>
|
||||||
{{ customer?.communicationDetails?.email }}
|
{{ customer?.communicationDetails?.email }}
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-flow-row gap-1 text-sm font-bold border-b border-solid border-disabled-customer -mx-4 py-4 px-14">
|
<div
|
||||||
|
class="grid grid-flow-row gap-1 text-sm font-bold border-b border-solid border-disabled-customer -mx-4 py-4 px-14"
|
||||||
|
>
|
||||||
@if (customer?.organisation?.name) {
|
@if (customer?.organisation?.name) {
|
||||||
<span>{{ customer?.organisation?.name }}</span>
|
<span>{{ customer?.organisation?.name }}</span>
|
||||||
}
|
}
|
||||||
@@ -16,23 +20,26 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-flow-col gap-4 justify-around mt-12">
|
<div class="grid grid-flow-col gap-4 justify-around mt-12">
|
||||||
<button class="border-2 border-solid border-brand rounded-full font-bold text-brand px-6 py-3 text-lg" (click)="close(false)">
|
<button
|
||||||
|
class="border-2 border-solid border-brand rounded-full font-bold text-brand px-6 py-3 text-lg"
|
||||||
|
(click)="close(false)"
|
||||||
|
>
|
||||||
neues Onlinekonto anlegen
|
neues Onlinekonto anlegen
|
||||||
</button>
|
</button>
|
||||||
@if (!isWebshopWithP4M) {
|
@if (!isWebshopWithP4M) {
|
||||||
<button
|
<button
|
||||||
class="border-2 border-solid border-brand rounded-full font-bold text-white px-6 py-3 text-lg bg-brand"
|
class="border-2 border-solid border-brand rounded-full font-bold text-white px-6 py-3 text-lg bg-brand"
|
||||||
(click)="close(true)"
|
(click)="close(true)"
|
||||||
>
|
>
|
||||||
Daten übernehmen
|
Daten übernehmen
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
@if (isWebshopWithP4M) {
|
<!-- @if (isWebshopWithP4M) {
|
||||||
<button
|
<button
|
||||||
class="border-2 border-solid border-brand rounded-full font-bold text-white px-6 py-3 text-lg bg-brand"
|
class="border-2 border-solid border-brand rounded-full font-bold text-white px-6 py-3 text-lg bg-brand"
|
||||||
(click)="selectCustomer()"
|
(click)="selectCustomer()"
|
||||||
>
|
>
|
||||||
Datensatz auswählen
|
Datensatz auswählen
|
||||||
</button>
|
</button>
|
||||||
}
|
} -->
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -9,11 +9,11 @@ import { CustomerCreateGuard } from './guards/customer-create.guard';
|
|||||||
import {
|
import {
|
||||||
CreateB2BCustomerComponent,
|
CreateB2BCustomerComponent,
|
||||||
CreateGuestCustomerComponent,
|
CreateGuestCustomerComponent,
|
||||||
CreateP4MCustomerComponent,
|
// CreateP4MCustomerComponent,
|
||||||
CreateStoreCustomerComponent,
|
CreateStoreCustomerComponent,
|
||||||
CreateWebshopCustomerComponent,
|
CreateWebshopCustomerComponent,
|
||||||
} from './create-customer';
|
} from './create-customer';
|
||||||
import { UpdateP4MWebshopCustomerComponent } from './create-customer/update-p4m-webshop-customer';
|
// import { UpdateP4MWebshopCustomerComponent } from './create-customer/update-p4m-webshop-customer';
|
||||||
import { CreateCustomerComponent } from './create-customer/create-customer.component';
|
import { CreateCustomerComponent } from './create-customer/create-customer.component';
|
||||||
import { CustomerDataEditB2BComponent } from './customer-search/edit-main-view/customer-data-edit-b2b.component';
|
import { CustomerDataEditB2BComponent } from './customer-search/edit-main-view/customer-data-edit-b2b.component';
|
||||||
import { CustomerDataEditB2CComponent } from './customer-search/edit-main-view/customer-data-edit-b2c.component';
|
import { CustomerDataEditB2CComponent } from './customer-search/edit-main-view/customer-data-edit-b2c.component';
|
||||||
@@ -40,8 +40,16 @@ export const routes: Routes = [
|
|||||||
path: '',
|
path: '',
|
||||||
component: CustomerSearchComponent,
|
component: CustomerSearchComponent,
|
||||||
children: [
|
children: [
|
||||||
{ path: 'search', component: CustomerMainViewComponent, data: { side: 'main', breadcrumb: 'main' } },
|
{
|
||||||
{ path: 'search/list', component: CustomerResultsMainViewComponent, data: { breadcrumb: 'search' } },
|
path: 'search',
|
||||||
|
component: CustomerMainViewComponent,
|
||||||
|
data: { side: 'main', breadcrumb: 'main' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'search/list',
|
||||||
|
component: CustomerResultsMainViewComponent,
|
||||||
|
data: { breadcrumb: 'search' },
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'search/filter',
|
path: 'search/filter',
|
||||||
component: CustomerFilterMainViewComponent,
|
component: CustomerFilterMainViewComponent,
|
||||||
@@ -80,7 +88,10 @@ export const routes: Routes = [
|
|||||||
{
|
{
|
||||||
path: 'search/:customerId/orders/:orderId/:orderItemId/history',
|
path: 'search/:customerId/orders/:orderId/:orderItemId/history',
|
||||||
component: CustomerOrderDetailsHistoryMainViewComponent,
|
component: CustomerOrderDetailsHistoryMainViewComponent,
|
||||||
data: { side: 'order-details', breadcrumb: 'order-details-history' },
|
data: {
|
||||||
|
side: 'order-details',
|
||||||
|
breadcrumb: 'order-details-history',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'search/:customerId/edit/b2b',
|
path: 'search/:customerId/edit/b2b',
|
||||||
@@ -140,13 +151,13 @@ export const routes: Routes = [
|
|||||||
{ path: 'create/webshop', component: CreateWebshopCustomerComponent },
|
{ path: 'create/webshop', component: CreateWebshopCustomerComponent },
|
||||||
{ path: 'create/b2b', component: CreateB2BCustomerComponent },
|
{ path: 'create/b2b', component: CreateB2BCustomerComponent },
|
||||||
{ path: 'create/guest', component: CreateGuestCustomerComponent },
|
{ path: 'create/guest', component: CreateGuestCustomerComponent },
|
||||||
{ path: 'create/webshop-p4m', component: CreateP4MCustomerComponent, data: { customerType: 'webshop' } },
|
// { path: 'create/webshop-p4m', component: CreateP4MCustomerComponent, data: { customerType: 'webshop' } },
|
||||||
{ path: 'create/store-p4m', component: CreateP4MCustomerComponent, data: { customerType: 'store' } },
|
// { path: 'create/store-p4m', component: CreateP4MCustomerComponent, data: { customerType: 'store' } },
|
||||||
{
|
// {
|
||||||
path: 'create/webshop-p4m/update',
|
// path: 'create/webshop-p4m/update',
|
||||||
component: UpdateP4MWebshopCustomerComponent,
|
// component: UpdateP4MWebshopCustomerComponent,
|
||||||
data: { customerType: 'webshop' },
|
// data: { customerType: 'webshop' },
|
||||||
},
|
// },
|
||||||
{
|
{
|
||||||
path: 'create-customer-main',
|
path: 'create-customer-main',
|
||||||
outlet: 'side',
|
outlet: 'side',
|
||||||
|
|||||||
@@ -1,254 +1,254 @@
|
|||||||
import {
|
import {
|
||||||
ChangeDetectionStrategy,
|
ChangeDetectionStrategy,
|
||||||
Component,
|
Component,
|
||||||
OnDestroy,
|
OnDestroy,
|
||||||
OnInit,
|
OnInit,
|
||||||
ViewChild,
|
ViewChild,
|
||||||
inject,
|
inject,
|
||||||
linkedSignal,
|
linkedSignal,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { BreadcrumbService } from '@core/breadcrumb';
|
import { BreadcrumbService } from '@core/breadcrumb';
|
||||||
import {
|
import {
|
||||||
KeyValueDTOOfStringAndString,
|
KeyValueDTOOfStringAndString,
|
||||||
OrderItemListItemDTO,
|
OrderItemListItemDTO,
|
||||||
} from '@generated/swagger/oms-api';
|
} from '@generated/swagger/oms-api';
|
||||||
import { UiErrorModalComponent, UiModalService } from '@ui/modal';
|
import { UiErrorModalComponent, UiModalService } from '@ui/modal';
|
||||||
import { UiScrollContainerComponent } from '@ui/scroll-container';
|
import { UiScrollContainerComponent } from '@ui/scroll-container';
|
||||||
import { BehaviorSubject, combineLatest, Subject } from 'rxjs';
|
import { BehaviorSubject, combineLatest, Subject } from 'rxjs';
|
||||||
import { first, map, shareReplay, takeUntil } from 'rxjs/operators';
|
import { first, map, shareReplay, takeUntil } from 'rxjs/operators';
|
||||||
import { GoodsInRemissionPreviewStore } from './goods-in-remission-preview.store';
|
import { GoodsInRemissionPreviewStore } from './goods-in-remission-preview.store';
|
||||||
import { Config } from '@core/config';
|
import { Config } from '@core/config';
|
||||||
import { ToasterService } from '@shared/shell';
|
import { ToasterService } from '@shared/shell';
|
||||||
import { PickupShelfInNavigationService } from '@shared/services/navigation';
|
import { PickupShelfInNavigationService } from '@shared/services/navigation';
|
||||||
import { CacheService } from '@core/cache';
|
import { CacheService } from '@core/cache';
|
||||||
import { TabService } from '@isa/core/tabs';
|
import { TabService } from '@isa/core/tabs';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'page-goods-in-remission-preview',
|
selector: 'page-goods-in-remission-preview',
|
||||||
templateUrl: 'goods-in-remission-preview.component.html',
|
templateUrl: 'goods-in-remission-preview.component.html',
|
||||||
styleUrls: ['goods-in-remission-preview.component.scss'],
|
styleUrls: ['goods-in-remission-preview.component.scss'],
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
providers: [GoodsInRemissionPreviewStore],
|
providers: [GoodsInRemissionPreviewStore],
|
||||||
standalone: false,
|
standalone: false,
|
||||||
})
|
})
|
||||||
export class GoodsInRemissionPreviewComponent implements OnInit, OnDestroy {
|
export class GoodsInRemissionPreviewComponent implements OnInit, OnDestroy {
|
||||||
tabService = inject(TabService);
|
tabService = inject(TabService);
|
||||||
private _pickupShelfInNavigationService = inject(
|
private _pickupShelfInNavigationService = inject(
|
||||||
PickupShelfInNavigationService,
|
PickupShelfInNavigationService,
|
||||||
);
|
);
|
||||||
@ViewChild(UiScrollContainerComponent)
|
@ViewChild(UiScrollContainerComponent)
|
||||||
scrollContainer: UiScrollContainerComponent;
|
scrollContainer: UiScrollContainerComponent;
|
||||||
|
|
||||||
items$ = this._store.results$;
|
items$ = this._store.results$;
|
||||||
|
|
||||||
itemLength$ = this.items$.pipe(map((items) => items?.length));
|
itemLength$ = this.items$.pipe(map((items) => items?.length));
|
||||||
|
|
||||||
hits$ = this._store.hits$;
|
hits$ = this._store.hits$;
|
||||||
|
|
||||||
loading$ = this._store.fetching$.pipe(shareReplay());
|
loading$ = this._store.fetching$.pipe(shareReplay());
|
||||||
|
|
||||||
changeActionLoader$ = new BehaviorSubject<boolean>(false);
|
changeActionLoader$ = new BehaviorSubject<boolean>(false);
|
||||||
|
|
||||||
listEmpty$ = combineLatest([this.loading$, this.hits$]).pipe(
|
listEmpty$ = combineLatest([this.loading$, this.hits$]).pipe(
|
||||||
map(([loading, hits]) => !loading && hits === 0),
|
map(([loading, hits]) => !loading && hits === 0),
|
||||||
shareReplay(),
|
shareReplay(),
|
||||||
);
|
);
|
||||||
|
|
||||||
actions$ = this.items$.pipe(map((items) => items[0]?.actions));
|
actions$ = this.items$.pipe(map((items) => items[0]?.actions));
|
||||||
|
|
||||||
private _onDestroy$ = new Subject<void>();
|
private _onDestroy$ = new Subject<void>();
|
||||||
|
|
||||||
byBuyerNumberFn = (item: OrderItemListItemDTO) => item.buyerNumber;
|
byBuyerNumberFn = (item: OrderItemListItemDTO) => item.buyerNumber;
|
||||||
|
|
||||||
byOrderNumberFn = (item: OrderItemListItemDTO) => item.orderNumber;
|
byOrderNumberFn = (item: OrderItemListItemDTO) => item.orderNumber;
|
||||||
|
|
||||||
byProcessingStatusFn = (item: OrderItemListItemDTO) => item.processingStatus;
|
byProcessingStatusFn = (item: OrderItemListItemDTO) => item.processingStatus;
|
||||||
|
|
||||||
byCompartmentCodeFn = (item: OrderItemListItemDTO) =>
|
byCompartmentCodeFn = (item: OrderItemListItemDTO) =>
|
||||||
item.compartmentInfo
|
item.compartmentInfo
|
||||||
? `${item.compartmentCode}_${item.compartmentInfo}`
|
? `${item.compartmentCode}_${item.compartmentInfo}`
|
||||||
: item.compartmentCode;
|
: item.compartmentCode;
|
||||||
|
|
||||||
private readonly SCROLL_POSITION_TOKEN = 'REMISSION_PREVIEW_SCROLL_POSITION';
|
private readonly SCROLL_POSITION_TOKEN = 'REMISSION_PREVIEW_SCROLL_POSITION';
|
||||||
|
|
||||||
remissionPath = linkedSignal(() => [
|
remissionPath = linkedSignal(() => [
|
||||||
'/',
|
'/',
|
||||||
this.tabService.activatedTab()?.id || this.tabService.nextId(),
|
this.tabService.activatedTab()?.id || Date.now(),
|
||||||
'remission',
|
'remission',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private _breadcrumb: BreadcrumbService,
|
private _breadcrumb: BreadcrumbService,
|
||||||
private _store: GoodsInRemissionPreviewStore,
|
private _store: GoodsInRemissionPreviewStore,
|
||||||
private _router: Router,
|
private _router: Router,
|
||||||
private _modal: UiModalService,
|
private _modal: UiModalService,
|
||||||
private _config: Config,
|
private _config: Config,
|
||||||
private _toast: ToasterService,
|
private _toast: ToasterService,
|
||||||
private _cache: CacheService,
|
private _cache: CacheService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.initInitialSearch();
|
this.initInitialSearch();
|
||||||
this.createBreadcrumb();
|
this.createBreadcrumb();
|
||||||
this.removeBreadcrumbs();
|
this.removeBreadcrumbs();
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
this._onDestroy$.next();
|
this._onDestroy$.next();
|
||||||
this._onDestroy$.complete();
|
this._onDestroy$.complete();
|
||||||
this._addScrollPositionToCache();
|
this._addScrollPositionToCache();
|
||||||
this.updateBreadcrumb();
|
this.updateBreadcrumb();
|
||||||
}
|
}
|
||||||
|
|
||||||
private _removeScrollPositionFromCache(): void {
|
private _removeScrollPositionFromCache(): void {
|
||||||
this._cache.delete({
|
this._cache.delete({
|
||||||
processId: this._config.get('process.ids.goodsIn'),
|
processId: this._config.get('process.ids.goodsIn'),
|
||||||
token: this.SCROLL_POSITION_TOKEN,
|
token: this.SCROLL_POSITION_TOKEN,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private _addScrollPositionToCache(): void {
|
private _addScrollPositionToCache(): void {
|
||||||
this._cache.set<number>(
|
this._cache.set<number>(
|
||||||
{
|
{
|
||||||
processId: this._config.get('process.ids.goodsIn'),
|
processId: this._config.get('process.ids.goodsIn'),
|
||||||
token: this.SCROLL_POSITION_TOKEN,
|
token: this.SCROLL_POSITION_TOKEN,
|
||||||
},
|
},
|
||||||
this.scrollContainer?.scrollPos,
|
this.scrollContainer?.scrollPos,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _getScrollPositionFromCache(): Promise<number> {
|
private async _getScrollPositionFromCache(): Promise<number> {
|
||||||
return await this._cache.get<number>({
|
return await this._cache.get<number>({
|
||||||
processId: this._config.get('process.ids.goodsIn'),
|
processId: this._config.get('process.ids.goodsIn'),
|
||||||
token: this.SCROLL_POSITION_TOKEN,
|
token: this.SCROLL_POSITION_TOKEN,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async createBreadcrumb() {
|
async createBreadcrumb() {
|
||||||
await this._breadcrumb.addOrUpdateBreadcrumbIfNotExists({
|
await this._breadcrumb.addOrUpdateBreadcrumbIfNotExists({
|
||||||
key: this._config.get('process.ids.goodsIn'),
|
key: this._config.get('process.ids.goodsIn'),
|
||||||
name: 'Abholfachremissionsvorschau',
|
name: 'Abholfachremissionsvorschau',
|
||||||
path: '/filiale/goods/in/preview',
|
path: '/filiale/goods/in/preview',
|
||||||
section: 'branch',
|
section: 'branch',
|
||||||
params: { view: 'remission' },
|
params: { view: 'remission' },
|
||||||
tags: ['goods-in', 'preview'],
|
tags: ['goods-in', 'preview'],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateBreadcrumb() {
|
async updateBreadcrumb() {
|
||||||
const crumbs = await this._breadcrumb
|
const crumbs = await this._breadcrumb
|
||||||
.getBreadcrumbsByKeyAndTags$(this._config.get('process.ids.goodsIn'), [
|
.getBreadcrumbsByKeyAndTags$(this._config.get('process.ids.goodsIn'), [
|
||||||
'goods-in',
|
'goods-in',
|
||||||
'preview',
|
'preview',
|
||||||
])
|
])
|
||||||
.pipe(first())
|
.pipe(first())
|
||||||
.toPromise();
|
.toPromise();
|
||||||
for (const crumb of crumbs) {
|
for (const crumb of crumbs) {
|
||||||
this._breadcrumb.patchBreadcrumb(crumb.id, {
|
this._breadcrumb.patchBreadcrumb(crumb.id, {
|
||||||
name: crumb.name,
|
name: crumb.name,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async removeBreadcrumbs() {
|
async removeBreadcrumbs() {
|
||||||
let breadcrumbsToDelete = await this._breadcrumb
|
let breadcrumbsToDelete = await this._breadcrumb
|
||||||
.getBreadcrumbsByKeyAndTags$(this._config.get('process.ids.goodsIn'), [
|
.getBreadcrumbsByKeyAndTags$(this._config.get('process.ids.goodsIn'), [
|
||||||
'goods-in',
|
'goods-in',
|
||||||
])
|
])
|
||||||
.pipe(first())
|
.pipe(first())
|
||||||
.toPromise();
|
.toPromise();
|
||||||
|
|
||||||
breadcrumbsToDelete = breadcrumbsToDelete.filter(
|
breadcrumbsToDelete = breadcrumbsToDelete.filter(
|
||||||
(crumb) =>
|
(crumb) =>
|
||||||
!crumb.tags.includes('preview') && !crumb.tags.includes('main'),
|
!crumb.tags.includes('preview') && !crumb.tags.includes('main'),
|
||||||
);
|
);
|
||||||
|
|
||||||
breadcrumbsToDelete.forEach((crumb) => {
|
breadcrumbsToDelete.forEach((crumb) => {
|
||||||
this._breadcrumb.removeBreadcrumb(crumb.id, true);
|
this._breadcrumb.removeBreadcrumb(crumb.id, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
const detailsCrumbs = await this._breadcrumb
|
const detailsCrumbs = await this._breadcrumb
|
||||||
.getBreadcrumbsByKeyAndTags$(this._config.get('process.ids.goodsIn'), [
|
.getBreadcrumbsByKeyAndTags$(this._config.get('process.ids.goodsIn'), [
|
||||||
'goods-in',
|
'goods-in',
|
||||||
'details',
|
'details',
|
||||||
])
|
])
|
||||||
.pipe(first())
|
.pipe(first())
|
||||||
.toPromise();
|
.toPromise();
|
||||||
const editCrumbs = await this._breadcrumb
|
const editCrumbs = await this._breadcrumb
|
||||||
.getBreadcrumbsByKeyAndTags$(this._config.get('process.ids.goodsIn'), [
|
.getBreadcrumbsByKeyAndTags$(this._config.get('process.ids.goodsIn'), [
|
||||||
'goods-in',
|
'goods-in',
|
||||||
'edit',
|
'edit',
|
||||||
])
|
])
|
||||||
.pipe(first())
|
.pipe(first())
|
||||||
.toPromise();
|
.toPromise();
|
||||||
|
|
||||||
detailsCrumbs.forEach((crumb) => {
|
detailsCrumbs.forEach((crumb) => {
|
||||||
this._breadcrumb.removeBreadcrumb(crumb.id, true);
|
this._breadcrumb.removeBreadcrumb(crumb.id, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
editCrumbs.forEach((crumb) => {
|
editCrumbs.forEach((crumb) => {
|
||||||
this._breadcrumb.removeBreadcrumb(crumb.id, true);
|
this._breadcrumb.removeBreadcrumb(crumb.id, true);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
initInitialSearch() {
|
initInitialSearch() {
|
||||||
if (this._store.hits === 0) {
|
if (this._store.hits === 0) {
|
||||||
this._store.searchResult$
|
this._store.searchResult$
|
||||||
.pipe(takeUntil(this._onDestroy$))
|
.pipe(takeUntil(this._onDestroy$))
|
||||||
.subscribe(async (result) => {
|
.subscribe(async (result) => {
|
||||||
await this.createBreadcrumb();
|
await this.createBreadcrumb();
|
||||||
|
|
||||||
this.scrollContainer?.scrollTo(
|
this.scrollContainer?.scrollTo(
|
||||||
(await this._getScrollPositionFromCache()) ?? 0,
|
(await this._getScrollPositionFromCache()) ?? 0,
|
||||||
);
|
);
|
||||||
this._removeScrollPositionFromCache();
|
this._removeScrollPositionFromCache();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
this._store.search();
|
this._store.search();
|
||||||
}
|
}
|
||||||
|
|
||||||
async navigateToRemission() {
|
async navigateToRemission() {
|
||||||
await this._router.navigate(this.remissionPath());
|
await this._router.navigate(this.remissionPath());
|
||||||
}
|
}
|
||||||
|
|
||||||
navigateToDetails(orderItem: OrderItemListItemDTO) {
|
navigateToDetails(orderItem: OrderItemListItemDTO) {
|
||||||
const nav = this._pickupShelfInNavigationService.detailRoute({
|
const nav = this._pickupShelfInNavigationService.detailRoute({
|
||||||
item: orderItem,
|
item: orderItem,
|
||||||
side: false,
|
side: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
this._router.navigate(nav.path, {
|
this._router.navigate(nav.path, {
|
||||||
queryParams: { ...nav.queryParams, view: 'remission' },
|
queryParams: { ...nav.queryParams, view: 'remission' },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleAction(action: KeyValueDTOOfStringAndString) {
|
async handleAction(action: KeyValueDTOOfStringAndString) {
|
||||||
this.changeActionLoader$.next(true);
|
this.changeActionLoader$.next(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await this._store
|
const response = await this._store
|
||||||
.createRemissionFromPreview()
|
.createRemissionFromPreview()
|
||||||
.pipe(first())
|
.pipe(first())
|
||||||
.toPromise();
|
.toPromise();
|
||||||
|
|
||||||
if (!response?.dialog) {
|
if (!response?.dialog) {
|
||||||
this._toast.open({
|
this._toast.open({
|
||||||
title: 'Abholfachremission',
|
title: 'Abholfachremission',
|
||||||
message: response?.message,
|
message: response?.message,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.navigateToRemission();
|
await this.navigateToRemission();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this._modal.open({
|
this._modal.open({
|
||||||
content: UiErrorModalComponent,
|
content: UiErrorModalComponent,
|
||||||
data: error,
|
data: error,
|
||||||
});
|
});
|
||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.changeActionLoader$.next(false);
|
this.changeActionLoader$.next(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,39 +1,60 @@
|
|||||||
<div class="shared-branch-selector-input-container" (click)="branchInput.focus(); openComplete()">
|
<div
|
||||||
<button (click)="onClose($event)" type="button" class="shared-branch-selector-input-icon p-2">
|
class="shared-branch-selector-input-container"
|
||||||
<shared-icon class="text-[#AEB7C1]" icon="magnify" [size]="32"></shared-icon>
|
(click)="branchInput.focus(); openComplete()"
|
||||||
</button>
|
>
|
||||||
<input
|
<button
|
||||||
#branchInput
|
(click)="onClose($event)"
|
||||||
class="shared-branch-selector-input"
|
type="button"
|
||||||
[class.shared-branch-selector-opend]="autocompleteComponent?.opend"
|
class="shared-branch-selector-input-icon p-2"
|
||||||
uiInput
|
>
|
||||||
type="text"
|
<shared-icon
|
||||||
[placeholder]="placeholder"
|
class="text-[#AEB7C1]"
|
||||||
[ngModel]="query$ | async"
|
icon="magnify"
|
||||||
(ngModelChange)="onQueryChange($event)"
|
[size]="32"
|
||||||
(keyup)="onKeyup($event)"
|
></shared-icon>
|
||||||
/>
|
</button>
|
||||||
@if ((query$ | async)?.length > 0) {
|
<input
|
||||||
<button class="shared-branch-selector-clear-input-icon pr-2" type="button" (click)="clear()">
|
#branchInput
|
||||||
<shared-icon class="text-[#1F466C]" icon="close" [size]="32"></shared-icon>
|
class="shared-branch-selector-input"
|
||||||
</button>
|
[class.shared-branch-selector-opend]="autocompleteComponent?.opend"
|
||||||
}
|
uiInput
|
||||||
</div>
|
type="text"
|
||||||
<ui-autocomplete class="shared-branch-selector-autocomplete z-modal w-full">
|
[placeholder]="placeholder"
|
||||||
@if (autocompleteComponent?.opend) {
|
[ngModel]="query$ | async"
|
||||||
<hr class="ml-3 text-[#9CB1C6]" uiAutocompleteSeparator />
|
(ngModelChange)="onQueryChange($event)"
|
||||||
}
|
(keyup)="onKeyup($event)"
|
||||||
@if ((filteredBranches$ | async)?.length > 0) {
|
/>
|
||||||
<p class="text-p2 p-4 font-normal" uiAutocompleteLabel>Filialvorschläge</p>
|
@if ((query$ | async)?.length > 0) {
|
||||||
}
|
<button
|
||||||
@for (branch of filteredBranches$ | async; track branch) {
|
class="shared-branch-selector-clear-input-icon pr-2"
|
||||||
<button
|
type="button"
|
||||||
class="shared-branch-selector-autocomplete-option min-h-[44px]"
|
(click)="clear()"
|
||||||
[class.shared-branch-selector-selected]="value && value.id === branch.id"
|
>
|
||||||
(click)="setBranch(branch)"
|
<shared-icon
|
||||||
[uiAutocompleteItem]="branch"
|
class="text-[#1F466C]"
|
||||||
>
|
icon="close"
|
||||||
<span class="text-lg font-semibold">{{ store.formatBranch(branch) }}</span>
|
[size]="32"
|
||||||
</button>
|
></shared-icon>
|
||||||
}
|
</button>
|
||||||
</ui-autocomplete>
|
}
|
||||||
|
</div>
|
||||||
|
<ui-autocomplete class="shared-branch-selector-autocomplete z-modal w-full">
|
||||||
|
@if (autocompleteComponent?.opend) {
|
||||||
|
<hr class="ml-3 text-[#9CB1C6]" uiAutocompleteSeparator />
|
||||||
|
}
|
||||||
|
@if ((filteredBranches$ | async)?.length > 0) {
|
||||||
|
<p class="text-p2 p-4 font-normal" uiAutocompleteLabel>Filialvorschläge</p>
|
||||||
|
}
|
||||||
|
@for (branch of filteredBranches$ | async; track branch) {
|
||||||
|
<button
|
||||||
|
class="shared-branch-selector-autocomplete-option min-h-[44px]"
|
||||||
|
[class.shared-branch-selector-selected]="value && value.id === branch.id"
|
||||||
|
(click)="setBranch(branch)"
|
||||||
|
[uiAutocompleteItem]="branch"
|
||||||
|
>
|
||||||
|
<span class="text-lg font-semibold">{{
|
||||||
|
store.formatBranch(branch)
|
||||||
|
}}</span>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</ui-autocomplete>
|
||||||
|
|||||||
@@ -3,7 +3,10 @@ import { Injectable } from '@angular/core';
|
|||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { CustomerInfoDTO } from '@generated/swagger/crm-api';
|
import { CustomerInfoDTO } from '@generated/swagger/crm-api';
|
||||||
import { NavigationRoute } from './defs/navigation-route';
|
import { NavigationRoute } from './defs/navigation-route';
|
||||||
import { encodeFormData, mapCustomerInfoDtoToCustomerCreateFormData } from 'apps/isa-app/src/page/customer';
|
import {
|
||||||
|
encodeFormData,
|
||||||
|
mapCustomerInfoDtoToCustomerCreateFormData,
|
||||||
|
} from 'apps/isa-app/src/page/customer';
|
||||||
|
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class CustomerCreateNavigation {
|
export class CustomerCreateNavigation {
|
||||||
@@ -33,7 +36,9 @@ export class CustomerCreateNavigation {
|
|||||||
|
|
||||||
navigateToDefault(params: { processId: NumberInput }): Promise<boolean> {
|
navigateToDefault(params: { processId: NumberInput }): Promise<boolean> {
|
||||||
const route = this.defaultRoute(params);
|
const route = this.defaultRoute(params);
|
||||||
return this._router.navigate(route.path, { queryParams: route.queryParams });
|
return this._router.navigate(route.path, {
|
||||||
|
queryParams: route.queryParams,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
createCustomerRoute(params: {
|
createCustomerRoute(params: {
|
||||||
@@ -54,7 +59,9 @@ export class CustomerCreateNavigation {
|
|||||||
];
|
];
|
||||||
|
|
||||||
let formData = params?.customerInfo
|
let formData = params?.customerInfo
|
||||||
? encodeFormData(mapCustomerInfoDtoToCustomerCreateFormData(params.customerInfo))
|
? encodeFormData(
|
||||||
|
mapCustomerInfoDtoToCustomerCreateFormData(params.customerInfo),
|
||||||
|
)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const urlTree = this._router.createUrlTree(path, {
|
const urlTree = this._router.createUrlTree(path, {
|
||||||
@@ -79,7 +86,9 @@ export class CustomerCreateNavigation {
|
|||||||
processId: NumberInput;
|
processId: NumberInput;
|
||||||
customerInfo: CustomerInfoDTO;
|
customerInfo: CustomerInfoDTO;
|
||||||
}): NavigationRoute {
|
}): NavigationRoute {
|
||||||
const formData = encodeFormData(mapCustomerInfoDtoToCustomerCreateFormData(customerInfo));
|
const formData = encodeFormData(
|
||||||
|
mapCustomerInfoDtoToCustomerCreateFormData(customerInfo),
|
||||||
|
);
|
||||||
const path = [
|
const path = [
|
||||||
'/kunde',
|
'/kunde',
|
||||||
coerceNumberProperty(processId),
|
coerceNumberProperty(processId),
|
||||||
@@ -88,14 +97,16 @@ export class CustomerCreateNavigation {
|
|||||||
outlets: {
|
outlets: {
|
||||||
primary: [
|
primary: [
|
||||||
'create',
|
'create',
|
||||||
customerInfo?.features?.find((feature) => feature.key === 'webshop') ? 'webshop-p4m' : 'store-p4m',
|
// customerInfo?.features?.find((feature) => feature.key === 'webshop') ? 'webshop-p4m' : 'store-p4m',
|
||||||
],
|
],
|
||||||
side: 'create-customer-main',
|
side: 'create-customer-main',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const urlTree = this._router.createUrlTree(path, { queryParams: { formData } });
|
const urlTree = this._router.createUrlTree(path, {
|
||||||
|
queryParams: { formData },
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
path,
|
path,
|
||||||
|
|||||||
@@ -1,31 +1,38 @@
|
|||||||
<div
|
@if (process(); as p) {
|
||||||
class="tab-wrapper flex flex-row items-center justify-between border-b-[0.188rem] border-solid h-14"
|
<div
|
||||||
[class.border-surface]="!(isActive$ | async)"
|
class="tab-wrapper flex flex-row items-center justify-between border-b-[0.188rem] border-solid h-14"
|
||||||
[class.border-brand]="isActive$ | async"
|
[class.border-surface]="!(isActive$ | async)"
|
||||||
>
|
[class.border-brand]="isActive$ | async"
|
||||||
<a
|
>
|
||||||
class="tab-link font-bold flex flex-row justify-center items-center whitespace-nowrap px-4 truncate max-w-[15rem] h-14"
|
<a
|
||||||
[routerLink]="routerLink$ | async"
|
class="tab-link font-bold flex flex-row justify-center items-center whitespace-nowrap px-4 truncate max-w-[15rem] h-14"
|
||||||
[queryParams]="queryParams$ | async"
|
[href]="currentLocationUrlTree()?.toString()"
|
||||||
(click)="scrollIntoView()"
|
(click)="navigateByUrl($event); scrollIntoView()"
|
||||||
>
|
>
|
||||||
<span class="truncate">
|
<span class="truncate">
|
||||||
{{ process?.name }}
|
{{ p.name }}
|
||||||
</span>
|
</span>
|
||||||
@if (process?.type !== 'cart-checkout') {
|
@if (p.type === 'cart') {
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="rounded-full px-3 h-[2.375rem] font-bold text-p1 flex flex-row items-center justify-between shopping-cart-count ml-4"
|
class="rounded-full px-3 h-[2.375rem] font-bold text-p1 flex flex-row items-center justify-between shopping-cart-count ml-4"
|
||||||
[class.active]="isActive$ | async"
|
[class.active]="isActive$ | async"
|
||||||
[routerLink]="getCheckoutPath((process$ | async)?.id)"
|
[routerLink]="getCheckoutPath((process$ | async)?.id)"
|
||||||
(click)="$event?.preventDefault(); $event?.stopPropagation()"
|
(click)="$event?.preventDefault(); $event?.stopPropagation()"
|
||||||
>
|
>
|
||||||
<shared-icon icon="shopping-cart-bold" [size]="22"></shared-icon>
|
<shared-icon icon="shopping-cart-bold" [size]="22"></shared-icon>
|
||||||
<span class="shopping-cart-count-label ml-2">{{ cartItemCount$ | async }}</span>
|
<span class="shopping-cart-count-label ml-2">{{
|
||||||
</button>
|
cartItemCount$ | async
|
||||||
}
|
}}</span>
|
||||||
</a>
|
</button>
|
||||||
<button type="button" class="tab-close-btn -ml-4 h-12 w-12 grid justify-center items-center" (click)="close()">
|
}
|
||||||
<shared-icon icon="close" [size]="28"></shared-icon>
|
</a>
|
||||||
</button>
|
<button
|
||||||
</div>
|
type="button"
|
||||||
|
class="tab-close-btn -ml-4 h-12 w-12 grid justify-center items-center"
|
||||||
|
(click)="close()"
|
||||||
|
>
|
||||||
|
<shared-icon icon="close" [size]="28"></shared-icon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,156 +1,208 @@
|
|||||||
import {
|
import {
|
||||||
Component,
|
Component,
|
||||||
ChangeDetectionStrategy,
|
ChangeDetectionStrategy,
|
||||||
Input,
|
OnDestroy,
|
||||||
OnDestroy,
|
OnInit,
|
||||||
OnInit,
|
OnChanges,
|
||||||
OnChanges,
|
SimpleChanges,
|
||||||
SimpleChanges,
|
EventEmitter,
|
||||||
EventEmitter,
|
Output,
|
||||||
Output,
|
ElementRef,
|
||||||
ElementRef,
|
inject,
|
||||||
} from '@angular/core';
|
computed,
|
||||||
import { Router } from '@angular/router';
|
input,
|
||||||
import { ApplicationProcess, ApplicationService } from '@core/application';
|
effect,
|
||||||
import { Breadcrumb, BreadcrumbService } from '@core/breadcrumb';
|
} from '@angular/core';
|
||||||
import { DomainCheckoutService } from '@domain/checkout';
|
import { Router } from '@angular/router';
|
||||||
import { CheckoutNavigationService } from '@shared/services/navigation';
|
import { ApplicationProcess, ApplicationService } from '@core/application';
|
||||||
import { BehaviorSubject, NEVER, Observable, combineLatest, isObservable } from 'rxjs';
|
import { Breadcrumb } from '@core/breadcrumb';
|
||||||
import { first, map, switchMap, tap } from 'rxjs/operators';
|
import { DomainCheckoutService } from '@domain/checkout';
|
||||||
|
import { CheckoutNavigationService } from '@shared/services/navigation';
|
||||||
@Component({
|
import {
|
||||||
selector: 'shell-process-bar-item',
|
BehaviorSubject,
|
||||||
templateUrl: 'process-bar-item.component.html',
|
NEVER,
|
||||||
styleUrls: ['process-bar-item.component.css'],
|
Observable,
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
combineLatest,
|
||||||
standalone: false,
|
isObservable,
|
||||||
})
|
} from 'rxjs';
|
||||||
export class ShellProcessBarItemComponent implements OnInit, OnDestroy, OnChanges {
|
import { map, switchMap, tap } from 'rxjs/operators';
|
||||||
private _process$ = new BehaviorSubject<ApplicationProcess>(undefined);
|
import { TabService } from '@isa/core/tabs';
|
||||||
|
|
||||||
process$ = this._process$.asObservable();
|
@Component({
|
||||||
|
selector: 'shell-process-bar-item',
|
||||||
@Input()
|
templateUrl: 'process-bar-item.component.html',
|
||||||
process: ApplicationProcess;
|
styleUrls: ['process-bar-item.component.css'],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
@Output()
|
standalone: false,
|
||||||
closed = new EventEmitter();
|
})
|
||||||
|
export class ShellProcessBarItemComponent
|
||||||
activatedProcessId$ = this._app.activatedProcessId$;
|
implements OnInit, OnDestroy, OnChanges
|
||||||
|
{
|
||||||
latestBreadcrumb$: Observable<Breadcrumb> = NEVER;
|
#tabService = inject(TabService);
|
||||||
|
|
||||||
routerLink$: Observable<string[] | any[]> = NEVER;
|
tab = computed(() => this.#tabService.entityMap()[this.process().id]);
|
||||||
|
|
||||||
queryParams$: Observable<object> = NEVER;
|
private _process$ = new BehaviorSubject<ApplicationProcess>(undefined);
|
||||||
|
|
||||||
isActive$: Observable<boolean> = NEVER;
|
process$ = this._process$.asObservable();
|
||||||
|
|
||||||
showCloseButton$: Observable<boolean> = NEVER;
|
process = input.required<ApplicationProcess>();
|
||||||
|
|
||||||
cartItemCount$: Observable<number> = NEVER;
|
@Output()
|
||||||
|
closed = new EventEmitter();
|
||||||
constructor(
|
|
||||||
private _breadcrumb: BreadcrumbService,
|
showCart = computed(() => {
|
||||||
private _app: ApplicationService,
|
const tab = this.tab();
|
||||||
private _router: Router,
|
|
||||||
private _checkout: DomainCheckoutService,
|
const pdata = tab.metadata?.process_data as { count?: number };
|
||||||
private _checkoutNavigationService: CheckoutNavigationService,
|
|
||||||
public _elRef: ElementRef<HTMLElement>,
|
if (!pdata) {
|
||||||
) {}
|
return false;
|
||||||
|
}
|
||||||
ngOnChanges({ process }: SimpleChanges): void {
|
|
||||||
if (process) {
|
return 'count' in pdata;
|
||||||
this._process$.next(process.currentValue);
|
});
|
||||||
}
|
|
||||||
}
|
currentLocationUrlTree = computed(() => {
|
||||||
|
const tab = this.tab();
|
||||||
ngOnInit() {
|
const current = tab.location.locations[tab.location.current];
|
||||||
this.initLatestBreadcrumb$();
|
|
||||||
this.initRouterLink$();
|
if (current?.url) {
|
||||||
this.initQueryParams$();
|
return this._router.parseUrl(current.url);
|
||||||
this.initIsActive$();
|
}
|
||||||
this.initShowCloseButton$();
|
|
||||||
this.initCartItemCount$();
|
return null;
|
||||||
}
|
});
|
||||||
|
|
||||||
scrollIntoView() {
|
navigateByUrl(event: MouseEvent) {
|
||||||
setTimeout(() => this._elRef.nativeElement.scrollIntoView({ behavior: 'smooth', block: 'center' }), 0);
|
event?.preventDefault();
|
||||||
}
|
this._router.navigateByUrl(this.currentLocationUrlTree());
|
||||||
|
}
|
||||||
getCheckoutPath(processId: number) {
|
|
||||||
return this._checkoutNavigationService.getCheckoutReviewPath(processId).path;
|
activatedProcessId$ = this._app.activatedProcessId$;
|
||||||
}
|
|
||||||
|
latestBreadcrumb$: Observable<Breadcrumb> = NEVER;
|
||||||
initLatestBreadcrumb$() {
|
|
||||||
this.latestBreadcrumb$ = this.process$.pipe(
|
routerLink$: Observable<string[] | any[]> = NEVER;
|
||||||
switchMap((process) => this._breadcrumb.getLastActivatedBreadcrumbByKey$(process?.id)),
|
|
||||||
);
|
queryParams$: Observable<object> = NEVER;
|
||||||
}
|
|
||||||
|
isActive$: Observable<boolean> = NEVER;
|
||||||
initRouterLink$() {
|
|
||||||
this.routerLink$ = this.latestBreadcrumb$.pipe(
|
showCloseButton$: Observable<boolean> = NEVER;
|
||||||
map((breadcrumb) => (breadcrumb?.path instanceof Array ? breadcrumb.path : [breadcrumb?.path])),
|
|
||||||
);
|
cartItemCount$: Observable<number> = NEVER;
|
||||||
}
|
|
||||||
|
constructor(
|
||||||
initQueryParams$() {
|
private _app: ApplicationService,
|
||||||
this.queryParams$ = this.latestBreadcrumb$.pipe(map((breadcrumb) => breadcrumb?.params));
|
private _router: Router,
|
||||||
}
|
private _checkout: DomainCheckoutService,
|
||||||
|
private _checkoutNavigationService: CheckoutNavigationService,
|
||||||
initIsActive$() {
|
public _elRef: ElementRef<HTMLElement>,
|
||||||
if (isObservable(this.activatedProcessId$) && isObservable(this.process$)) {
|
) {}
|
||||||
this.isActive$ = combineLatest([this.activatedProcessId$, this.process$]).pipe(
|
|
||||||
map(([activatedId, process]) => process?.id === activatedId),
|
ngOnChanges({ process }: SimpleChanges): void {
|
||||||
tap((isActive) => {
|
if (process) {
|
||||||
if (isActive) {
|
this._process$.next(process.currentValue);
|
||||||
this.scrollIntoView();
|
}
|
||||||
}
|
}
|
||||||
}),
|
|
||||||
);
|
ngOnInit() {
|
||||||
}
|
this.initRouterLink$();
|
||||||
}
|
this.initQueryParams$();
|
||||||
|
this.initIsActive$();
|
||||||
initShowCloseButton$() {
|
this.initShowCloseButton$();
|
||||||
if (isObservable(this.isActive$) && isObservable(this.process$)) {
|
this.initCartItemCount$();
|
||||||
this.showCloseButton$ = this.process$.pipe(map((process) => process?.closeable));
|
}
|
||||||
}
|
|
||||||
}
|
scrollIntoView() {
|
||||||
|
setTimeout(
|
||||||
initCartItemCount$() {
|
() =>
|
||||||
this.cartItemCount$ = this.process$.pipe(
|
this._elRef.nativeElement.scrollIntoView({
|
||||||
switchMap((process) => this._checkout?.getShoppingCart({ processId: process?.id })),
|
behavior: 'smooth',
|
||||||
map((cart) => cart?.items?.length ?? 0),
|
block: 'center',
|
||||||
);
|
}),
|
||||||
}
|
0,
|
||||||
|
);
|
||||||
ngOnDestroy() {
|
}
|
||||||
this._process$.complete();
|
|
||||||
}
|
getCheckoutPath(processId: number) {
|
||||||
|
return this._checkoutNavigationService.getCheckoutReviewPath(processId)
|
||||||
async close() {
|
.path;
|
||||||
const breadcrumb = await this.getLatestBreadcrumbForSection();
|
}
|
||||||
await this.navigate(breadcrumb);
|
|
||||||
this._app.removeProcess(this.process.id);
|
initRouterLink$() {
|
||||||
this.closed.emit();
|
this.routerLink$ = this.latestBreadcrumb$.pipe(
|
||||||
}
|
map((breadcrumb) =>
|
||||||
|
breadcrumb?.path instanceof Array
|
||||||
getLatestBreadcrumbForSection(): Promise<Breadcrumb> {
|
? breadcrumb.path
|
||||||
return this._breadcrumb
|
: [breadcrumb?.path],
|
||||||
.getLatestBreadcrumbForSection('customer', (c) => c.key !== this.process?.id)
|
),
|
||||||
.pipe(first())
|
);
|
||||||
.toPromise();
|
}
|
||||||
}
|
|
||||||
|
initQueryParams$() {
|
||||||
async navigate(breadcrumb?: Breadcrumb) {
|
this.queryParams$ = this.latestBreadcrumb$.pipe(
|
||||||
if (breadcrumb) {
|
map((breadcrumb) => breadcrumb?.params),
|
||||||
if (breadcrumb.path instanceof Array) {
|
);
|
||||||
await this._router.navigate(breadcrumb.path, { queryParams: breadcrumb.params });
|
}
|
||||||
} else {
|
|
||||||
await this._router.navigate([breadcrumb.path], { queryParams: breadcrumb.params });
|
initIsActive$() {
|
||||||
}
|
if (isObservable(this.activatedProcessId$) && isObservable(this.process$)) {
|
||||||
} else {
|
this.isActive$ = combineLatest([
|
||||||
await this._router.navigate(['/kunde/dashboard']);
|
this.activatedProcessId$,
|
||||||
}
|
this.process$,
|
||||||
}
|
]).pipe(
|
||||||
}
|
map(([activatedId, process]) => process?.id === activatedId),
|
||||||
|
tap((isActive) => {
|
||||||
|
if (isActive) {
|
||||||
|
this.scrollIntoView();
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
initShowCloseButton$() {
|
||||||
|
if (isObservable(this.isActive$) && isObservable(this.process$)) {
|
||||||
|
this.showCloseButton$ = this.process$.pipe(
|
||||||
|
map((process) => process?.closeable),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
initCartItemCount$() {
|
||||||
|
this.cartItemCount$ = this.process$.pipe(
|
||||||
|
switchMap((process) =>
|
||||||
|
this._checkout?.getShoppingCart({ processId: process?.id }),
|
||||||
|
),
|
||||||
|
map((cart) => cart?.items?.length ?? 0),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy() {
|
||||||
|
this._process$.complete();
|
||||||
|
}
|
||||||
|
|
||||||
|
async close() {
|
||||||
|
await this.navigate();
|
||||||
|
this._app.removeProcess(this.process().id);
|
||||||
|
this.closed.emit();
|
||||||
|
}
|
||||||
|
|
||||||
|
async navigate(breadcrumb?: Breadcrumb) {
|
||||||
|
if (breadcrumb) {
|
||||||
|
if (breadcrumb.path instanceof Array) {
|
||||||
|
await this._router.navigate(breadcrumb.path, {
|
||||||
|
queryParams: breadcrumb.params,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await this._router.navigate([breadcrumb.path], {
|
||||||
|
queryParams: breadcrumb.params,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await this._router.navigate(['/kunde/dashboard']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,187 +1,206 @@
|
|||||||
import { coerceArray } from '@angular/cdk/coercion';
|
import { coerceArray } from '@angular/cdk/coercion';
|
||||||
import { Component, ChangeDetectionStrategy, OnInit, ViewChild, ElementRef } from '@angular/core';
|
import {
|
||||||
import { Router } from '@angular/router';
|
Component,
|
||||||
import { ApplicationProcess, ApplicationService } from '@core/application';
|
ChangeDetectionStrategy,
|
||||||
import { BreadcrumbService } from '@core/breadcrumb';
|
OnInit,
|
||||||
import { DomainCheckoutService } from '@domain/checkout';
|
ViewChild,
|
||||||
import { injectOpenMessageModal } from '@modal/message';
|
ElementRef,
|
||||||
import { CustomerOrdersNavigationService, ProductCatalogNavigationService } from '@shared/services/navigation';
|
} from '@angular/core';
|
||||||
import { NEVER, Observable, of } from 'rxjs';
|
import { Router } from '@angular/router';
|
||||||
import { delay, first, map, switchMap } from 'rxjs/operators';
|
import { ApplicationProcess, ApplicationService } from '@core/application';
|
||||||
|
import { BreadcrumbService } from '@core/breadcrumb';
|
||||||
@Component({
|
import { DomainCheckoutService } from '@domain/checkout';
|
||||||
selector: 'shell-process-bar',
|
import { injectOpenMessageModal } from '@modal/message';
|
||||||
templateUrl: 'process-bar.component.html',
|
import {
|
||||||
styleUrls: ['process-bar.component.css'],
|
CustomerOrdersNavigationService,
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
ProductCatalogNavigationService,
|
||||||
standalone: false,
|
} from '@shared/services/navigation';
|
||||||
})
|
import { NEVER, Observable, of } from 'rxjs';
|
||||||
export class ShellProcessBarComponent implements OnInit {
|
import { delay, first, map, switchMap } from 'rxjs/operators';
|
||||||
@ViewChild('processContainer')
|
|
||||||
processContainer: ElementRef;
|
@Component({
|
||||||
|
selector: 'shell-process-bar',
|
||||||
section$: Observable<'customer' | 'branch'> = NEVER;
|
templateUrl: 'process-bar.component.html',
|
||||||
|
styleUrls: ['process-bar.component.css'],
|
||||||
processes$: Observable<ApplicationProcess[]> = NEVER;
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
standalone: false,
|
||||||
showStartProcessText$: Observable<boolean> = NEVER;
|
})
|
||||||
|
export class ShellProcessBarComponent implements OnInit {
|
||||||
hovered: boolean;
|
@ViewChild('processContainer')
|
||||||
showScrollArrows: boolean;
|
processContainer: ElementRef;
|
||||||
showArrowLeft: boolean;
|
|
||||||
showArrowRight: boolean;
|
section$: Observable<'customer' | 'branch'> = NEVER;
|
||||||
|
|
||||||
trackByFn = (_: number, process: ApplicationProcess) => process.id;
|
processes$: Observable<ApplicationProcess[]> = NEVER;
|
||||||
|
|
||||||
openMessageModal = injectOpenMessageModal();
|
showStartProcessText$: Observable<boolean> = NEVER;
|
||||||
|
|
||||||
constructor(
|
hovered: boolean;
|
||||||
private _app: ApplicationService,
|
showScrollArrows: boolean;
|
||||||
private _router: Router,
|
showArrowLeft: boolean;
|
||||||
private _catalogNavigationService: ProductCatalogNavigationService,
|
showArrowRight: boolean;
|
||||||
private _customerOrderNavigationService: CustomerOrdersNavigationService,
|
|
||||||
private _checkoutService: DomainCheckoutService,
|
trackByFn = (_: number, process: ApplicationProcess) => process.id;
|
||||||
private _breadcrumb: BreadcrumbService,
|
|
||||||
) {}
|
openMessageModal = injectOpenMessageModal();
|
||||||
|
|
||||||
ngOnInit() {
|
constructor(
|
||||||
this.initSection$();
|
private _app: ApplicationService,
|
||||||
this.initProcesses$();
|
private _router: Router,
|
||||||
this.initShowStartProcessText$();
|
private _catalogNavigationService: ProductCatalogNavigationService,
|
||||||
this.checkScrollArrowVisibility();
|
private _customerOrderNavigationService: CustomerOrdersNavigationService,
|
||||||
}
|
private _checkoutService: DomainCheckoutService,
|
||||||
|
private _breadcrumb: BreadcrumbService,
|
||||||
initSection$() {
|
) {}
|
||||||
this.section$ = of('customer');
|
|
||||||
}
|
ngOnInit() {
|
||||||
|
this.initSection$();
|
||||||
initProcesses$() {
|
this.initProcesses$();
|
||||||
this.processes$ = this.section$.pipe(switchMap((section) => this._app.getProcesses$(section)));
|
this.initShowStartProcessText$();
|
||||||
}
|
this.checkScrollArrowVisibility();
|
||||||
|
}
|
||||||
initShowStartProcessText$() {
|
|
||||||
this.showStartProcessText$ = this.processes$.pipe(map((processes) => processes.length === 0));
|
initSection$() {
|
||||||
}
|
this.section$ = of(undefined);
|
||||||
|
}
|
||||||
async createProcess(target: string = 'product') {
|
|
||||||
const process = await this.createCartProcess();
|
initProcesses$() {
|
||||||
this.navigateTo(target, process);
|
this.processes$ = this.section$.pipe(
|
||||||
|
switchMap((section) => this._app.getProcesses$(section)),
|
||||||
setTimeout(() => this.scrollToEnd(), 25);
|
map((processes) =>
|
||||||
}
|
processes.filter((process) => process.type === 'cart'),
|
||||||
|
),
|
||||||
static REGEX_PROCESS_NAME = /^Vorgang \d+$/;
|
);
|
||||||
|
}
|
||||||
async createCartProcess() {
|
|
||||||
return this._app.createCustomerProcess();
|
initShowStartProcessText$() {
|
||||||
}
|
this.showStartProcessText$ = this.processes$.pipe(
|
||||||
|
map((processes) => processes.length === 0),
|
||||||
async navigateTo(target: string, process: ApplicationProcess) {
|
);
|
||||||
switch (target) {
|
}
|
||||||
case 'product':
|
|
||||||
await this._catalogNavigationService.getArticleSearchBasePath(process.id).navigate();
|
async createProcess(target = 'product') {
|
||||||
break;
|
// const process = await this.createCartProcess();
|
||||||
case 'customer':
|
this.navigateTo(target, Date.now());
|
||||||
await this._router.navigate(['/kunde', process.id, 'customer', 'search']);
|
|
||||||
break;
|
setTimeout(() => this.scrollToEnd(), 25);
|
||||||
case 'goods-out':
|
}
|
||||||
await this._router.navigate(['/kunde', process.id, 'goods', 'out']);
|
|
||||||
break;
|
static REGEX_PROCESS_NAME = /^Vorgang \d+$/;
|
||||||
case 'order':
|
|
||||||
await this._customerOrderNavigationService.getCustomerOrdersBasePath(process.id).navigate();
|
async createCartProcess() {
|
||||||
break;
|
return this._app.createCustomerProcess();
|
||||||
|
}
|
||||||
default:
|
|
||||||
await this._router.navigate(['/kunde', process.id, target]);
|
async navigateTo(target: string, processId: number) {
|
||||||
break;
|
switch (target) {
|
||||||
}
|
case 'product':
|
||||||
}
|
await this._catalogNavigationService
|
||||||
|
.getArticleSearchBasePath(processId)
|
||||||
async closeAllProcesses() {
|
.navigate();
|
||||||
const processes = await this.processes$.pipe(first()).toPromise();
|
break;
|
||||||
this.openMessageModal({
|
case 'customer':
|
||||||
title: 'Vorgänge schließen',
|
await this._router.navigate([
|
||||||
message: `Sind Sie sich sicher, dass sie alle ${processes.length} Vorgänge schließen wollen?`,
|
'/kunde',
|
||||||
actions: [
|
processId,
|
||||||
{ label: 'Abbrechen', value: false },
|
'customer',
|
||||||
{
|
'search',
|
||||||
label: 'leere Warenkörbe',
|
]);
|
||||||
value: true,
|
break;
|
||||||
action: () => this.handleCloseEmptyCartProcesses(),
|
case 'goods-out':
|
||||||
},
|
await this._router.navigate(['/kunde', processId, 'goods', 'out']);
|
||||||
{
|
break;
|
||||||
label: 'Ja, alle',
|
case 'order':
|
||||||
value: true,
|
await this._customerOrderNavigationService
|
||||||
primary: true,
|
.getCustomerOrdersBasePath(processId)
|
||||||
action: () => this.handleCloseAllProcesses(),
|
.navigate();
|
||||||
},
|
break;
|
||||||
],
|
|
||||||
});
|
default:
|
||||||
this.checkScrollArrowVisibility();
|
await this._router.navigate(['/kunde', processId, target]);
|
||||||
}
|
break;
|
||||||
|
}
|
||||||
async handleCloseEmptyCartProcesses() {
|
}
|
||||||
let processes = await this.processes$.pipe(first()).toPromise();
|
|
||||||
for (const process of processes) {
|
async closeAllProcesses() {
|
||||||
const cart = await this._checkoutService.getShoppingCart({ processId: process.id }).pipe(first()).toPromise();
|
const processes = await this.processes$.pipe(first()).toPromise();
|
||||||
|
this.openMessageModal({
|
||||||
if (cart?.items?.length === 0 || cart?.items === undefined) {
|
title: 'Vorgänge schließen',
|
||||||
this._app.removeProcess(process?.id);
|
message: `Sind Sie sich sicher, dass sie alle ${processes.length} Vorgänge schließen wollen?`,
|
||||||
}
|
actions: [
|
||||||
|
{ label: 'Abbrechen', value: false },
|
||||||
processes = await this.processes$.pipe(delay(1), first()).toPromise();
|
{
|
||||||
|
label: 'leere Warenkörbe',
|
||||||
if (processes.length === 0) {
|
value: true,
|
||||||
this._router.navigate(['/kunde', 'dashboard']);
|
action: () => this.handleCloseEmptyCartProcesses(),
|
||||||
} else {
|
},
|
||||||
const lastest = processes.reduce(
|
{
|
||||||
(prev, current) => (prev.activated > current.activated ? prev : current),
|
label: 'Ja, alle',
|
||||||
processes[0],
|
value: true,
|
||||||
);
|
primary: true,
|
||||||
const crumb = await this._breadcrumb.getLastActivatedBreadcrumbByKey$(lastest.id).pipe(first()).toPromise();
|
action: () => this.handleCloseAllProcesses(),
|
||||||
if (crumb) {
|
},
|
||||||
this._router.navigate(coerceArray(crumb.path), { queryParams: crumb.params });
|
],
|
||||||
} else {
|
});
|
||||||
this._router.navigate(['/kunde', lastest.id, 'product']);
|
this.checkScrollArrowVisibility();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
async handleCloseEmptyCartProcesses() {
|
||||||
}
|
let processes = await this.processes$.pipe(first()).toPromise();
|
||||||
|
for (const process of processes) {
|
||||||
async handleCloseAllProcesses() {
|
const cart = await this._checkoutService
|
||||||
const processes = await this.processes$.pipe(first()).toPromise();
|
.getShoppingCart({ processId: process.id })
|
||||||
processes.forEach((process) => this._app.removeProcess(process?.id));
|
.pipe(first())
|
||||||
this._router.navigate(['/kunde', 'dashboard']);
|
.toPromise();
|
||||||
}
|
|
||||||
|
if (cart?.items?.length === 0 || cart?.items === undefined) {
|
||||||
onMouseWheel(event: any) {
|
this._app.removeProcess(process?.id);
|
||||||
// Ermöglicht es, am Desktop die Prozessleiste mit dem Mausrad hoch/runter horizontal zu scrollen
|
}
|
||||||
if (event.deltaY > 0) {
|
|
||||||
this.processContainer.nativeElement.scrollLeft += 100;
|
processes = await this.processes$.pipe(delay(1), first()).toPromise();
|
||||||
} else {
|
|
||||||
this.processContainer.nativeElement.scrollLeft -= 100;
|
this._router.navigate(['/kunde', 'dashboard']);
|
||||||
}
|
}
|
||||||
event.preventDefault();
|
}
|
||||||
}
|
|
||||||
|
async handleCloseAllProcesses() {
|
||||||
scrollLeft() {
|
const processes = await this.processes$.pipe(first()).toPromise();
|
||||||
this.processContainer.nativeElement.scrollLeft -= 100;
|
processes.forEach((process) => this._app.removeProcess(process?.id));
|
||||||
}
|
this._router.navigate(['/kunde', 'dashboard']);
|
||||||
|
}
|
||||||
scrollRight() {
|
|
||||||
this.processContainer.nativeElement.scrollLeft += 100;
|
onMouseWheel(event: any) {
|
||||||
}
|
// Ermöglicht es, am Desktop die Prozessleiste mit dem Mausrad hoch/runter horizontal zu scrollen
|
||||||
|
if (event.deltaY > 0) {
|
||||||
scrollToEnd() {
|
this.processContainer.nativeElement.scrollLeft += 100;
|
||||||
this.processContainer.nativeElement.scrollLeft =
|
} else {
|
||||||
this.processContainer?.nativeElement?.scrollWidth + this.processContainer?.nativeElement?.scrollLeft;
|
this.processContainer.nativeElement.scrollLeft -= 100;
|
||||||
}
|
}
|
||||||
|
event.preventDefault();
|
||||||
checkScrollArrowVisibility() {
|
}
|
||||||
this.showScrollArrows = this.processContainer?.nativeElement?.scrollWidth > 0;
|
|
||||||
this.showArrowRight =
|
scrollLeft() {
|
||||||
((this.processContainer?.nativeElement?.scrollWidth - this.processContainer?.nativeElement?.scrollLeft) | 0) <=
|
this.processContainer.nativeElement.scrollLeft -= 100;
|
||||||
this.processContainer?.nativeElement?.offsetWidth;
|
}
|
||||||
this.showArrowLeft = this.processContainer?.nativeElement?.scrollLeft <= 0;
|
|
||||||
}
|
scrollRight() {
|
||||||
}
|
this.processContainer.nativeElement.scrollLeft += 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
scrollToEnd() {
|
||||||
|
this.processContainer.nativeElement.scrollLeft =
|
||||||
|
this.processContainer?.nativeElement?.scrollWidth +
|
||||||
|
this.processContainer?.nativeElement?.scrollLeft;
|
||||||
|
}
|
||||||
|
|
||||||
|
checkScrollArrowVisibility() {
|
||||||
|
this.showScrollArrows =
|
||||||
|
this.processContainer?.nativeElement?.scrollWidth > 0;
|
||||||
|
this.showArrowRight =
|
||||||
|
((this.processContainer?.nativeElement?.scrollWidth -
|
||||||
|
this.processContainer?.nativeElement?.scrollLeft) |
|
||||||
|
0) <=
|
||||||
|
this.processContainer?.nativeElement?.offsetWidth;
|
||||||
|
this.showArrowLeft = this.processContainer?.nativeElement?.scrollLeft <= 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -16,14 +16,16 @@
|
|||||||
</a>
|
</a>
|
||||||
|
|
||||||
<div class="side-menu-group-sub-item-wrapper">
|
<div class="side-menu-group-sub-item-wrapper">
|
||||||
@if (customerSearchRoute$ | async; as customerSearchRoute) {
|
@if (
|
||||||
|
customerSearchRoute() || customerCreateRoute() || customerRewardRoute()
|
||||||
|
) {
|
||||||
<a
|
<a
|
||||||
class="side-menu-group-item"
|
class="side-menu-group-item"
|
||||||
(click)="closeSideMenu(); focusSearchBox()"
|
(click)="closeSideMenu(); focusSearchBox()"
|
||||||
[routerLink]="customerSearchRoute.path"
|
[routerLink]="customerSearchRoute().path"
|
||||||
[queryParams]="customerSearchRoute.queryParams"
|
[queryParams]="customerSearchRoute().queryParams"
|
||||||
sharedRegexRouterLinkActive="active"
|
sharedRegexRouterLinkActive="active"
|
||||||
sharedRegexRouterLinkActiveTest="^\/kunde\/\d*\/customer"
|
sharedRegexRouterLinkActiveTest="^(\/kunde\/\d*\/customer|\/\d*\/reward)"
|
||||||
(isActiveChange)="customerActive($event); focusSearchBox()"
|
(isActiveChange)="customerActive($event); focusSearchBox()"
|
||||||
>
|
>
|
||||||
<span class="side-menu-group-item-icon">
|
<span class="side-menu-group-item-icon">
|
||||||
@@ -32,11 +34,11 @@
|
|||||||
<span class="side-menu-group-item-label">Kunden</span>
|
<span class="side-menu-group-item-label">Kunden</span>
|
||||||
<button
|
<button
|
||||||
class="side-menu-group-arrow"
|
class="side-menu-group-arrow"
|
||||||
[class.side-menu-item-rotate]="customerExpanded"
|
[class.side-menu-item-rotate]="customerExpanded()"
|
||||||
(click)="
|
(click)="
|
||||||
$event.stopPropagation();
|
$event.stopPropagation();
|
||||||
$event.preventDefault();
|
$event.preventDefault();
|
||||||
customerExpanded = !customerExpanded
|
toggleCustomerExpanded()
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<shared-icon icon="keyboard-arrow-down"></shared-icon>
|
<shared-icon icon="keyboard-arrow-down"></shared-icon>
|
||||||
@@ -44,13 +46,16 @@
|
|||||||
</a>
|
</a>
|
||||||
}
|
}
|
||||||
|
|
||||||
<div class="side-menu-group-sub-items" [class.hidden]="!customerExpanded">
|
<div
|
||||||
@if (customerSearchRoute$ | async; as customerSearchRoute) {
|
class="side-menu-group-sub-items"
|
||||||
|
[class.hidden]="!customerExpanded()"
|
||||||
|
>
|
||||||
|
@if (customerSearchRoute() || customerRewardRoute()) {
|
||||||
<a
|
<a
|
||||||
class="side-menu-group-item"
|
class="side-menu-group-item"
|
||||||
(click)="closeSideMenu(); focusSearchBox()"
|
(click)="closeSideMenu(); focusSearchBox()"
|
||||||
[routerLink]="customerSearchRoute.path"
|
[routerLink]="customerSearchRoute().path"
|
||||||
[queryParams]="customerSearchRoute.queryParams"
|
[queryParams]="customerSearchRoute().queryParams"
|
||||||
sharedRegexRouterLinkActive="active"
|
sharedRegexRouterLinkActive="active"
|
||||||
sharedRegexRouterLinkActiveTest="^\/kunde\/\d*\/customer\/(\(search|search)"
|
sharedRegexRouterLinkActiveTest="^\/kunde\/\d*\/customer\/(\(search|search)"
|
||||||
(isActiveChange)="focusSearchBox()"
|
(isActiveChange)="focusSearchBox()"
|
||||||
@@ -59,12 +64,12 @@
|
|||||||
<span class="side-menu-group-item-label">Suchen</span>
|
<span class="side-menu-group-item-label">Suchen</span>
|
||||||
</a>
|
</a>
|
||||||
}
|
}
|
||||||
@if (customerCreateRoute$ | async; as customerCreateRoute) {
|
@if (customerCreateRoute() || customerRewardRoute()) {
|
||||||
<a
|
<a
|
||||||
class="side-menu-group-item"
|
class="side-menu-group-item"
|
||||||
(click)="closeSideMenu()"
|
(click)="closeSideMenu()"
|
||||||
[routerLink]="customerCreateRoute.path"
|
[routerLink]="customerCreateRoute().path"
|
||||||
[queryParams]="customerCreateRoute.queryParams"
|
[queryParams]="customerCreateRoute().queryParams"
|
||||||
sharedRegexRouterLinkActive="active"
|
sharedRegexRouterLinkActive="active"
|
||||||
sharedRegexRouterLinkActiveTest="^\/kunde\/\d*\/customer\/(\(create|create)"
|
sharedRegexRouterLinkActiveTest="^\/kunde\/\d*\/customer\/(\(create|create)"
|
||||||
>
|
>
|
||||||
@@ -72,6 +77,19 @@
|
|||||||
<span class="side-menu-group-item-label">Erfassen</span>
|
<span class="side-menu-group-item-label">Erfassen</span>
|
||||||
</a>
|
</a>
|
||||||
}
|
}
|
||||||
|
<!-- @if (customerRewardRoute()) {
|
||||||
|
<a
|
||||||
|
class="side-menu-group-item"
|
||||||
|
(click)="closeSideMenu(); focusSearchBox()"
|
||||||
|
[routerLink]="customerRewardRoute()"
|
||||||
|
(isActiveChange)="focusSearchBox()"
|
||||||
|
sharedRegexRouterLinkActive="active"
|
||||||
|
sharedRegexRouterLinkActiveTest="^\/\d*\/reward"
|
||||||
|
>
|
||||||
|
<span class="side-menu-group-item-icon"> </span>
|
||||||
|
<span class="side-menu-group-item-label">Prämienshop</span>
|
||||||
|
</a>
|
||||||
|
} -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -93,11 +111,7 @@
|
|||||||
*ifRole="'Store'"
|
*ifRole="'Store'"
|
||||||
class="side-menu-group-item"
|
class="side-menu-group-item"
|
||||||
(click)="closeSideMenu(); focusSearchBox()"
|
(click)="closeSideMenu(); focusSearchBox()"
|
||||||
[routerLink]="[
|
[routerLink]="['/', tabId(), 'return']"
|
||||||
'/',
|
|
||||||
processService.activatedTab()?.id || processService.nextId(),
|
|
||||||
'return',
|
|
||||||
]"
|
|
||||||
(isActiveChange)="focusSearchBox()"
|
(isActiveChange)="focusSearchBox()"
|
||||||
>
|
>
|
||||||
<span class="side-menu-group-item-icon w-[2.375rem] h-12">
|
<span class="side-menu-group-item-icon w-[2.375rem] h-12">
|
||||||
@@ -258,11 +272,7 @@
|
|||||||
<a
|
<a
|
||||||
class="side-menu-group-item"
|
class="side-menu-group-item"
|
||||||
(click)="closeSideMenu(); focusSearchBox()"
|
(click)="closeSideMenu(); focusSearchBox()"
|
||||||
[routerLink]="[
|
[routerLink]="['/', tabId(), 'remission']"
|
||||||
'/',
|
|
||||||
processService.activatedTab()?.id || processService.nextId(),
|
|
||||||
'remission',
|
|
||||||
]"
|
|
||||||
(isActiveChange)="focusSearchBox(); remissionExpanded.set($event)"
|
(isActiveChange)="focusSearchBox(); remissionExpanded.set($event)"
|
||||||
routerLinkActive="active"
|
routerLinkActive="active"
|
||||||
#rlActive="routerLinkActive"
|
#rlActive="routerLinkActive"
|
||||||
@@ -288,11 +298,7 @@
|
|||||||
<a
|
<a
|
||||||
class="side-menu-group-item"
|
class="side-menu-group-item"
|
||||||
(click)="closeSideMenu(); focusSearchBox()"
|
(click)="closeSideMenu(); focusSearchBox()"
|
||||||
[routerLink]="[
|
[routerLink]="['/', tabId(), 'remission']"
|
||||||
'/',
|
|
||||||
processService.activatedTab()?.id || processService.nextId(),
|
|
||||||
'remission',
|
|
||||||
]"
|
|
||||||
(isActiveChange)="focusSearchBox()"
|
(isActiveChange)="focusSearchBox()"
|
||||||
sharedRegexRouterLinkActive="active"
|
sharedRegexRouterLinkActive="active"
|
||||||
sharedRegexRouterLinkActiveTest="^\/\d*\/remission\/(mandatory|department)"
|
sharedRegexRouterLinkActiveTest="^\/\d*\/remission\/(mandatory|department)"
|
||||||
@@ -303,12 +309,7 @@
|
|||||||
<a
|
<a
|
||||||
class="side-menu-group-item"
|
class="side-menu-group-item"
|
||||||
(click)="closeSideMenu(); focusSearchBox()"
|
(click)="closeSideMenu(); focusSearchBox()"
|
||||||
[routerLink]="[
|
[routerLink]="['/', tabId(), 'remission', 'return-receipt']"
|
||||||
'/',
|
|
||||||
processService.activatedTab()?.id || processService.nextId(),
|
|
||||||
'remission',
|
|
||||||
'return-receipt',
|
|
||||||
]"
|
|
||||||
(isActiveChange)="focusSearchBox()"
|
(isActiveChange)="focusSearchBox()"
|
||||||
sharedRegexRouterLinkActive="active"
|
sharedRegexRouterLinkActive="active"
|
||||||
sharedRegexRouterLinkActiveTest="^\/\d*\/remission\/return-receipt"
|
sharedRegexRouterLinkActiveTest="^\/\d*\/remission\/return-receipt"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
Component,
|
Component,
|
||||||
ChangeDetectionStrategy,
|
ChangeDetectionStrategy,
|
||||||
Inject,
|
computed,
|
||||||
ChangeDetectorRef,
|
ChangeDetectorRef,
|
||||||
inject,
|
inject,
|
||||||
DOCUMENT,
|
DOCUMENT,
|
||||||
@@ -29,10 +29,12 @@ import {
|
|||||||
PickUpShelfOutNavigationService,
|
PickUpShelfOutNavigationService,
|
||||||
ProductCatalogNavigationService,
|
ProductCatalogNavigationService,
|
||||||
} from '@shared/services/navigation';
|
} from '@shared/services/navigation';
|
||||||
|
import { toObservable, toSignal } from '@angular/core/rxjs-interop';
|
||||||
|
|
||||||
import { TabService } from '@isa/core/tabs';
|
import { TabService } from '@isa/core/tabs';
|
||||||
import { NgIconComponent, provideIcons } from '@ng-icons/core';
|
import { NgIconComponent, provideIcons } from '@ng-icons/core';
|
||||||
import { isaNavigationRemission2, isaNavigationReturn } from '@isa/icons';
|
import { isaNavigationRemission2, isaNavigationReturn } from '@isa/icons';
|
||||||
|
import z from 'zod';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'shell-side-menu',
|
selector: 'shell-side-menu',
|
||||||
@@ -68,7 +70,21 @@ export class ShellSideMenuComponent {
|
|||||||
#pickUpShelfInNavigation = inject(PickupShelfInNavigationService);
|
#pickUpShelfInNavigation = inject(PickupShelfInNavigationService);
|
||||||
#cdr = inject(ChangeDetectorRef);
|
#cdr = inject(ChangeDetectorRef);
|
||||||
#document = inject(DOCUMENT);
|
#document = inject(DOCUMENT);
|
||||||
processService = inject(TabService);
|
tabService = inject(TabService);
|
||||||
|
|
||||||
|
staticTabIds = Object.values(
|
||||||
|
this.#config.get('process.ids', z.record(z.coerce.number())),
|
||||||
|
);
|
||||||
|
|
||||||
|
tabId = computed(() => {
|
||||||
|
const tabId = this.tabService.activatedTab()?.id;
|
||||||
|
if (this.staticTabIds.includes(tabId)) {
|
||||||
|
return this.nextId();
|
||||||
|
}
|
||||||
|
return tabId || this.nextId();
|
||||||
|
});
|
||||||
|
|
||||||
|
tabId$ = toObservable(this.tabId);
|
||||||
|
|
||||||
branchKey$ = this.#stockService.StockCurrentBranch().pipe(
|
branchKey$ = this.#stockService.StockCurrentBranch().pipe(
|
||||||
retry(3),
|
retry(3),
|
||||||
@@ -93,6 +109,10 @@ export class ShellSideMenuComponent {
|
|||||||
return this.#environment.matchTablet();
|
return this.#environment.matchTablet();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
nextId() {
|
||||||
|
return Date.now();
|
||||||
|
}
|
||||||
|
|
||||||
customerBasePath$ = this.activeProcess$.pipe(
|
customerBasePath$ = this.activeProcess$.pipe(
|
||||||
map((process) => {
|
map((process) => {
|
||||||
if (
|
if (
|
||||||
@@ -109,18 +129,28 @@ export class ShellSideMenuComponent {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
customerSearchRoute$ = this.getLastActivatedCustomerProcessId$().pipe(
|
customerSearchRoute = toSignal(
|
||||||
map((processId) => {
|
this.getLastActivatedCustomerProcessId$().pipe(
|
||||||
return this.#customerSearchNavigation.defaultRoute({ processId });
|
map((processId) => {
|
||||||
}),
|
return this.#customerSearchNavigation.defaultRoute({ processId });
|
||||||
|
}),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
customerCreateRoute$ = this.getLastActivatedCustomerProcessId$().pipe(
|
customerCreateRoute = toSignal(
|
||||||
map((processId) => {
|
this.getLastActivatedCustomerProcessId$().pipe(
|
||||||
return this.#customerCreateNavigation.defaultRoute({ processId });
|
map((processId) => {
|
||||||
}),
|
return this.#customerCreateNavigation.defaultRoute({ processId });
|
||||||
|
}),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
customerRewardRoute = computed(() => {
|
||||||
|
const routeName = 'reward';
|
||||||
|
const tabId = this.tabService.activatedTab()?.id;
|
||||||
|
return this.#router.createUrlTree(['/', tabId || this.nextId(), routeName]);
|
||||||
|
});
|
||||||
|
|
||||||
pickUpShelfOutRoutePath$ = this.getLastActivatedCustomerProcessId$().pipe(
|
pickUpShelfOutRoutePath$ = this.getLastActivatedCustomerProcessId$().pipe(
|
||||||
map((processId) => {
|
map((processId) => {
|
||||||
if (processId) {
|
if (processId) {
|
||||||
@@ -204,26 +234,25 @@ export class ShellSideMenuComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
shelfExpanded = false;
|
shelfExpanded = false;
|
||||||
customerExpanded = false;
|
customerExpanded = signal(false);
|
||||||
remissionExpanded = signal(false);
|
remissionExpanded = signal(false);
|
||||||
|
|
||||||
customerActive(isActive: boolean) {
|
customerActive(isActive: boolean) {
|
||||||
if (isActive) {
|
if (isActive) {
|
||||||
this.expandCustomer();
|
this.customerExpanded.set(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toggleCustomerExpanded() {
|
||||||
|
this.customerExpanded.set(!this.customerExpanded());
|
||||||
|
}
|
||||||
|
|
||||||
shelfActive(isActive: boolean) {
|
shelfActive(isActive: boolean) {
|
||||||
if (isActive) {
|
if (isActive) {
|
||||||
this.expandShelf();
|
this.expandShelf();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
expandCustomer() {
|
|
||||||
this.customerExpanded = true;
|
|
||||||
this.#cdr.markForCheck();
|
|
||||||
}
|
|
||||||
|
|
||||||
expandShelf() {
|
expandShelf() {
|
||||||
this.shelfExpanded = true;
|
this.shelfExpanded = true;
|
||||||
this.#cdr.markForCheck();
|
this.#cdr.markForCheck();
|
||||||
@@ -322,23 +351,7 @@ export class ShellSideMenuComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getLastActivatedCustomerProcessId$() {
|
getLastActivatedCustomerProcessId$() {
|
||||||
return this.#app.getProcesses$('customer').pipe(
|
return this.tabId$;
|
||||||
map((processes) => {
|
|
||||||
const lastCustomerProcess = processes
|
|
||||||
.filter((process) => process.type === 'cart')
|
|
||||||
.reduce((last, current) => {
|
|
||||||
if (!last) return current;
|
|
||||||
|
|
||||||
if (last.activated > current.activated) {
|
|
||||||
return last;
|
|
||||||
} else {
|
|
||||||
return current;
|
|
||||||
}
|
|
||||||
}, undefined);
|
|
||||||
|
|
||||||
return lastCustomerProcess?.id ?? Date.now();
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
closeSideMenu() {
|
closeSideMenu() {
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import {
|
||||||
|
argsToTemplate,
|
||||||
|
moduleMetadata,
|
||||||
|
type Meta,
|
||||||
|
type StoryObj,
|
||||||
|
} from '@storybook/angular';
|
||||||
|
import {
|
||||||
|
InlineInputComponent,
|
||||||
|
InputControlDirective,
|
||||||
|
} from '@isa/ui/input-controls';
|
||||||
|
|
||||||
|
const meta: Meta<InlineInputComponent> = {
|
||||||
|
component: InlineInputComponent,
|
||||||
|
title: 'ui/input-controls/InlineInput',
|
||||||
|
argTypes: {
|
||||||
|
size: { control: 'select', options: ['small', 'medium'] },
|
||||||
|
},
|
||||||
|
args: {
|
||||||
|
size: 'medium',
|
||||||
|
},
|
||||||
|
decorators: [
|
||||||
|
moduleMetadata({
|
||||||
|
imports: [InputControlDirective],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
render: (args) => ({
|
||||||
|
props: args,
|
||||||
|
template: `
|
||||||
|
<ui-inline-input ${argsToTemplate(args)}>
|
||||||
|
<input type="text" placeholder="Enter inline text" />
|
||||||
|
</ui-inline-input>
|
||||||
|
`,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
type Story = StoryObj<InlineInputComponent>;
|
||||||
|
|
||||||
|
export const Primary: Story = {
|
||||||
|
args: {
|
||||||
|
size: 'medium',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WithLabel: Story = {
|
||||||
|
args: {
|
||||||
|
size: 'medium',
|
||||||
|
},
|
||||||
|
render: (args) => ({
|
||||||
|
props: args,
|
||||||
|
template: `
|
||||||
|
<ui-inline-input ${argsToTemplate(args)}>
|
||||||
|
<label>Label</label>
|
||||||
|
<input type="text" placeholder="Enter inline text" />
|
||||||
|
</ui-inline-input>
|
||||||
|
`,
|
||||||
|
}),
|
||||||
|
};
|
||||||
@@ -12,7 +12,7 @@ variables:
|
|||||||
value: '4'
|
value: '4'
|
||||||
# Minor Version einstellen
|
# Minor Version einstellen
|
||||||
- name: 'Minor'
|
- name: 'Minor'
|
||||||
value: '1'
|
value: '2'
|
||||||
- name: 'Patch'
|
- name: 'Patch'
|
||||||
value: "$[counter(format('{0}.{1}', variables['Major'], variables['Minor']),0)]"
|
value: "$[counter(format('{0}.{1}', variables['Major'], variables['Minor']),0)]"
|
||||||
- name: 'BuildUniqueID'
|
- name: 'BuildUniqueID'
|
||||||
|
|||||||
200
docs/architecture/README.md
Normal file
200
docs/architecture/README.md
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
# Architecture Decision Records (ADRs)
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Architecture Decision Records (ADRs) are lightweight documents that capture important architectural decisions made during the development of the ISA-Frontend project. They provide context for why certain decisions were made, helping current and future team members understand the reasoning behind architectural choices.
|
||||||
|
|
||||||
|
## What are ADRs?
|
||||||
|
|
||||||
|
An Architecture Decision Record is a document that captures a single architectural decision and its rationale. The goal of an ADR is to document the architectural decisions that are being made so that:
|
||||||
|
|
||||||
|
- **Future team members** can understand why certain decisions were made
|
||||||
|
- **Current team members** can refer back to the reasoning behind decisions
|
||||||
|
- **Architectural evolution** can be tracked over time
|
||||||
|
- **Knowledge transfer** is facilitated during team changes
|
||||||
|
|
||||||
|
## ADR Structure
|
||||||
|
|
||||||
|
Each ADR follows a consistent structure based on our [TEMPLATE.md](./TEMPLATE.md) and includes:
|
||||||
|
|
||||||
|
- **Problem Statement**: What architectural challenge needs to be addressed
|
||||||
|
- **Decision**: The architectural decision made
|
||||||
|
- **Rationale**: Why this decision was chosen
|
||||||
|
- **Consequences**: Both positive and negative outcomes of the decision
|
||||||
|
- **Alternatives**: Other options that were considered
|
||||||
|
- **Implementation**: Technical details and examples
|
||||||
|
- **Status**: Current state of the decision (Draft, Approved, Superseded, etc.)
|
||||||
|
|
||||||
|
## Naming Convention
|
||||||
|
|
||||||
|
ADRs should follow this naming pattern:
|
||||||
|
|
||||||
|
```
|
||||||
|
NNNN-short-descriptive-title.md
|
||||||
|
```
|
||||||
|
|
||||||
|
Where:
|
||||||
|
- `NNNN` is a 4-digit sequential number (e.g., 0001, 0002, 0003...)
|
||||||
|
- `short-descriptive-title` uses kebab-case and briefly describes the decision
|
||||||
|
- `.md` indicates it's a Markdown file
|
||||||
|
|
||||||
|
### Examples:
|
||||||
|
- `0001-use-standalone-components.md`
|
||||||
|
- `0002-adopt-ngrx-signals.md`
|
||||||
|
- `0003-implement-micro-frontend-architecture.md`
|
||||||
|
- `0004-choose-vitest-over-jest.md`
|
||||||
|
|
||||||
|
## Process Guidelines
|
||||||
|
|
||||||
|
### 1. When to Create an ADR
|
||||||
|
|
||||||
|
Create an ADR when making decisions about:
|
||||||
|
|
||||||
|
- **Architecture patterns** (e.g., micro-frontends, monorepo structure)
|
||||||
|
- **Technology choices** (e.g., testing frameworks, state management)
|
||||||
|
- **Development practices** (e.g., code organization, build processes)
|
||||||
|
- **Technical standards** (e.g., coding conventions, performance requirements)
|
||||||
|
- **Infrastructure decisions** (e.g., deployment strategies, CI/CD processes)
|
||||||
|
|
||||||
|
### 2. ADR Lifecycle
|
||||||
|
|
||||||
|
```
|
||||||
|
Draft → Under Review → Approved → [Superseded/Deprecated]
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Draft**: Initial version, being written
|
||||||
|
- **Under Review**: Shared with team for feedback and discussion
|
||||||
|
- **Approved**: Team has agreed and decision is implemented
|
||||||
|
- **Superseded**: Replaced by a newer ADR
|
||||||
|
- **Deprecated**: No longer applicable but kept for historical reference
|
||||||
|
|
||||||
|
### 3. Creation Process
|
||||||
|
|
||||||
|
1. **Identify the Need**: Recognize an architectural decision needs documentation
|
||||||
|
2. **Create from Template**: Copy [TEMPLATE.md](./TEMPLATE.md) to create new ADR
|
||||||
|
3. **Fill in Content**: Complete all sections with relevant information
|
||||||
|
4. **Set Status to Draft**: Mark the document as "Draft" initially
|
||||||
|
5. **Share for Review**: Present to team for discussion and feedback
|
||||||
|
6. **Iterate**: Update based on team input
|
||||||
|
7. **Approve**: Once consensus is reached, mark as "Approved"
|
||||||
|
8. **Implement**: Begin implementation of the architectural decision
|
||||||
|
|
||||||
|
### 4. Review Process
|
||||||
|
|
||||||
|
- **Author Review**: Self-review for completeness and clarity
|
||||||
|
- **Peer Review**: Share with relevant team members for technical review
|
||||||
|
- **Architecture Review**: Present in architecture meetings if significant
|
||||||
|
- **Final Approval**: Get sign-off from technical leads/architects
|
||||||
|
|
||||||
|
## Angular/Nx Specific Considerations
|
||||||
|
|
||||||
|
When writing ADRs for this project, consider these Angular/Nx specific aspects:
|
||||||
|
|
||||||
|
### Architecture Decisions
|
||||||
|
- **Library organization** in the monorepo structure
|
||||||
|
- **Dependency management** between applications and libraries
|
||||||
|
- **Feature module vs. standalone component** approaches
|
||||||
|
- **State management patterns** (NgRx, Signals, Services)
|
||||||
|
- **Routing strategies** for large applications
|
||||||
|
|
||||||
|
### Technical Decisions
|
||||||
|
- **Build optimization** strategies using Nx
|
||||||
|
- **Testing approaches** for different types of libraries
|
||||||
|
- **Code sharing patterns** across applications
|
||||||
|
- **Performance optimization** techniques
|
||||||
|
- **Bundle splitting** and lazy loading strategies
|
||||||
|
|
||||||
|
### Development Workflow
|
||||||
|
- **Nx executor usage** for custom tasks
|
||||||
|
- **Generator patterns** for code scaffolding
|
||||||
|
- **Linting and formatting** configurations
|
||||||
|
- **CI/CD pipeline** optimizations using Nx affected commands
|
||||||
|
|
||||||
|
## Template Usage
|
||||||
|
|
||||||
|
### Getting Started
|
||||||
|
|
||||||
|
1. Copy the [TEMPLATE.md](./TEMPLATE.md) file
|
||||||
|
2. Rename it following the naming convention
|
||||||
|
3. Replace placeholder text with actual content
|
||||||
|
4. Focus on the "why" not just the "what"
|
||||||
|
5. Include concrete examples and code snippets
|
||||||
|
6. Consider both immediate and long-term consequences
|
||||||
|
|
||||||
|
### Key Template Sections
|
||||||
|
|
||||||
|
- **Decision**: State the architectural decision clearly and concisely
|
||||||
|
- **Context**: Provide background information and constraints
|
||||||
|
- **Consequences**: Be honest about both benefits and drawbacks
|
||||||
|
- **Implementation**: Include practical examples relevant to Angular/Nx
|
||||||
|
- **Alternatives**: Show you considered other options
|
||||||
|
|
||||||
|
## Examples of Good ADRs
|
||||||
|
|
||||||
|
Here are some example titles that would make good ADRs for this project:
|
||||||
|
|
||||||
|
- **State Management**: "0001-adopt-ngrx-signals-for-component-state.md"
|
||||||
|
- **Testing Strategy**: "0002-use-angular-testing-utilities-over-spectator.md"
|
||||||
|
- **Code Organization**: "0003-implement-domain-driven-library-structure.md"
|
||||||
|
- **Performance**: "0004-implement-lazy-loading-for-feature-modules.md"
|
||||||
|
- **Build Process**: "0005-use-nx-cloud-for-distributed-task-execution.md"
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### Writing Effective ADRs
|
||||||
|
|
||||||
|
1. **Be Concise**: Keep it focused and to the point
|
||||||
|
2. **Be Specific**: Include concrete examples and implementation details
|
||||||
|
3. **Be Honest**: Document both pros and cons honestly
|
||||||
|
4. **Be Timely**: Write ADRs close to when decisions are made
|
||||||
|
5. **Be Collaborative**: Involve relevant team members in the process
|
||||||
|
|
||||||
|
### Maintenance
|
||||||
|
|
||||||
|
- **Review Regularly**: Check ADRs during architecture reviews
|
||||||
|
- **Update Status**: Keep status current as decisions evolve
|
||||||
|
- **Link Related ADRs**: Reference connected decisions
|
||||||
|
- **Archive Outdated**: Mark superseded ADRs appropriately
|
||||||
|
|
||||||
|
### Code Examples
|
||||||
|
|
||||||
|
When including code examples:
|
||||||
|
- Use actual project syntax and patterns
|
||||||
|
- Include both TypeScript and template examples where relevant
|
||||||
|
- Show before/after scenarios for changes
|
||||||
|
- Reference specific files in the codebase when possible
|
||||||
|
|
||||||
|
## Tools and Integration
|
||||||
|
|
||||||
|
### Recommended Tools
|
||||||
|
|
||||||
|
- **Markdown Editor**: Use any markdown-capable editor
|
||||||
|
- **Version Control**: All ADRs are tracked in Git
|
||||||
|
- **Review Process**: Use PR reviews for ADR approval
|
||||||
|
- **Documentation**: Link ADRs from relevant code comments
|
||||||
|
|
||||||
|
### Integration with Development
|
||||||
|
|
||||||
|
- Reference ADR numbers in commit messages when implementing decisions
|
||||||
|
- Include ADR links in PR descriptions for architectural changes
|
||||||
|
- Update ADRs when decisions need modification
|
||||||
|
- Use ADRs as reference during code reviews
|
||||||
|
|
||||||
|
## Getting Help
|
||||||
|
|
||||||
|
### Questions or Issues?
|
||||||
|
|
||||||
|
- **Team Discussions**: Bring up in team meetings or Slack
|
||||||
|
- **Architecture Review**: Present in architecture meetings
|
||||||
|
- **Documentation**: Update this README if process improvements are needed
|
||||||
|
|
||||||
|
### Resources
|
||||||
|
|
||||||
|
- [Architecture Decision Records (ADRs) - Michael Nygard](https://cognitect.com/blog/2011/11/15/documenting-architecture-decisions)
|
||||||
|
- [ADR GitHub Organization](https://adr.github.io/)
|
||||||
|
- [Nx Documentation](https://nx.dev/getting-started/intro)
|
||||||
|
- [Angular Architecture Guide](https://angular.dev/guide/architecture)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*This ADR system helps maintain architectural consistency and knowledge sharing across the ISA-Frontend project. Keep it updated and use it regularly for the best results.*
|
||||||
138
docs/architecture/TEMPLATE.md
Normal file
138
docs/architecture/TEMPLATE.md
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
# ADR NNNN: <short-descriptive-title>
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|-------|-------|
|
||||||
|
| Status | Draft / Under Review / Approved / Superseded by ADR NNNN / Deprecated |
|
||||||
|
| Date | YYYY-MM-DD |
|
||||||
|
| Owners | <author(s)> |
|
||||||
|
| Participants | <key reviewers / stakeholders> |
|
||||||
|
| Related ADRs | NNNN (title), NNNN (title) |
|
||||||
|
| Tags | architecture, <domain>, <category> |
|
||||||
|
|
||||||
|
---
|
||||||
|
## Summary (Decision in One Sentence)
|
||||||
|
Concise statement of the architectural decision. Avoid rationale here—just the what.
|
||||||
|
|
||||||
|
## Context & Problem Statement
|
||||||
|
Describe the background and the problem this decision addresses.
|
||||||
|
- Business drivers / user needs
|
||||||
|
- Technical constraints (performance, security, scalability, compliance, legacy, regulatory)
|
||||||
|
- Current pain points / gaps
|
||||||
|
- Measurable goals / success criteria (e.g. reduce build time by 30%)
|
||||||
|
|
||||||
|
### Scope
|
||||||
|
What is in scope and explicitly out of scope for this decision.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
State the decision clearly (active voice). Include high-level approach or pattern selection, not implementation detail.
|
||||||
|
|
||||||
|
## Rationale
|
||||||
|
Why this option was selected:
|
||||||
|
- Alignment with strategic/technical direction
|
||||||
|
- Trade-offs considered
|
||||||
|
- Data, benchmarks, experiments, spikes
|
||||||
|
- Impact on developer experience / velocity
|
||||||
|
- Long-term maintainability & extensibility
|
||||||
|
|
||||||
|
## Alternatives Considered
|
||||||
|
| Alternative | Summary | Pros | Cons | Reason Not Chosen |
|
||||||
|
|-------------|---------|------|------|-------------------|
|
||||||
|
| Option A | | | | |
|
||||||
|
| Option B | | | | |
|
||||||
|
| Option C | | | | |
|
||||||
|
|
||||||
|
Add deeper detail below if needed:
|
||||||
|
### Option A – <name>
|
||||||
|
### Option B – <name>
|
||||||
|
### Option C – <name>
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
### Positive
|
||||||
|
- …
|
||||||
|
### Negative / Risks / Debt Introduced
|
||||||
|
- …
|
||||||
|
### Neutral / Open Questions
|
||||||
|
- …
|
||||||
|
|
||||||
|
## Implementation Plan
|
||||||
|
High-level rollout strategy. Break into phases if applicable.
|
||||||
|
1. Phase 0 – Spike / Validation
|
||||||
|
2. Phase 1 – Foundation / Infrastructure
|
||||||
|
3. Phase 2 – Incremental Adoption / Migration
|
||||||
|
4. Phase 3 – Hardening / Optimization
|
||||||
|
5. Phase 4 – Decommission Legacy
|
||||||
|
|
||||||
|
### Tasks / Workstreams
|
||||||
|
- Infra / tooling changes
|
||||||
|
- Library additions (Nx generators, new libs under `libs/<domain>`)
|
||||||
|
- Refactors / migrations
|
||||||
|
- Testing strategy updates (Jest → Vitest, Signals adoption, etc.)
|
||||||
|
- Documentation & onboarding materials
|
||||||
|
|
||||||
|
### Acceptance Criteria
|
||||||
|
List objective criteria to mark implementation complete.
|
||||||
|
|
||||||
|
### Rollback Plan
|
||||||
|
How to revert safely if outcomes are negative.
|
||||||
|
|
||||||
|
## Architectural Impact
|
||||||
|
### Nx / Monorepo Layout
|
||||||
|
Describe changes to library boundaries, tags, dependency graph, affected projects.
|
||||||
|
### Module / Library Design
|
||||||
|
New or modified public APIs (`src/index.ts` changes, path aliases additions to `tsconfig.base.json`).
|
||||||
|
### State Management
|
||||||
|
Implications for Signals, NgRx, resource factories, persistence patterns (`withStorage`).
|
||||||
|
### Runtime & Performance
|
||||||
|
Bundle size, lazy loading, code splitting, caching, SSR/hydration considerations.
|
||||||
|
### Security & Compliance
|
||||||
|
AuthZ/AuthN, token handling, data residency, PII, secure storage.
|
||||||
|
### Observability & Logging
|
||||||
|
Logging contexts (`@isa/core/logging`), metrics, tracing hooks.
|
||||||
|
### DX / Tooling
|
||||||
|
Generators, lint rules, schematic updates, local dev flow.
|
||||||
|
|
||||||
|
## Detailed Design Elements
|
||||||
|
(Optional deeper technical articulation.)
|
||||||
|
- Sequence diagrams / component diagrams
|
||||||
|
- Data flow / async flow
|
||||||
|
- Error handling strategy
|
||||||
|
- Concurrency / cancellation (e.g. `rxMethod` + `switchMap` usage)
|
||||||
|
- Abstractions & extension points
|
||||||
|
|
||||||
|
## Code Examples
|
||||||
|
### Before
|
||||||
|
```ts
|
||||||
|
// Previous approach (simplified)
|
||||||
|
```
|
||||||
|
### After
|
||||||
|
```ts
|
||||||
|
// New approach (simplified)
|
||||||
|
```
|
||||||
|
### Migration Snippet
|
||||||
|
```ts
|
||||||
|
// Example incremental migration pattern
|
||||||
|
```
|
||||||
|
|
||||||
|
## Open Questions / Follow-Ups
|
||||||
|
- Unresolved design clarifications
|
||||||
|
- Dependent ADRs required
|
||||||
|
- External approvals needed
|
||||||
|
|
||||||
|
## Decision Review & Revalidation
|
||||||
|
When and how this ADR will be re-evaluated (date, trigger conditions, metrics thresholds).
|
||||||
|
|
||||||
|
## Status Log
|
||||||
|
| Date | Change | Author |
|
||||||
|
|------|--------|--------|
|
||||||
|
| YYYY-MM-DD | Created (Draft) | |
|
||||||
|
| YYYY-MM-DD | Approved | |
|
||||||
|
| YYYY-MM-DD | Superseded by ADR NNNN | |
|
||||||
|
|
||||||
|
## References
|
||||||
|
- Links to spike notes, benchmark results
|
||||||
|
- External articles, standards, RFCs
|
||||||
|
- Related code PRs / commits
|
||||||
|
|
||||||
|
---
|
||||||
|
> Document updates MUST reference this ADR number in commit messages: `ADR-NNNN:` prefix.
|
||||||
|
> Keep this document updated through all lifecycle stages.
|
||||||
350
docs/architecture/adr/0001-implement-data-access-api-requests.md
Normal file
350
docs/architecture/adr/0001-implement-data-access-api-requests.md
Normal file
@@ -0,0 +1,350 @@
|
|||||||
|
# ADR 0001: Implement `data-access` API Requests
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|-------|-------|
|
||||||
|
| Status | Draft |
|
||||||
|
| Date | 29.09.2025 |
|
||||||
|
| Owners | Lorenz, Nino |
|
||||||
|
| Participants | N/A |
|
||||||
|
| Related ADRs | N/A |
|
||||||
|
| Tags | architecture, data-access, library, swagger |
|
||||||
|
|
||||||
|
---
|
||||||
|
## Summary (Decision in One Sentence)
|
||||||
|
Standardize all data-access libraries with a three-layer architecture: Zod schemas for validation, domain models extending generated DTOs, and services with consistent error handling and logging.
|
||||||
|
|
||||||
|
## Context & Problem Statement
|
||||||
|
|
||||||
|
Inconsistent patterns across data-access libraries (`catalogue`, `remission`, `crm`, `oms`) cause:
|
||||||
|
- High cognitive load when switching domains
|
||||||
|
- Duplicated validation and error handling code
|
||||||
|
- Mixed approaches to request cancellation and logging
|
||||||
|
- No standard for extending generated DTOs
|
||||||
|
|
||||||
|
**Goals:** Standardize structure, reduce boilerplate 40%, eliminate validation runtime errors, improve type safety.
|
||||||
|
|
||||||
|
**Constraints:** Must integrate with generated Swagger clients, support AbortSignal, align with `@isa/core/logging`.
|
||||||
|
|
||||||
|
**Scope:** Schema validation, model extensions, service patterns, standard exports.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Implement a **three-layer architecture** for all data-access libraries:
|
||||||
|
|
||||||
|
1. **Schema Layer** (`schemas/`): Zod schemas for input validation and type inference
|
||||||
|
- Pattern: `<Operation>Schema` with `<Operation>` and `<Operation>Input` types
|
||||||
|
- Example: `SearchItemsSchema`, `SearchItems`, `SearchItemsInput`
|
||||||
|
|
||||||
|
2. **Model Layer** (`models/`): Domain-specific interfaces extending generated DTOs
|
||||||
|
- Pattern: `interface MyModel extends GeneratedDTO { ... }`
|
||||||
|
- Use `EntityContainer<T>` for lazy-loaded relationships
|
||||||
|
|
||||||
|
3. **Service Layer** (`services/`): Injectable services integrating Swagger clients
|
||||||
|
- Pattern: Async methods with AbortSignal support
|
||||||
|
- Standardized error handling with `ResponseArgsError`
|
||||||
|
- Structured logging via `@isa/core/logging`
|
||||||
|
|
||||||
|
**Standard exports structure:**
|
||||||
|
```typescript
|
||||||
|
// src/index.ts
|
||||||
|
export * from './lib/models';
|
||||||
|
export * from './lib/schemas';
|
||||||
|
export * from './lib/services';
|
||||||
|
// Optional: stores, helpers
|
||||||
|
```
|
||||||
|
|
||||||
|
## Rationale
|
||||||
|
|
||||||
|
**Why this approach:**
|
||||||
|
- **Type Safety**: Zod provides runtime validation + compile-time types with zero manual type definitions
|
||||||
|
- **Separation of Concerns**: Clear boundaries between validation, domain logic, and API integration
|
||||||
|
- **Consistency**: Identical patterns across all domains reduce cognitive load
|
||||||
|
- **Maintainability**: Changes to generated clients don't break domain-specific enhancements
|
||||||
|
- **Developer Experience**: Auto-completion, type inference, and standardized error handling improve velocity
|
||||||
|
|
||||||
|
**Evidence supporting this decision:**
|
||||||
|
- Analysis of 4 existing data-access libraries shows these patterns emerging naturally
|
||||||
|
- `RemissionReturnReceiptService` demonstrates successful integration with logging
|
||||||
|
- `EntityContainer<T>` pattern proven effective for relationship management
|
||||||
|
- Zod validation catches input errors before API calls, reducing backend load
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
**Positive:** Consistent patterns, runtime + compile-time type safety, clear maintainability, reusable utilities, structured debugging, optimized performance.
|
||||||
|
|
||||||
|
**Negative:** Migration effort for existing libs, learning curve for Zod, ~1-2ms validation overhead, extra abstraction layer.
|
||||||
|
|
||||||
|
**Open Questions:** User-facing error message conventions, testing standards.
|
||||||
|
|
||||||
|
## Detailed Design Elements
|
||||||
|
|
||||||
|
### Schema Validation Pattern
|
||||||
|
**Structure:**
|
||||||
|
```typescript
|
||||||
|
// Input validation schema
|
||||||
|
export const SearchByTermSchema = z.object({
|
||||||
|
searchTerm: z.string().min(1, 'Search term must not be empty'),
|
||||||
|
skip: z.number().int().min(0).default(0),
|
||||||
|
take: z.number().int().min(1).max(100).default(20),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Type inference
|
||||||
|
export type SearchByTerm = z.infer<typeof SearchByTermSchema>;
|
||||||
|
export type SearchByTermInput = z.input<typeof SearchByTermSchema>;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Model Extension Pattern
|
||||||
|
**Generated DTO Extension:**
|
||||||
|
```typescript
|
||||||
|
import { ProductDTO } from '@generated/swagger/cat-search-api';
|
||||||
|
|
||||||
|
export interface Product extends ProductDTO {
|
||||||
|
name: string;
|
||||||
|
contributors: string;
|
||||||
|
catalogProductNumber: string;
|
||||||
|
// Domain-specific enhancements
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Entity Container Pattern:**
|
||||||
|
```typescript
|
||||||
|
export interface Return extends ReturnDTO {
|
||||||
|
id: number;
|
||||||
|
receipts: EntityContainer<Receipt>[]; // Lazy-loaded relationships
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Service Implementation Pattern
|
||||||
|
**Standard service structure:**
|
||||||
|
```typescript
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class DomainService {
|
||||||
|
#apiService = inject(GeneratedApiService);
|
||||||
|
#logger = logger(() => ({ service: 'DomainService' }));
|
||||||
|
|
||||||
|
async fetchData(params: InputType, abortSignal?: AbortSignal): Promise<ResultType> {
|
||||||
|
const validated = ValidationSchema.parse(params);
|
||||||
|
|
||||||
|
let req$ = this.#apiService.operation(validated);
|
||||||
|
if (abortSignal) {
|
||||||
|
req$ = req$.pipe(takeUntilAborted(abortSignal));
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await firstValueFrom(req$);
|
||||||
|
|
||||||
|
if (res.error) {
|
||||||
|
this.#logger.error('Operation failed', new Error(res.message));
|
||||||
|
throw new ResponseArgsError(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.result as ResultType;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Error Handling Strategy
|
||||||
|
1. **Input Validation**: Zod schemas validate and transform inputs
|
||||||
|
2. **API Error Handling**: Check `res.error` from generated clients
|
||||||
|
3. **Structured Logging**: Log errors with context via `@isa/core/logging`
|
||||||
|
4. **Error Propagation**: Throw `ResponseArgsError` for consistent handling
|
||||||
|
|
||||||
|
### Concurrency & Cancellation
|
||||||
|
- **AbortSignal Support**: All async operations accept optional AbortSignal
|
||||||
|
- **RxJS Integration**: Use `takeUntilAborted` operator for cancellation
|
||||||
|
- **Promise Pattern**: `firstValueFrom` prevents subscription memory leaks
|
||||||
|
- **Caching**: `@InFlight` decorator prevents duplicate concurrent requests
|
||||||
|
|
||||||
|
### Extension Points
|
||||||
|
- **Custom Decorators**: `@Cache`, `@InFlight`, `@CacheTimeToLive`
|
||||||
|
- **Schema Transformations**: Zod `.transform()` for data normalization
|
||||||
|
- **Model Inheritance**: Interface extension for domain-specific properties
|
||||||
|
- **Service Composition**: Services can depend on other domain services
|
||||||
|
|
||||||
|
## Code Examples
|
||||||
|
|
||||||
|
### Complete Data-Access Library Structure
|
||||||
|
See full examples in existing implementations:
|
||||||
|
- `libs/catalogue/data-access` - Basic patterns
|
||||||
|
- `libs/remission/data-access` - Advanced with EntityContainer
|
||||||
|
- `libs/crm/data-access` - Service examples
|
||||||
|
- `libs/oms/data-access` - Model extensions
|
||||||
|
|
||||||
|
**Quick Reference:**
|
||||||
|
```typescript
|
||||||
|
// libs/domain/data-access/src/lib/schemas/fetch-items.schema.ts
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
export const FetchItemsSchema = z.object({
|
||||||
|
categoryId: z.string().min(1),
|
||||||
|
skip: z.number().int().min(0).default(0),
|
||||||
|
take: z.number().int().min(1).max(100).default(20),
|
||||||
|
filters: z.record(z.any()).default({}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type FetchItems = z.infer<typeof FetchItemsSchema>;
|
||||||
|
export type FetchItemsInput = z.input<typeof FetchItemsSchema>;
|
||||||
|
|
||||||
|
// libs/domain/data-access/src/lib/models/item.ts
|
||||||
|
import { ItemDTO } from '@generated/swagger/domain-api';
|
||||||
|
import { EntityContainer } from '@isa/common/data-access';
|
||||||
|
import { Category } from './category';
|
||||||
|
|
||||||
|
export interface Item extends ItemDTO {
|
||||||
|
id: number;
|
||||||
|
displayName: string;
|
||||||
|
category: EntityContainer<Category>;
|
||||||
|
// Domain-specific enhancements
|
||||||
|
isAvailable: boolean;
|
||||||
|
formattedPrice: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Service
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class ItemService {
|
||||||
|
#itemService = inject(GeneratedItemService);
|
||||||
|
#logger = logger(() => ({ service: 'ItemService' }));
|
||||||
|
|
||||||
|
async fetchItems(
|
||||||
|
params: FetchItemsInput,
|
||||||
|
abortSignal?: AbortSignal
|
||||||
|
): Promise<Item[]> {
|
||||||
|
this.#logger.debug('Fetching items', () => ({ params }));
|
||||||
|
|
||||||
|
const { categoryId, skip, take, filters } = FetchItemsSchema.parse(params);
|
||||||
|
|
||||||
|
let req$ = this.#itemService.getItems({
|
||||||
|
categoryId,
|
||||||
|
queryToken: { skip, take, filter: filters }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (abortSignal) {
|
||||||
|
req$ = req$.pipe(takeUntilAborted(abortSignal));
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await firstValueFrom(req$);
|
||||||
|
|
||||||
|
if (res.error) {
|
||||||
|
this.#logger.error('Failed to fetch items', new Error(res.message));
|
||||||
|
throw new ResponseArgsError(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#logger.info('Successfully fetched items', () => ({
|
||||||
|
count: res.result?.length || 0
|
||||||
|
}));
|
||||||
|
|
||||||
|
return res.result as Item[];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// libs/domain/data-access/src/index.ts
|
||||||
|
export * from './lib/models';
|
||||||
|
export * from './lib/schemas';
|
||||||
|
export * from './lib/services';
|
||||||
|
```
|
||||||
|
|
||||||
|
### Usage in Feature Components
|
||||||
|
```typescript
|
||||||
|
// feature component using the data-access library
|
||||||
|
import { Component, inject, signal } from '@angular/core';
|
||||||
|
import { ItemService, Item, FetchItemsInput } from '@isa/domain/data-access';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-item-list',
|
||||||
|
template: `
|
||||||
|
@if (loading()) {
|
||||||
|
<div>Loading...</div>
|
||||||
|
} @else {
|
||||||
|
@for (item of items(); track item.id) {
|
||||||
|
<div>{{ item.displayName }}</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
})
|
||||||
|
export class ItemListComponent {
|
||||||
|
#itemService = inject(ItemService);
|
||||||
|
|
||||||
|
items = signal<Item[]>([]);
|
||||||
|
loading = signal(false);
|
||||||
|
|
||||||
|
async loadItems(categoryId: string) {
|
||||||
|
this.loading.set(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const params: FetchItemsInput = {
|
||||||
|
categoryId,
|
||||||
|
take: 50,
|
||||||
|
filters: { active: true }
|
||||||
|
};
|
||||||
|
|
||||||
|
const items = await this.#itemService.fetchItems(params);
|
||||||
|
this.items.set(items);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load items', error);
|
||||||
|
} finally {
|
||||||
|
this.loading.set(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Migration Pattern for Existing Services
|
||||||
|
```typescript
|
||||||
|
// Before: Direct HTTP client usage
|
||||||
|
@Injectable()
|
||||||
|
export class LegacyItemService {
|
||||||
|
constructor(private http: HttpClient) {}
|
||||||
|
|
||||||
|
getItems(categoryId: string): Observable<any> {
|
||||||
|
return this.http.get(`/api/items?category=${categoryId}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// After: Standardized data-access pattern
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class ItemService {
|
||||||
|
#itemService = inject(GeneratedItemService);
|
||||||
|
#logger = logger(() => ({ service: 'ItemService' }));
|
||||||
|
|
||||||
|
async fetchItems(params: FetchItemsInput, abortSignal?: AbortSignal): Promise<Item[]> {
|
||||||
|
const validated = FetchItemsSchema.parse(params);
|
||||||
|
// ... standard implementation pattern
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Open Questions / Follow-Ups
|
||||||
|
- Unresolved design clarifications
|
||||||
|
- Dependent ADRs required
|
||||||
|
- External approvals needed
|
||||||
|
|
||||||
|
## Decision Review & Revalidation
|
||||||
|
When and how this ADR will be re-evaluated (date, trigger conditions, metrics thresholds).
|
||||||
|
|
||||||
|
## Status Log
|
||||||
|
| Date | Change | Author |
|
||||||
|
|------|--------|--------|
|
||||||
|
| 2025-10-02 | Condensed for readability | Lorenz, Nino |
|
||||||
|
| 2025-09-29 | Created (Draft) | Lorenz |
|
||||||
|
| 2025-09-25 | Analysis completed, comprehensive patterns documented | Lorenz, Nino |
|
||||||
|
|
||||||
|
## References
|
||||||
|
**Existing Implementation Examples:**
|
||||||
|
- `libs/catalogue/data-access` - Basic schema and service patterns
|
||||||
|
- `libs/remission/data-access` - Advanced patterns with EntityContainer and stores
|
||||||
|
- `libs/common/data-access` - Shared utilities and response types
|
||||||
|
- `generated/swagger/` - Generated API clients integration
|
||||||
|
|
||||||
|
**Key Dependencies:**
|
||||||
|
- [Zod](https://github.com/colinhacks/zod) - Schema validation library
|
||||||
|
- [ng-swagger-gen](https://github.com/cyclosproject/ng-swagger-gen) - OpenAPI client generation
|
||||||
|
- `@isa/core/logging` - Structured logging infrastructure
|
||||||
|
- `@isa/common/data-access` - Shared utilities and types
|
||||||
|
|
||||||
|
**Related Documentation:**
|
||||||
|
- ISA Frontend Copilot Instructions - Data-access patterns
|
||||||
|
- Tech Stack Documentation - Architecture overview
|
||||||
|
- Code Style Guidelines - TypeScript and Angular patterns
|
||||||
|
|
||||||
|
---
|
||||||
|
> Document updates MUST reference this ADR number in commit messages: `ADR-NNNN:` prefix.
|
||||||
|
> Keep this document updated through all lifecycle stages.
|
||||||
@@ -1,128 +1,136 @@
|
|||||||
# Tech Stack Documentation
|
# Tech Stack Documentation
|
||||||
|
|
||||||
## Core Technologies
|
## Core Technologies
|
||||||
|
|
||||||
### Frontend Framework
|
### Frontend Framework
|
||||||
|
|
||||||
- **[Angular](https://angular.dev/overview)** (Latest Version)
|
- **[Angular](https://angular.dev/overview)** (Latest Version)
|
||||||
- Modern web framework for building scalable single-page applications
|
- Modern web framework for building scalable single-page applications
|
||||||
- Leverages TypeScript for enhanced type safety and developer experience
|
- Leverages TypeScript for enhanced type safety and developer experience
|
||||||
|
|
||||||
### State Management
|
### State Management
|
||||||
|
|
||||||
- **[NgRx](https://ngrx.io/docs)**
|
- **[NgRx](https://ngrx.io/docs)**
|
||||||
- Redux-inspired state management for Angular applications
|
- Redux-inspired state management for Angular applications
|
||||||
- Provides predictable state container and powerful dev tools
|
- Provides predictable state container and powerful dev tools
|
||||||
- Used for managing complex application state and side effects
|
- Used for managing complex application state and side effects
|
||||||
|
|
||||||
### Styling
|
### Styling
|
||||||
|
|
||||||
- **[Tailwind CSS](https://tailwindcss.com/)**
|
- **[Tailwind CSS](https://tailwindcss.com/)**
|
||||||
- Utility-first CSS framework
|
- Utility-first CSS framework
|
||||||
- Enables rapid UI development with pre-built classes
|
- Enables rapid UI development with pre-built classes
|
||||||
- Highly customizable through configuration
|
- Highly customizable through configuration
|
||||||
|
|
||||||
## Development Tools
|
## Development Tools
|
||||||
|
|
||||||
### Language
|
### Language
|
||||||
|
|
||||||
- **[TypeScript](https://www.typescriptlang.org/docs/handbook/intro.html)**
|
- **[TypeScript](https://www.typescriptlang.org/docs/handbook/intro.html)**
|
||||||
- Strongly typed programming language
|
- Strongly typed programming language
|
||||||
- Provides enhanced IDE support and compile-time error checking
|
- Provides enhanced IDE support and compile-time error checking
|
||||||
- Used throughout the entire application
|
- Used throughout the entire application
|
||||||
|
|
||||||
### Runtime
|
### Runtime
|
||||||
|
|
||||||
- **[Node.js](https://nodejs.org/docs/latest-v22.x/api/index.html)**
|
- **[Node.js](https://nodejs.org/docs/latest-v22.x/api/index.html)**
|
||||||
- JavaScript runtime environment
|
- JavaScript runtime environment
|
||||||
- Used for development server and build tools
|
- Used for development server and build tools
|
||||||
- Required for running npm scripts and development tools
|
- Required for running npm scripts and development tools
|
||||||
|
|
||||||
### Testing Framework
|
### Testing Framework
|
||||||
|
|
||||||
- **[Jest](https://jestjs.io/docs/getting-started)**
|
- **[Jest](https://jestjs.io/docs/getting-started)**
|
||||||
|
|
||||||
- Primary testing framework
|
- Primary testing framework
|
||||||
- Used for unit and integration tests
|
- Used for unit and integration tests
|
||||||
- Provides snapshot testing capabilities
|
- Provides snapshot testing capabilities
|
||||||
|
|
||||||
- **[Spectator](https://ngneat.github.io/spectator/)**
|
- **[Spectator](https://ngneat.github.io/spectator/)**
|
||||||
- Angular testing utility library
|
- Angular testing utility library
|
||||||
- Simplifies component testing
|
- Simplifies component testing
|
||||||
- Reduces boilerplate in test files
|
- Reduces boilerplate in test files
|
||||||
|
|
||||||
### UI Development
|
### UI Development
|
||||||
|
|
||||||
- **[Storybook](https://storybook.js.org/docs/get-started/frameworks/angular)**
|
- **[Storybook](https://storybook.js.org/docs/get-started/frameworks/angular)**
|
||||||
- UI component development environment
|
- UI component development environment
|
||||||
- Enables isolated component development
|
- Enables isolated component development
|
||||||
- Serves as living documentation for components
|
- Serves as living documentation for components
|
||||||
|
|
||||||
### Utilities
|
### Utilities
|
||||||
|
|
||||||
- **[date-fns](https://date-fns.org/docs/Getting-Started)**
|
- **[date-fns](https://date-fns.org/docs/Getting-Started)**
|
||||||
- Modern JavaScript date utility library
|
- Modern JavaScript date utility library
|
||||||
- Used for consistent date formatting and manipulation
|
- Used for consistent date formatting and manipulation
|
||||||
- Tree-shakeable to optimize bundle size
|
- Tree-shakeable to optimize bundle size
|
||||||
- **[Lodash](https://lodash.com/)**
|
- **[Lodash](https://lodash.com/)**
|
||||||
- Utility library for common JavaScript tasks
|
- Utility library for common JavaScript tasks
|
||||||
- **[UUID](https://www.npmjs.com/package/uuid)**
|
- **[UUID](https://www.npmjs.com/package/uuid)**
|
||||||
- Generates unique identifiers
|
- Generates unique identifiers
|
||||||
- **[Zod](https://github.com/colinhacks/zod)**
|
- **[Zod](https://github.com/colinhacks/zod)**
|
||||||
- TypeScript-first schema validation library
|
- TypeScript-first schema validation library
|
||||||
|
|
||||||
## Additional Technical Areas
|
## Additional Technical Areas
|
||||||
|
|
||||||
### Authentication & Authorization
|
### Authentication & Authorization
|
||||||
|
|
||||||
- **[angular-oauth2-oidc](https://github.com/manfredsteyer/angular-oauth2-oidc)**
|
- **[angular-oauth2-oidc](https://github.com/manfredsteyer/angular-oauth2-oidc)**
|
||||||
- Simplifies implementing OAuth2 and OIDC authentication in Angular.
|
- Simplifies implementing OAuth2 and OIDC authentication in Angular.
|
||||||
- **[angular-oauth2-oidc-jwks](https://github.com/manfredsteyer/angular-oauth2-oidc)**
|
- **[angular-oauth2-oidc-jwks](https://github.com/manfredsteyer/angular-oauth2-oidc)**
|
||||||
- Adds JWKS support for secure token management.
|
- Adds JWKS support for secure token management.
|
||||||
|
|
||||||
### Real-Time Communication
|
### Real-Time Communication
|
||||||
|
|
||||||
- **[@microsoft/signalr](https://www.npmjs.com/package/@microsoft/signalr)**
|
- **[@microsoft/signalr](https://www.npmjs.com/package/@microsoft/signalr)**
|
||||||
- Provides real-time communication between client and server components.
|
- Provides real-time communication between client and server components.
|
||||||
|
|
||||||
### Barcode Scanning
|
### Barcode Scanning
|
||||||
|
|
||||||
- **[Scandit Web Data Capture Barcode](https://www.scandit.com/documentation/web/)**
|
- **[Scandit Web Data Capture Barcode](https://www.scandit.com/documentation/web/)**
|
||||||
- Robust barcode scanning capabilities integrated into the application.
|
- Robust barcode scanning capabilities integrated into the application.
|
||||||
- **[Scandit Web Data Capture Core](https://www.scandit.com/documentation/web/)**
|
- **[Scandit Web Data Capture Core](https://www.scandit.com/documentation/web/)**
|
||||||
- Core library supporting the barcode scanning features.
|
- Core library supporting the barcode scanning features.
|
||||||
|
|
||||||
### Tooling
|
### Tooling
|
||||||
|
|
||||||
- **[Nx](https://nx.dev/)**
|
- **[Nx](https://nx.dev/)**
|
||||||
- Powerful monorepo tool for Angular and other frontend applications.
|
- Powerful monorepo tool for Angular and other frontend applications.
|
||||||
- **[Husky](https://typicode.github.io/husky/#/)**
|
- **[Husky](https://typicode.github.io/husky/#/)**
|
||||||
- Manages Git hooks for consistent developer workflows.
|
- Manages Git hooks for consistent developer workflows.
|
||||||
- **[ESLint](https://eslint.org/) & [Prettier](https://prettier.io/)**
|
- **[ESLint](https://eslint.org/) & [Prettier](https://prettier.io/)**
|
||||||
- Linting and formatting tools to maintain consistent code quality.
|
- Linting and formatting tools to maintain consistent code quality.
|
||||||
- **[Storybook](https://storybook.js.org/)**
|
- **[Storybook](https://storybook.js.org/)**
|
||||||
- Isolated component development and living documentation environment.
|
- Isolated component development and living documentation environment.
|
||||||
|
|
||||||
## Development Environment Setup
|
## Domain Libraries
|
||||||
|
|
||||||
1. **Required Software**
|
### Customer Relationship Management (CRM)
|
||||||
|
|
||||||
- Node.js (Latest LTS version)
|
- **`@isa/crm/data-access`**
|
||||||
- npm (comes with Node.js)
|
- Handles data access logic for customer-related features.
|
||||||
- Git
|
- Contains services for fetching and managing customer data.
|
||||||
|
|
||||||
2. **IDE Recommendations**
|
## Development Environment Setup
|
||||||
|
|
||||||
- Visual Studio Code with following extensions:
|
1. **Required Software**
|
||||||
- Angular Language Service
|
|
||||||
- ESLint
|
- Node.js (Latest LTS version)
|
||||||
- Prettier
|
- npm (comes with Node.js)
|
||||||
- Tailwind CSS IntelliSense
|
- Git
|
||||||
|
|
||||||
3. **Getting Started**
|
2. **IDE Recommendations**
|
||||||
```bash
|
|
||||||
npm install # Install dependencies
|
- Visual Studio Code with following extensions:
|
||||||
npm run start # Start development server
|
- Angular Language Service
|
||||||
npm run test # Run tests
|
- ESLint
|
||||||
npm run storybook # Start Storybook
|
- Prettier
|
||||||
```
|
- Tailwind CSS IntelliSense
|
||||||
|
|
||||||
|
3. **Getting Started**
|
||||||
|
```bash
|
||||||
|
npm install # Install dependencies
|
||||||
|
npm run start # Start development server
|
||||||
|
npm run test # Run tests
|
||||||
|
npm run storybook # Start Storybook
|
||||||
|
```
|
||||||
|
|||||||
@@ -31,8 +31,9 @@ const PARAMETER_CODEC = new ParameterCodec();
|
|||||||
export class BaseService {
|
export class BaseService {
|
||||||
constructor(
|
constructor(
|
||||||
protected config: CatConfiguration,
|
protected config: CatConfiguration,
|
||||||
protected http: HttpClient,
|
protected http: HttpClient
|
||||||
) {}
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
private _rootUrl: string = '';
|
private _rootUrl: string = '';
|
||||||
|
|
||||||
@@ -56,7 +57,7 @@ export class BaseService {
|
|||||||
*/
|
*/
|
||||||
protected newParams(): HttpParams {
|
protected newParams(): HttpParams {
|
||||||
return new HttpParams({
|
return new HttpParams({
|
||||||
encoder: PARAMETER_CODEC,
|
encoder: PARAMETER_CODEC
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
* Auocomplete-Ergebnis
|
* Auocomplete-Ergebnis
|
||||||
*/
|
*/
|
||||||
export interface AutocompleteDTO {
|
export interface AutocompleteDTO {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Anzeige / Bezeichner
|
* Anzeige / Bezeichner
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { CatalogType } from './catalog-type';
|
|||||||
* Suchabfrage
|
* Suchabfrage
|
||||||
*/
|
*/
|
||||||
export interface AutocompleteTokenDTO {
|
export interface AutocompleteTokenDTO {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Katalogbereich
|
* Katalogbereich
|
||||||
*/
|
*/
|
||||||
@@ -13,7 +14,7 @@ export interface AutocompleteTokenDTO {
|
|||||||
/**
|
/**
|
||||||
* Filter
|
* Filter
|
||||||
*/
|
*/
|
||||||
filter?: { [key: string]: string };
|
filter?: {[key: string]: string};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Eingabe
|
* Eingabe
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { AvailabilityType } from './availability-type';
|
|||||||
* Verfügbarkeit
|
* Verfügbarkeit
|
||||||
*/
|
*/
|
||||||
export interface AvailabilityDTO {
|
export interface AvailabilityDTO {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Voraussichtliches Lieferdatum
|
* Voraussichtliches Lieferdatum
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
export type AvailabilityType = 0 | 1 | 2 | 32 | 256 | 512 | 1024 | 2048 | 4096 | 8192 | 16384;
|
export type AvailabilityType = 0 | 1 | 2 | 32 | 256 | 512 | 1024 | 2048 | 4096 | 8192 | 16384;
|
||||||
@@ -1,2 +1,2 @@
|
|||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
export type Avoirdupois = 0 | 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128 | 256 | 512 | 1024 | 2048 | 4096;
|
export type Avoirdupois = 0 | 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128 | 256 | 512 | 1024 | 2048 | 4096;
|
||||||
@@ -3,4 +3,4 @@
|
|||||||
/**
|
/**
|
||||||
* Katalogbereich
|
* Katalogbereich
|
||||||
*/
|
*/
|
||||||
export type CatalogType = 0 | 1 | 2 | 4;
|
export type CatalogType = 0 | 1 | 2 | 4;
|
||||||
@@ -1,2 +1,2 @@
|
|||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
export type CRUDA = 0 | 1 | 2 | 4 | 8 | 16;
|
export type CRUDA = 0 | 1 | 2 | 4 | 8 | 16;
|
||||||
@@ -1,2 +1,2 @@
|
|||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
export type DialogContentType = 0 | 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128;
|
export type DialogContentType = 0 | 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128;
|
||||||
@@ -1,2 +1,2 @@
|
|||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
export type DialogSettings = 0 | 1 | 2 | 4;
|
export type DialogSettings = 0 | 1 | 2 | 4;
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
import { TouchedBase } from './touched-base';
|
import { TouchedBase } from './touched-base';
|
||||||
import { CRUDA } from './cruda';
|
import { CRUDA } from './cruda';
|
||||||
import { EntityStatus } from './entity-status';
|
import { EntityStatus } from './entity-status';
|
||||||
export interface EntityDTO extends TouchedBase {
|
export interface EntityDTO extends TouchedBase{
|
||||||
changed?: string;
|
changed?: string;
|
||||||
created?: string;
|
created?: string;
|
||||||
cruda?: CRUDA;
|
cruda?: CRUDA;
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
export type EntityStatus = 0 | 1 | 2 | 4 | 8;
|
export type EntityStatus = 0 | 1 | 2 | 4 | 8;
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
* Bild
|
* Bild
|
||||||
*/
|
*/
|
||||||
export interface ImageDTO {
|
export interface ImageDTO {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Copyright
|
* Copyright
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
export type InputType = 0 | 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128 | 256 | 512 | 1024 | 2048 | 3072 | 4096 | 8192 | 12288;
|
export type InputType = 0 | 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128 | 256 | 512 | 1024 | 2048 | 3072 | 4096 | 8192 | 12288;
|
||||||
@@ -11,7 +11,8 @@ import { StockInfoDTO } from './stock-info-dto';
|
|||||||
import { Successor } from './successor';
|
import { Successor } from './successor';
|
||||||
import { TextDTO } from './text-dto';
|
import { TextDTO } from './text-dto';
|
||||||
import { ItemType } from './item-type';
|
import { ItemType } from './item-type';
|
||||||
export interface ItemDTO extends EntityDTO {
|
export interface ItemDTO extends EntityDTO{
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Verfügbarkeit laut Katalog
|
* Verfügbarkeit laut Katalog
|
||||||
*/
|
*/
|
||||||
@@ -30,7 +31,7 @@ export interface ItemDTO extends EntityDTO {
|
|||||||
/**
|
/**
|
||||||
* Weitere Artikel-IDs
|
* Weitere Artikel-IDs
|
||||||
*/
|
*/
|
||||||
ids?: { [key: string]: number };
|
ids?: {[key: string]: number};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Primary image / Id des Hauptbilds
|
* Primary image / Id des Hauptbilds
|
||||||
@@ -45,7 +46,7 @@ export interface ItemDTO extends EntityDTO {
|
|||||||
/**
|
/**
|
||||||
* Markierungen (Lesezeichen) wie (BOD, Prämie)
|
* Markierungen (Lesezeichen) wie (BOD, Prämie)
|
||||||
*/
|
*/
|
||||||
labels?: { [key: string]: string };
|
labels?: {[key: string]: string};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Produkt-Stammdaten
|
* Produkt-Stammdaten
|
||||||
|
|||||||
@@ -3,4 +3,4 @@
|
|||||||
/**
|
/**
|
||||||
* Artikel-/Produkttyp
|
* Artikel-/Produkttyp
|
||||||
*/
|
*/
|
||||||
export type ItemType = 0 | 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128 | 256 | 512 | 1024 | 2048 | 4096 | 8192 | 16384 | 32768 | 65536;
|
export type ItemType = 0 | 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128 | 256 | 512 | 1024 | 2048 | 4096 | 8192 | 16384 | 32768 | 65536;
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
export interface LesepunkteRequest {
|
export interface LesepunkteRequest {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Artikel ID
|
* Artikel ID
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
import { ResponseArgsOfIEnumerableOfAutocompleteDTO } from './response-args-of-ienumerable-of-autocomplete-dto';
|
import { ResponseArgsOfIEnumerableOfAutocompleteDTO } from './response-args-of-ienumerable-of-autocomplete-dto';
|
||||||
export interface ListResponseArgsOfAutocompleteDTO extends ResponseArgsOfIEnumerableOfAutocompleteDTO {
|
export interface ListResponseArgsOfAutocompleteDTO extends ResponseArgsOfIEnumerableOfAutocompleteDTO{
|
||||||
completed?: boolean;
|
completed?: boolean;
|
||||||
hits?: number;
|
hits?: number;
|
||||||
skip?: number;
|
skip?: number;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
import { ResponseArgsOfIEnumerableOfItemDTO } from './response-args-of-ienumerable-of-item-dto';
|
import { ResponseArgsOfIEnumerableOfItemDTO } from './response-args-of-ienumerable-of-item-dto';
|
||||||
export interface ListResponseArgsOfItemDTO extends ResponseArgsOfIEnumerableOfItemDTO {
|
export interface ListResponseArgsOfItemDTO extends ResponseArgsOfIEnumerableOfItemDTO{
|
||||||
completed?: boolean;
|
completed?: boolean;
|
||||||
hits?: number;
|
hits?: number;
|
||||||
skip?: number;
|
skip?: number;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { TouchedBase } from './touched-base';
|
import { TouchedBase } from './touched-base';
|
||||||
import { PriceValueDTO } from './price-value-dto';
|
import { PriceValueDTO } from './price-value-dto';
|
||||||
import { VATValueDTO } from './vatvalue-dto';
|
import { VATValueDTO } from './vatvalue-dto';
|
||||||
export interface PriceDTO extends TouchedBase {
|
export interface PriceDTO extends TouchedBase{
|
||||||
value?: PriceValueDTO;
|
value?: PriceValueDTO;
|
||||||
vat?: VATValueDTO;
|
vat?: VATValueDTO;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
import { TouchedBase } from './touched-base';
|
import { TouchedBase } from './touched-base';
|
||||||
export interface PriceValueDTO extends TouchedBase {
|
export interface PriceValueDTO extends TouchedBase{
|
||||||
currency?: string;
|
currency?: string;
|
||||||
currencySymbol?: string;
|
currencySymbol?: string;
|
||||||
value?: number;
|
value?: number;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
export interface ProblemDetails {
|
export interface ProblemDetails {
|
||||||
detail?: string;
|
detail?: string;
|
||||||
extensions: { [key: string]: any };
|
extensions: {[key: string]: any};
|
||||||
instance?: string;
|
instance?: string;
|
||||||
status?: number;
|
status?: number;
|
||||||
title?: string;
|
title?: string;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { TouchedBase } from './touched-base';
|
import { TouchedBase } from './touched-base';
|
||||||
import { SizeOfString } from './size-of-string';
|
import { SizeOfString } from './size-of-string';
|
||||||
import { WeightOfAvoirdupois } from './weight-of-avoirdupois';
|
import { WeightOfAvoirdupois } from './weight-of-avoirdupois';
|
||||||
export interface ProductDTO extends TouchedBase {
|
export interface ProductDTO extends TouchedBase{
|
||||||
additionalName?: string;
|
additionalName?: string;
|
||||||
catalogProductNumber?: string;
|
catalogProductNumber?: string;
|
||||||
contributors?: string;
|
contributors?: string;
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
import { QueryTokenDTO2 } from './query-token-dto2';
|
import { QueryTokenDTO2 } from './query-token-dto2';
|
||||||
import { CatalogType } from './catalog-type';
|
import { CatalogType } from './catalog-type';
|
||||||
export interface QueryTokenDTO extends QueryTokenDTO2 {
|
export interface QueryTokenDTO extends QueryTokenDTO2{
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Katalogbereich
|
* Katalogbereich
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
import { OrderByDTO } from './order-by-dto';
|
import { OrderByDTO } from './order-by-dto';
|
||||||
export interface QueryTokenDTO2 {
|
export interface QueryTokenDTO2 {
|
||||||
filter?: { [key: string]: string };
|
filter?: {[key: string]: string};
|
||||||
friendlyName?: string;
|
friendlyName?: string;
|
||||||
fuzzy?: number;
|
fuzzy?: number;
|
||||||
hitsOnly?: boolean;
|
hitsOnly?: boolean;
|
||||||
ids?: Array<number>;
|
ids?: Array<number>;
|
||||||
input?: { [key: string]: string };
|
input?: {[key: string]: string};
|
||||||
options?: { [key: string]: string };
|
options?: {[key: string]: string};
|
||||||
orderBy?: Array<OrderByDTO>;
|
orderBy?: Array<OrderByDTO>;
|
||||||
skip?: number;
|
skip?: number;
|
||||||
take?: number;
|
take?: number;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
import { ResponseArgs } from './response-args';
|
import { ResponseArgs } from './response-args';
|
||||||
export interface ResponseArgsOfIDictionaryOfLongAndNullableInteger extends ResponseArgs {
|
export interface ResponseArgsOfIDictionaryOfLongAndNullableInteger extends ResponseArgs{
|
||||||
result?: { [key: string]: number };
|
result?: {[key: string]: number};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
import { ResponseArgs } from './response-args';
|
import { ResponseArgs } from './response-args';
|
||||||
import { AutocompleteDTO } from './autocomplete-dto';
|
import { AutocompleteDTO } from './autocomplete-dto';
|
||||||
export interface ResponseArgsOfIEnumerableOfAutocompleteDTO extends ResponseArgs {
|
export interface ResponseArgsOfIEnumerableOfAutocompleteDTO extends ResponseArgs{
|
||||||
result?: Array<AutocompleteDTO>;
|
result?: Array<AutocompleteDTO>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
import { ResponseArgs } from './response-args';
|
import { ResponseArgs } from './response-args';
|
||||||
import { InputGroupDTO } from './input-group-dto';
|
import { InputGroupDTO } from './input-group-dto';
|
||||||
export interface ResponseArgsOfIEnumerableOfInputGroupDTO extends ResponseArgs {
|
export interface ResponseArgsOfIEnumerableOfInputGroupDTO extends ResponseArgs{
|
||||||
result?: Array<InputGroupDTO>;
|
result?: Array<InputGroupDTO>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
import { ResponseArgs } from './response-args';
|
import { ResponseArgs } from './response-args';
|
||||||
import { ItemDTO } from './item-dto';
|
import { ItemDTO } from './item-dto';
|
||||||
export interface ResponseArgsOfIEnumerableOfItemDTO extends ResponseArgs {
|
export interface ResponseArgsOfIEnumerableOfItemDTO extends ResponseArgs{
|
||||||
result?: Array<ItemDTO>;
|
result?: Array<ItemDTO>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
import { ResponseArgs } from './response-args';
|
import { ResponseArgs } from './response-args';
|
||||||
import { OrderByDTO } from './order-by-dto';
|
import { OrderByDTO } from './order-by-dto';
|
||||||
export interface ResponseArgsOfIEnumerableOfOrderByDTO extends ResponseArgs {
|
export interface ResponseArgsOfIEnumerableOfOrderByDTO extends ResponseArgs{
|
||||||
result?: Array<OrderByDTO>;
|
result?: Array<OrderByDTO>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
import { ResponseArgs } from './response-args';
|
import { ResponseArgs } from './response-args';
|
||||||
import { QueryTokenDTO } from './query-token-dto';
|
import { QueryTokenDTO } from './query-token-dto';
|
||||||
export interface ResponseArgsOfIEnumerableOfQueryTokenDTO extends ResponseArgs {
|
export interface ResponseArgsOfIEnumerableOfQueryTokenDTO extends ResponseArgs{
|
||||||
result?: Array<QueryTokenDTO>;
|
result?: Array<QueryTokenDTO>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
import { ResponseArgs } from './response-args';
|
import { ResponseArgs } from './response-args';
|
||||||
import { ItemDTO } from './item-dto';
|
import { ItemDTO } from './item-dto';
|
||||||
export interface ResponseArgsOfItemDTO extends ResponseArgs {
|
export interface ResponseArgsOfItemDTO extends ResponseArgs{
|
||||||
result?: ItemDTO;
|
result?: ItemDTO;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
import { ResponseArgs } from './response-args';
|
import { ResponseArgs } from './response-args';
|
||||||
import { UISettingsDTO } from './uisettings-dto';
|
import { UISettingsDTO } from './uisettings-dto';
|
||||||
export interface ResponseArgsOfUISettingsDTO extends ResponseArgs {
|
export interface ResponseArgsOfUISettingsDTO extends ResponseArgs{
|
||||||
result?: UISettingsDTO;
|
result?: UISettingsDTO;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { DialogOfString } from './dialog-of-string';
|
|||||||
export interface ResponseArgs {
|
export interface ResponseArgs {
|
||||||
dialog?: DialogOfString;
|
dialog?: DialogOfString;
|
||||||
error: boolean;
|
error: boolean;
|
||||||
invalidProperties?: { [key: string]: string };
|
invalidProperties?: {[key: string]: string};
|
||||||
message?: string;
|
message?: string;
|
||||||
requestId?: number;
|
requestId?: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
export interface ReviewDTO {
|
export interface ReviewDTO {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Autor
|
* Autor
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
* Regalinfo
|
* Regalinfo
|
||||||
*/
|
*/
|
||||||
export interface ShelfInfoDTO {
|
export interface ShelfInfoDTO {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sortiment
|
* Sortiment
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -3,4 +3,5 @@
|
|||||||
/**
|
/**
|
||||||
* Shop
|
* Shop
|
||||||
*/
|
*/
|
||||||
export interface ShopDTO {}
|
export interface ShopDTO {
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
* Eigenchaften
|
* Eigenchaften
|
||||||
*/
|
*/
|
||||||
export interface SpecDTO {
|
export interface SpecDTO {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* PK
|
* PK
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { StockStatus } from './stock-status';
|
|||||||
* Bestandsinformation
|
* Bestandsinformation
|
||||||
*/
|
*/
|
||||||
export interface StockInfoDTO {
|
export interface StockInfoDTO {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Filiale PK
|
* Filiale PK
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -3,4 +3,4 @@
|
|||||||
/**
|
/**
|
||||||
* Dispositionsstatus
|
* Dispositionsstatus
|
||||||
*/
|
*/
|
||||||
export type StockStatus = 0 | 1 | 2 | 3 | 4;
|
export type StockStatus = 0 | 1 | 2 | 3 | 4;
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
import { ProductDTO } from './product-dto';
|
import { ProductDTO } from './product-dto';
|
||||||
export interface Successor extends ProductDTO {
|
export interface Successor extends ProductDTO{
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* PK
|
* PK
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
export interface TextDTO {
|
export interface TextDTO {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* PK
|
* PK
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
export interface TouchedBase {}
|
export interface TouchedBase {
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
export interface TranslationDTO {
|
export interface TranslationDTO {
|
||||||
target?: string;
|
target?: string;
|
||||||
values?: { [key: string]: string };
|
values?: {[key: string]: string};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
import { QuerySettingsDTO } from './query-settings-dto';
|
import { QuerySettingsDTO } from './query-settings-dto';
|
||||||
import { TranslationDTO } from './translation-dto';
|
import { TranslationDTO } from './translation-dto';
|
||||||
export interface UISettingsDTO extends QuerySettingsDTO {
|
export interface UISettingsDTO extends QuerySettingsDTO{
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Url Template für Detail-Bild
|
* Url Template für Detail-Bild
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
export type VATType = 0 | 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128;
|
export type VATType = 0 | 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128;
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user