mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-28 22:42:11 +01:00
Compare commits
119 Commits
feature/52
...
feature/52
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5bddaa83d5 | ||
|
|
dd9c60e0c0 | ||
|
|
0d58a5288e | ||
|
|
384952413b | ||
|
|
09677e8df8 | ||
|
|
e6dc08007b | ||
|
|
54aa18a3a3 | ||
|
|
d70c95743c | ||
|
|
9c8c42da69 | ||
|
|
afc6351509 | ||
|
|
2fc83cd8f7 | ||
|
|
9f775e01e2 | ||
|
|
c5d057e3a7 | ||
|
|
e5c09c030c | ||
|
|
0269473a18 | ||
|
|
707802ce0d | ||
|
|
e00de7598d | ||
|
|
516b7748c2 | ||
|
|
cffa7721bc | ||
|
|
066ab5d5be | ||
|
|
3bbf79a3c3 | ||
|
|
357485e32f | ||
|
|
39984342a6 | ||
|
|
c52f18e979 | ||
|
|
e58ec93087 | ||
|
|
4e6204817d | ||
|
|
c41355bcdf | ||
|
|
fa8e601660 | ||
|
|
708ec01704 | ||
|
|
332699ca74 | ||
|
|
3b0a63a53a | ||
|
|
327fdc745d | ||
|
|
297ec9100d | ||
|
|
298ab1acbe | ||
|
|
fe77a0ea8b | ||
|
|
48f588f53b | ||
|
|
7f4af304ac | ||
|
|
643b2b0e60 | ||
|
|
cd1ff5f277 | ||
|
|
46c70cae3e | ||
|
|
d2dcf638e3 | ||
|
|
a4241cbd7a | ||
|
|
dd3705f8bc | ||
|
|
514715589b | ||
|
|
0740273dbc | ||
|
|
bbb9c5d39c | ||
|
|
f0bd957a07 | ||
|
|
e4f289c67d | ||
|
|
2af16d92ea | ||
|
|
99e8e7cfe0 | ||
|
|
ac728f2dd9 | ||
|
|
2e012a124a | ||
|
|
d22e320294 | ||
|
|
a0f24aac17 | ||
|
|
7ae484fc83 | ||
|
|
0dcb31973f | ||
|
|
c2f393d249 | ||
|
|
2dbf7dda37 | ||
|
|
cce15a2137 | ||
|
|
14a5a67a1e | ||
|
|
d7d535c10d | ||
|
|
ad00899b6e | ||
|
|
0addf392b6 | ||
|
|
1e84223076 | ||
|
|
244984b6cf | ||
|
|
b39abe630d | ||
|
|
239ab52890 | ||
|
|
4732656a0f | ||
|
|
0da9800ca0 | ||
|
|
baf4a0dfbc | ||
|
|
da5a42280a | ||
|
|
4d29189c8d | ||
|
|
32bd3e26d2 | ||
|
|
6d26f7f6c0 | ||
|
|
72bcacefb6 | ||
|
|
71e9a6da0e | ||
|
|
b339a6d79f | ||
|
|
0b4aef5f6c | ||
|
|
c5182809ac | ||
|
|
f4b541c7c0 | ||
|
|
afe6c6abcc | ||
|
|
3f233f9580 | ||
|
|
6f9d4d9218 | ||
|
|
4111663d8c | ||
|
|
2beeba5c92 | ||
|
|
edab1322c8 | ||
|
|
59ce736faa | ||
|
|
3cd6f4bd58 | ||
|
|
594acaa5f5 | ||
|
|
76ff54dd3a | ||
|
|
598df7d5ed | ||
|
|
442670bdd0 | ||
|
|
b015e97e1f | ||
|
|
65ab3bfc0a | ||
|
|
e674378080 | ||
|
|
40c9d51dfc | ||
|
|
5f74c6ddf8 | ||
|
|
a36d746fb8 | ||
|
|
f6b2b554bb | ||
|
|
465df27858 | ||
|
|
7c907645dc | ||
|
|
6fee35c756 | ||
|
|
c15077aa86 | ||
|
|
f051a97e53 | ||
|
|
1b26a44a37 | ||
|
|
80b2508708 | ||
|
|
e9affd2359 | ||
|
|
8f8b9153b0 | ||
|
|
9a4121e2bf | ||
|
|
50b7f21394 | ||
|
|
a67375557d | ||
|
|
6e7c56fcb9 | ||
|
|
05e257b922 | ||
|
|
d7d61915fa | ||
|
|
d0220b6246 | ||
|
|
32336ba5b4 | ||
|
|
1f26d5285b | ||
|
|
be0bff0535 | ||
|
|
cb7391e66f |
436
.github/copilot-instructions.md
vendored
436
.github/copilot-instructions.md
vendored
@@ -1,21 +1,415 @@
|
||||
# Mentor Instructions
|
||||
|
||||
## Introduction
|
||||
|
||||
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.
|
||||
|
||||
## Tone and Personality
|
||||
|
||||
Maintain a professional, objective, and direct tone consistently:
|
||||
|
||||
- **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.
|
||||
- **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.
|
||||
|
||||
## Behavioral Guidelines
|
||||
|
||||
- **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.
|
||||
- **Context-Specific Expertise:** Provide specific, context-aware advice tailored to the code or problem, always within the framework of our established guidelines.
|
||||
- **Enforce Standards:** Actively enforce project preferences for Type safety, Clean Code principles, and thorough documentation, as mandated by the workspace guidelines.
|
||||
## ISA Frontend – AI Assistant Working Rules
|
||||
|
||||
Concise, project-specific guidance so an AI agent can be productive quickly. Focus on THESE patterns; avoid generic boilerplate.
|
||||
|
||||
### 1. Monorepo & Tooling
|
||||
|
||||
- Nx workspace (Angular 20 + Libraries under `libs/**`, main app `apps/isa-app`).
|
||||
- Scripts (see `package.json`):
|
||||
- Dev serve: `npm start` (=> `nx serve isa-app --ssl`).
|
||||
- Library tests (exclude app): `npm test` (Jest + emerging Vitest). CI uses `npm run ci`.
|
||||
- Build dev: `npm run build`; prod: `npm run build-prod`.
|
||||
- Storybook: `npm run storybook`.
|
||||
- Swagger codegen: `npm run generate:swagger` then `npm run fix:files:swagger`.
|
||||
- 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`).
|
||||
|
||||
### 1.a Project Tree (Detailed Overview)
|
||||
|
||||
```
|
||||
.
|
||||
├─ apps/
|
||||
│ └─ isa-app/ # Main Angular app (Jest). Legacy non-standalone root component pattern.
|
||||
│ ├─ 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.
|
||||
|
||||
40
.github/instructions/nx.instructions.md
vendored
40
.github/instructions/nx.instructions.md
vendored
@@ -1,40 +0,0 @@
|
||||
---
|
||||
applyTo: '**'
|
||||
---
|
||||
|
||||
// This file is automatically generated by Nx Console
|
||||
|
||||
You are in an nx workspace using Nx 21.2.1 and npm as the package manager.
|
||||
|
||||
You have access to the Nx MCP server and the tools it provides. Use them. Follow these guidelines in order to best help the user:
|
||||
|
||||
# General Guidelines
|
||||
- When answering questions, use the nx_workspace tool first to gain an understanding of the workspace architecture
|
||||
- For questions around nx configuration, best practices or if you're unsure, use the nx_docs tool to get relevant, up-to-date docs!! Always use this instead of assuming things about nx configuration
|
||||
- If the user needs help with an Nx configuration or project graph error, use the 'nx_workspace' tool to get any errors
|
||||
- To help answer questions about the workspace structure or simply help with demonstrating how tasks depend on each other, use the 'nx_visualize_graph' tool
|
||||
|
||||
# Generation Guidelines
|
||||
If the user wants to generate something, use the following flow:
|
||||
|
||||
- learn about the nx workspace and any specifics the user needs by using the 'nx_workspace' tool and the 'nx_project_details' tool if applicable
|
||||
- get the available generators using the 'nx_generators' tool
|
||||
- decide which generator to use. If no generators seem relevant, check the 'nx_available_plugins' tool to see if the user could install a plugin to help them
|
||||
- get generator details using the 'nx_generator_schema' tool
|
||||
- you may use the 'nx_docs' tool to learn more about a specific generator or technology if you're unsure
|
||||
- decide which options to provide in order to best complete the user's request. Don't make any assumptions and keep the options minimalistic
|
||||
- open the generator UI using the 'nx_open_generate_ui' tool
|
||||
- wait for the user to finish the generator
|
||||
- read the generator log file using the 'nx_read_generator_log' tool
|
||||
- use the information provided in the log file to answer the user's question or continue with what they were doing
|
||||
|
||||
# Running Tasks Guidelines
|
||||
If the user wants help with tasks or commands (which include keywords like "test", "build", "lint", or other similar actions), use the following flow:
|
||||
- Use the 'nx_current_running_tasks_details' tool to get the list of tasks (this can include tasks that were completed, stopped or failed).
|
||||
- If there are any tasks, ask the user if they would like help with a specific task then use the 'nx_current_running_task_output' tool to get the terminal output for that task/command
|
||||
- Use the terminal output from 'nx_current_running_task_output' to see what's wrong and help the user fix their problem. Use the appropriate tools if necessary
|
||||
- If the user would like to rerun the task or command, always use `nx run <taskId>` to rerun in the terminal. This will ensure that the task will run in the nx context and will be run the same way it originally executed
|
||||
- If the task was marked as "continuous" do not offer to rerun the task. This task is already running and the user can see the output in the terminal. You can use 'nx_current_running_task_output' to get the output of the task to verify the output.
|
||||
|
||||
|
||||
|
||||
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**
|
||||
39
.github/testing-instructions.md
vendored
39
.github/testing-instructions.md
vendored
@@ -2,8 +2,10 @@
|
||||
|
||||
## Framework and Tools
|
||||
|
||||
- Use **Jest** as the testing framework.
|
||||
- For unit tests, utilize **Spectator** to simplify Angular component testing.
|
||||
- **Vitest** is the recommended testing framework.
|
||||
[Vitest Documentation (latest)](https://context7.com/vitest-dev/vitest/llms.txt?topic=getting+started)
|
||||
- **Jest** and **Spectator** are **deprecated**.
|
||||
Do not use them for new tests. Existing tests should be migrated to Vitest where possible.
|
||||
|
||||
## Guidelines
|
||||
|
||||
@@ -23,28 +25,31 @@
|
||||
## Example Test Structure
|
||||
|
||||
```typescript
|
||||
// Example using Jest and Spectator
|
||||
import { createComponentFactory, Spectator } from '@ngneat/spectator';
|
||||
// Example using Vitest (Jest and Spectator are deprecated)
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { render } from '@testing-library/angular';
|
||||
import { MyComponent } from './my-component.component';
|
||||
|
||||
describe('MyComponent', () => {
|
||||
let spectator: Spectator<MyComponent>;
|
||||
const createComponent = createComponentFactory(MyComponent);
|
||||
let component: MyComponent;
|
||||
|
||||
beforeEach(() => {
|
||||
spectator = createComponent();
|
||||
beforeEach(async () => {
|
||||
const { fixture } = await render(MyComponent);
|
||||
component = fixture.componentInstance;
|
||||
});
|
||||
|
||||
it('should display the correct title', () => {
|
||||
it('should display the correct title', async () => {
|
||||
// Arrange
|
||||
const expectedTitle = 'Hello World';
|
||||
|
||||
// Act
|
||||
spectator.component.title = expectedTitle;
|
||||
spectator.detectChanges();
|
||||
component.title = expectedTitle;
|
||||
// If using Angular, trigger change detection:
|
||||
// fixture.detectChanges();
|
||||
|
||||
// Assert
|
||||
expect(spectator.query('h1')).toHaveText(expectedTitle);
|
||||
const compiled = fixture.nativeElement as HTMLElement;
|
||||
expect(compiled.querySelector('h1')?.textContent).toBe(expectedTitle);
|
||||
});
|
||||
|
||||
it('should handle error cases gracefully', () => {
|
||||
@@ -52,15 +57,17 @@ describe('MyComponent', () => {
|
||||
const invalidInput = null;
|
||||
|
||||
// Act
|
||||
spectator.component.input = invalidInput;
|
||||
component.input = invalidInput;
|
||||
|
||||
// Assert
|
||||
expect(() => spectator.component.processInput()).toThrowError('Invalid input');
|
||||
expect(() => component.processInput()).toThrowError('Invalid input');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- [Jest Documentation](https://jestjs.io/docs/getting-started)
|
||||
- [Spectator Documentation](https://ngneat.github.io/spectator/)
|
||||
- [Vitest Documentation (latest)](https://context7.com/vitest-dev/vitest/llms.txt?topic=getting+started)
|
||||
- [Vitest Official Guide](https://vitest.dev/guide/)
|
||||
- [Testing Library for Angular](https://testing-library.com/docs/angular-testing-library/intro/)
|
||||
- **Jest** and **Spectator** documentation are deprecated
|
||||
|
||||
150
.gitignore
vendored
150
.gitignore
vendored
@@ -1,72 +1,78 @@
|
||||
# See http://help.github.com/ignore-files/ for more about ignoring files.
|
||||
|
||||
.matomo
|
||||
junit.xml
|
||||
|
||||
# compiled output
|
||||
/dist
|
||||
/tmp
|
||||
/out-tsc
|
||||
|
||||
/
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
|
||||
# profiling files
|
||||
chrome-profiler-events.json
|
||||
speed-measure-plugin.json
|
||||
|
||||
# IDEs and editors
|
||||
/.idea
|
||||
.project
|
||||
.classpath
|
||||
.c9/
|
||||
*.launch
|
||||
.settings/
|
||||
*.sublime-workspace
|
||||
|
||||
# IDE - VSCode
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
.history/*
|
||||
|
||||
# misc
|
||||
/.angular/cache
|
||||
/.sass-cache
|
||||
/connect.lock
|
||||
/coverage
|
||||
/testresults
|
||||
/libpeerconnection.log
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
yarn.lock
|
||||
testem.log
|
||||
/typings
|
||||
|
||||
# System Files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
libs/swagger/src/lib/*
|
||||
*storybook.log
|
||||
|
||||
|
||||
.nx/cache
|
||||
.nx/workspace-data
|
||||
.angular
|
||||
.claude
|
||||
|
||||
|
||||
storybook-static
|
||||
|
||||
.cursor\rules\nx-rules.mdc
|
||||
.github\instructions\nx.instructions.md
|
||||
.cursor/rules/nx-rules.mdc
|
||||
.github/instructions/nx.instructions.md
|
||||
|
||||
vite.config.*.timestamp*
|
||||
vitest.config.*.timestamp*
|
||||
# See http://help.github.com/ignore-files/ for more about ignoring files.
|
||||
|
||||
.matomo
|
||||
junit.xml
|
||||
|
||||
# compiled output
|
||||
/dist
|
||||
/tmp
|
||||
/out-tsc
|
||||
|
||||
/
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
|
||||
# profiling files
|
||||
chrome-profiler-events.json
|
||||
speed-measure-plugin.json
|
||||
|
||||
# IDEs and editors
|
||||
/.idea
|
||||
.project
|
||||
.classpath
|
||||
.c9/
|
||||
*.launch
|
||||
.settings/
|
||||
*.sublime-workspace
|
||||
|
||||
# IDE - VSCode
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
.history/*
|
||||
|
||||
# misc
|
||||
/.angular/cache
|
||||
/.sass-cache
|
||||
/connect.lock
|
||||
/coverage
|
||||
/testresults
|
||||
/libpeerconnection.log
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
yarn.lock
|
||||
testem.log
|
||||
/typings
|
||||
|
||||
# System Files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
libs/swagger/src/lib/*
|
||||
*storybook.log
|
||||
|
||||
|
||||
.nx/cache
|
||||
.nx/workspace-data
|
||||
.angular
|
||||
.claude
|
||||
|
||||
|
||||
storybook-static
|
||||
|
||||
.cursor\rules\nx-rules.mdc
|
||||
.github\instructions\nx.instructions.md
|
||||
.cursor/rules/nx-rules.mdc
|
||||
.github/instructions/nx.instructions.md
|
||||
|
||||
vite.config.*.timestamp*
|
||||
vitest.config.*.timestamp*
|
||||
|
||||
.mcp.json
|
||||
.memory.json
|
||||
|
||||
nx.instructions.md
|
||||
CLAUDE.md
|
||||
|
||||
192
.vscode/settings.json
vendored
192
.vscode/settings.json
vendored
@@ -1,92 +1,100 @@
|
||||
{
|
||||
"editor.accessibilitySupport": "off",
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"exportall.config.exclude": [".test.", ".spec.", ".stories."],
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"eslint.validate": [
|
||||
"json"
|
||||
],
|
||||
"[html]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[typescriptreact]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"exportall.config.folderListener": [
|
||||
"/libs/oms/data-access/src/lib/models",
|
||||
"/libs/oms/data-access/src/lib/schemas",
|
||||
"/libs/catalogue/data-access/src/lib/models",
|
||||
"/libs/common/data-access/src/lib/models",
|
||||
"/libs/common/data-access/src/lib/error",
|
||||
"/libs/oms/data-access/src/lib/errors/return-process"
|
||||
],
|
||||
"github.copilot.chat.commitMessageGeneration.instructions": [
|
||||
{
|
||||
"file": ".github/commit-instructions.md"
|
||||
}
|
||||
],
|
||||
"github.copilot.chat.codeGeneration.instructions": [
|
||||
{
|
||||
"file": ".vscode/llms/angular.txt"
|
||||
},
|
||||
{
|
||||
"file": "docs/tech-stack.md"
|
||||
},
|
||||
{
|
||||
"file": "docs/guidelines/code-style.md"
|
||||
},
|
||||
{
|
||||
"file": "docs/guidelines/project-structure.md"
|
||||
},
|
||||
{
|
||||
"file": "docs/guidelines/state-management.md"
|
||||
},
|
||||
{
|
||||
"file": "docs/guidelines/testing.md"
|
||||
}
|
||||
],
|
||||
"github.copilot.chat.testGeneration.instructions": [
|
||||
{
|
||||
"file": ".github/testing-instructions.md"
|
||||
},
|
||||
{
|
||||
"file": "docs/tech-stack.md"
|
||||
},
|
||||
{
|
||||
"file": "docs/guidelines/code-style.md"
|
||||
},
|
||||
{
|
||||
"file": "docs/guidelines/testing.md"
|
||||
}
|
||||
],
|
||||
"github.copilot.chat.reviewSelection.instructions": [
|
||||
{
|
||||
"file": ".github/copilot-instructions.md"
|
||||
},
|
||||
{
|
||||
"file": ".github/review-instructions.md"
|
||||
},
|
||||
{
|
||||
"file": "docs/tech-stack.md"
|
||||
},
|
||||
{
|
||||
"file": "docs/guidelines/code-style.md"
|
||||
},
|
||||
{
|
||||
"file": "docs/guidelines/project-structure.md"
|
||||
},
|
||||
{
|
||||
"file": "docs/guidelines/state-management.md"
|
||||
},
|
||||
{
|
||||
"file": "docs/guidelines/testing.md"
|
||||
}
|
||||
],
|
||||
"nxConsole.generateAiAgentRules": true,
|
||||
"chat.mcp.enabled": true,
|
||||
"chat.mcp.discovery.enabled": true
|
||||
}
|
||||
{
|
||||
"editor.accessibilitySupport": "off",
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"exportall.config.exclude": [".test.", ".spec.", ".stories."],
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"eslint.validate": [
|
||||
"json"
|
||||
],
|
||||
"[html]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[typescriptreact]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[markdown]": {
|
||||
"editor.formatOnSave": false
|
||||
},
|
||||
"exportall.config.folderListener": [
|
||||
"/libs/oms/data-access/src/lib/models",
|
||||
"/libs/oms/data-access/src/lib/schemas",
|
||||
"/libs/catalogue/data-access/src/lib/models",
|
||||
"/libs/common/data-access/src/lib/models",
|
||||
"/libs/common/data-access/src/lib/error",
|
||||
"/libs/oms/data-access/src/lib/errors/return-process"
|
||||
],
|
||||
"github.copilot.chat.commitMessageGeneration.instructions": [
|
||||
{
|
||||
"file": ".github/commit-instructions.md"
|
||||
}
|
||||
],
|
||||
"github.copilot.chat.codeGeneration.instructions": [
|
||||
{
|
||||
"file": ".vscode/llms/angular.txt"
|
||||
},
|
||||
{
|
||||
"file": "docs/tech-stack.md"
|
||||
},
|
||||
{
|
||||
"file": "docs/guidelines/code-style.md"
|
||||
},
|
||||
{
|
||||
"file": "docs/guidelines/project-structure.md"
|
||||
},
|
||||
{
|
||||
"file": "docs/guidelines/state-management.md"
|
||||
},
|
||||
{
|
||||
"file": "docs/guidelines/testing.md"
|
||||
}
|
||||
],
|
||||
"github.copilot.chat.testGeneration.instructions": [
|
||||
{
|
||||
"file": ".github/testing-instructions.md"
|
||||
},
|
||||
{
|
||||
"file": "docs/tech-stack.md"
|
||||
},
|
||||
{
|
||||
"file": "docs/guidelines/code-style.md"
|
||||
},
|
||||
{
|
||||
"file": "docs/guidelines/testing.md"
|
||||
}
|
||||
],
|
||||
"github.copilot.chat.reviewSelection.instructions": [
|
||||
{
|
||||
"file": ".github/copilot-instructions.md"
|
||||
},
|
||||
{
|
||||
"file": ".github/review-instructions.md"
|
||||
},
|
||||
{
|
||||
"file": "docs/tech-stack.md"
|
||||
},
|
||||
{
|
||||
"file": "docs/guidelines/code-style.md"
|
||||
},
|
||||
{
|
||||
"file": "docs/guidelines/project-structure.md"
|
||||
},
|
||||
{
|
||||
"file": "docs/guidelines/state-management.md"
|
||||
},
|
||||
{
|
||||
"file": "docs/guidelines/testing.md"
|
||||
}
|
||||
],
|
||||
"nxConsole.generateAiAgentRules": true,
|
||||
"chat.mcp.discovery.enabled": {
|
||||
"claude-desktop": true,
|
||||
"windsurf": true,
|
||||
"cursor-global": true,
|
||||
"cursor-workspace": true
|
||||
},
|
||||
"chat.mcp.access": "all"
|
||||
}
|
||||
|
||||
148
CLAUDE.md
Normal file
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
|
||||
@@ -8,7 +8,7 @@ WORKDIR /app
|
||||
COPY . .
|
||||
RUN umask 0022
|
||||
RUN npm version ${SEMVERSION}
|
||||
RUN npm install --foreground-scripts --legacy-peer-deps
|
||||
RUN npm ci --foreground-scripts
|
||||
RUN if [ "${IS_PRODUCTION}" = "true" ] ; then npm run-script build-prod ; else npm run-script build ; fi
|
||||
|
||||
# stage final
|
||||
|
||||
@@ -1,162 +1,162 @@
|
||||
{
|
||||
"name": "isa-app",
|
||||
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||
"projectType": "application",
|
||||
"prefix": "app",
|
||||
"sourceRoot": "apps/isa-app/src",
|
||||
"tags": [],
|
||||
"targets": {
|
||||
"build": {
|
||||
"executor": "@angular-devkit/build-angular:application",
|
||||
"options": {
|
||||
"allowedCommonJsDependencies": [
|
||||
"lodash",
|
||||
"moment",
|
||||
"jsrsasign",
|
||||
"pdfjs-dist/build/pdf",
|
||||
"pdfjs-dist/web/pdf_viewer",
|
||||
"pdfjs-dist/es5/build/pdf",
|
||||
"pdfjs-dist/es5/web/pdf_viewer"
|
||||
],
|
||||
"outputPath": "dist/isa-app",
|
||||
"index": "apps/isa-app/src/index.html",
|
||||
"browser": "apps/isa-app/src/main.ts",
|
||||
"polyfills": ["zone.js"],
|
||||
"tsConfig": "apps/isa-app/tsconfig.app.json",
|
||||
"inlineStyleLanguage": "scss",
|
||||
"assets": [
|
||||
"apps/isa-app/src/favicon.ico",
|
||||
"apps/isa-app/src/assets",
|
||||
"apps/isa-app/src/config",
|
||||
"apps/isa-app/src/silent-refresh.html",
|
||||
"apps/isa-app/src/manifest.webmanifest",
|
||||
{
|
||||
"glob": "**/*",
|
||||
"input": "node_modules/scandit-web-datacapture-barcode/build/engine",
|
||||
"output": "scandit"
|
||||
}
|
||||
],
|
||||
"styles": [
|
||||
"@angular/cdk/overlay-prebuilt.css",
|
||||
"apps/isa-app/src/tailwind.scss",
|
||||
"apps/isa-app/src/styles.scss"
|
||||
],
|
||||
"scripts": []
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"budgets": [
|
||||
{
|
||||
"type": "initial",
|
||||
"maximumWarning": "2mb",
|
||||
"maximumError": "5mb"
|
||||
},
|
||||
{
|
||||
"type": "anyComponentStyle",
|
||||
"maximumWarning": "25kb"
|
||||
}
|
||||
],
|
||||
"fileReplacements": [
|
||||
{
|
||||
"replace": "apps/isa-app/src/environments/environment.ts",
|
||||
"with": "apps/isa-app/src/environments/environment.prod.ts"
|
||||
}
|
||||
],
|
||||
"outputHashing": "all",
|
||||
"serviceWorker": "apps/isa-app/ngsw-config.json"
|
||||
},
|
||||
"development": {
|
||||
"optimization": false,
|
||||
"extractLicenses": false,
|
||||
"sourceMap": true
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "production",
|
||||
"outputs": ["{options.outputPath}"]
|
||||
},
|
||||
"serve": {
|
||||
"executor": "@angular-devkit/build-angular:dev-server",
|
||||
"configurations": {
|
||||
"production": {
|
||||
"buildTarget": "isa-app:build:production"
|
||||
},
|
||||
"development": {
|
||||
"buildTarget": "isa-app:build:development"
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "development",
|
||||
"continuous": true
|
||||
},
|
||||
"extract-i18n": {
|
||||
"executor": "@angular-devkit/build-angular:extract-i18n",
|
||||
"options": {
|
||||
"buildTarget": "isa-app:build"
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"executor": "@nx/eslint:lint"
|
||||
},
|
||||
"test": {
|
||||
"executor": "@nx/jest:jest",
|
||||
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
|
||||
"options": {
|
||||
"jestConfig": "apps/isa-app/jest.config.ts"
|
||||
}
|
||||
},
|
||||
"serve-static": {
|
||||
"executor": "@nx/web:file-server",
|
||||
"options": {
|
||||
"buildTarget": "isa-app:build",
|
||||
"staticFilePath": "dist/apps/isa-app/browser",
|
||||
"spa": true
|
||||
}
|
||||
},
|
||||
"storybook": {
|
||||
"executor": "@storybook/angular:start-storybook",
|
||||
"options": {
|
||||
"port": 4400,
|
||||
"configDir": "apps/isa-app/.storybook",
|
||||
"browserTarget": "isa-app:build",
|
||||
"compodoc": false,
|
||||
"open": false,
|
||||
"assets": [
|
||||
{
|
||||
"glob": "**/*",
|
||||
"input": "apps/isa-app/src/assets",
|
||||
"output": "/assets"
|
||||
}
|
||||
],
|
||||
"styles": [
|
||||
"@angular/cdk/overlay-prebuilt.css",
|
||||
"apps/isa-app/src/tailwind.scss",
|
||||
"apps/isa-app/src/styles.scss"
|
||||
]
|
||||
},
|
||||
"configurations": {
|
||||
"ci": {
|
||||
"quiet": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"build-storybook": {
|
||||
"executor": "@storybook/angular:build-storybook",
|
||||
"outputs": ["{options.outputDir}"],
|
||||
"options": {
|
||||
"outputDir": "dist/storybook/isa-app",
|
||||
"configDir": "apps/isa-app/.storybook",
|
||||
"browserTarget": "isa-app:build",
|
||||
"compodoc": false,
|
||||
"styles": [
|
||||
"@angular/cdk/overlay-prebuilt.css",
|
||||
"apps/isa-app/src/tailwind.scss",
|
||||
"apps/isa-app/src/styles.scss"
|
||||
]
|
||||
},
|
||||
"configurations": {
|
||||
"ci": {
|
||||
"quiet": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
{
|
||||
"name": "isa-app",
|
||||
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||
"projectType": "application",
|
||||
"prefix": "app",
|
||||
"sourceRoot": "apps/isa-app/src",
|
||||
"tags": [],
|
||||
"targets": {
|
||||
"build": {
|
||||
"executor": "@angular-devkit/build-angular:application",
|
||||
"options": {
|
||||
"allowedCommonJsDependencies": [
|
||||
"lodash",
|
||||
"moment",
|
||||
"jsrsasign",
|
||||
"pdfjs-dist/build/pdf",
|
||||
"pdfjs-dist/web/pdf_viewer",
|
||||
"pdfjs-dist/es5/build/pdf",
|
||||
"pdfjs-dist/es5/web/pdf_viewer"
|
||||
],
|
||||
"outputPath": "dist/isa-app",
|
||||
"index": "apps/isa-app/src/index.html",
|
||||
"browser": "apps/isa-app/src/main.ts",
|
||||
"polyfills": ["zone.js"],
|
||||
"tsConfig": "apps/isa-app/tsconfig.app.json",
|
||||
"inlineStyleLanguage": "scss",
|
||||
"assets": [
|
||||
"apps/isa-app/src/favicon.ico",
|
||||
"apps/isa-app/src/assets",
|
||||
"apps/isa-app/src/config",
|
||||
"apps/isa-app/src/silent-refresh.html",
|
||||
"apps/isa-app/src/manifest.webmanifest",
|
||||
{
|
||||
"glob": "**/*",
|
||||
"input": "node_modules/scandit-web-datacapture-barcode/build/engine",
|
||||
"output": "scandit"
|
||||
}
|
||||
],
|
||||
"styles": [
|
||||
"@angular/cdk/overlay-prebuilt.css",
|
||||
"apps/isa-app/src/tailwind.scss",
|
||||
"apps/isa-app/src/styles.scss"
|
||||
],
|
||||
"scripts": []
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"budgets": [
|
||||
{
|
||||
"type": "initial",
|
||||
"maximumWarning": "2mb",
|
||||
"maximumError": "5mb"
|
||||
},
|
||||
{
|
||||
"type": "anyComponentStyle",
|
||||
"maximumWarning": "25kb"
|
||||
}
|
||||
],
|
||||
"fileReplacements": [
|
||||
{
|
||||
"replace": "apps/isa-app/src/environments/environment.ts",
|
||||
"with": "apps/isa-app/src/environments/environment.prod.ts"
|
||||
}
|
||||
],
|
||||
"outputHashing": "all",
|
||||
"serviceWorker": "apps/isa-app/ngsw-config.json"
|
||||
},
|
||||
"development": {
|
||||
"optimization": false,
|
||||
"extractLicenses": false,
|
||||
"sourceMap": true
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "production",
|
||||
"outputs": ["{options.outputPath}"]
|
||||
},
|
||||
"serve": {
|
||||
"executor": "@angular-devkit/build-angular:dev-server",
|
||||
"configurations": {
|
||||
"production": {
|
||||
"buildTarget": "isa-app:build:production"
|
||||
},
|
||||
"development": {
|
||||
"buildTarget": "isa-app:build:development"
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "development",
|
||||
"continuous": true
|
||||
},
|
||||
"extract-i18n": {
|
||||
"executor": "@angular-devkit/build-angular:extract-i18n",
|
||||
"options": {
|
||||
"buildTarget": "isa-app:build"
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"executor": "@nx/eslint:lint"
|
||||
},
|
||||
"test": {
|
||||
"executor": "@nx/jest:jest",
|
||||
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
|
||||
"options": {
|
||||
"jestConfig": "apps/isa-app/jest.config.ts"
|
||||
}
|
||||
},
|
||||
"serve-static": {
|
||||
"executor": "@nx/web:file-server",
|
||||
"options": {
|
||||
"buildTarget": "isa-app:build",
|
||||
"staticFilePath": "dist/apps/isa-app/browser",
|
||||
"spa": true
|
||||
}
|
||||
},
|
||||
"storybook": {
|
||||
"executor": "@storybook/angular:start-storybook",
|
||||
"options": {
|
||||
"port": 4400,
|
||||
"configDir": "apps/isa-app/.storybook",
|
||||
"browserTarget": "isa-app:build",
|
||||
"compodoc": false,
|
||||
"open": false,
|
||||
"assets": [
|
||||
{
|
||||
"glob": "**/*",
|
||||
"input": "apps/isa-app/src/assets",
|
||||
"output": "/assets"
|
||||
}
|
||||
],
|
||||
"styles": [
|
||||
"@angular/cdk/overlay-prebuilt.css",
|
||||
"apps/isa-app/src/tailwind.scss",
|
||||
"apps/isa-app/src/styles.scss"
|
||||
]
|
||||
},
|
||||
"configurations": {
|
||||
"ci": {
|
||||
"quiet": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"build-storybook": {
|
||||
"executor": "@storybook/angular:build-storybook",
|
||||
"outputs": ["{options.outputDir}"],
|
||||
"options": {
|
||||
"outputDir": "dist/storybook/isa-app",
|
||||
"configDir": "apps/isa-app/.storybook",
|
||||
"browserTarget": "isa-app:build",
|
||||
"compodoc": false,
|
||||
"styles": [
|
||||
"@angular/cdk/overlay-prebuilt.css",
|
||||
"apps/isa-app/src/tailwind.scss",
|
||||
"apps/isa-app/src/styles.scss"
|
||||
]
|
||||
},
|
||||
"configurations": {
|
||||
"ci": {
|
||||
"quiet": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,214 +1,250 @@
|
||||
import { isDevMode, NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import {
|
||||
CanActivateCartGuard,
|
||||
CanActivateCartWithProcessIdGuard,
|
||||
CanActivateCustomerGuard,
|
||||
CanActivateCustomerOrdersGuard,
|
||||
CanActivateCustomerOrdersWithProcessIdGuard,
|
||||
CanActivateCustomerWithProcessIdGuard,
|
||||
CanActivateGoodsInGuard,
|
||||
CanActivateProductGuard,
|
||||
CanActivateProductWithProcessIdGuard,
|
||||
CanActivateRemissionGuard,
|
||||
CanActivateTaskCalendarGuard,
|
||||
IsAuthenticatedGuard,
|
||||
} from './guards';
|
||||
import { CanActivateAssortmentGuard } from './guards/can-activate-assortment.guard';
|
||||
import { CanActivatePackageInspectionGuard } from './guards/can-activate-package-inspection.guard';
|
||||
import { MainComponent } from './main.component';
|
||||
import { PreviewComponent } from './preview';
|
||||
import {
|
||||
BranchSectionResolver,
|
||||
CustomerSectionResolver,
|
||||
ProcessIdResolver,
|
||||
} from './resolvers';
|
||||
import { TokenLoginComponent, TokenLoginModule } from './token-login';
|
||||
import { ProcessIdGuard } from './guards/process-id.guard';
|
||||
import {
|
||||
ActivateProcessIdGuard,
|
||||
ActivateProcessIdWithConfigKeyGuard,
|
||||
} from './guards/activate-process-id.guard';
|
||||
import { MatomoRouteData } from 'ngx-matomo-client';
|
||||
import { tabResolverFn } from '@isa/core/tabs';
|
||||
import { provideScrollPositionRestoration } from '@isa/utils/scroll-position';
|
||||
|
||||
const routes: Routes = [
|
||||
{ path: '', redirectTo: 'kunde/dashboard', pathMatch: 'full' },
|
||||
{
|
||||
path: 'login',
|
||||
children: [
|
||||
{ path: ':token', component: TokenLoginComponent },
|
||||
{ path: '**', redirectTo: 'kunde', pathMatch: 'full' },
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
canActivate: [IsAuthenticatedGuard],
|
||||
children: [
|
||||
{
|
||||
path: 'kunde',
|
||||
component: MainComponent,
|
||||
children: [
|
||||
{
|
||||
path: 'dashboard',
|
||||
loadChildren: () =>
|
||||
import('@page/dashboard').then((m) => m.DashboardModule),
|
||||
data: {
|
||||
matomo: {
|
||||
title: 'Dashboard',
|
||||
} as MatomoRouteData,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'product',
|
||||
loadChildren: () =>
|
||||
import('@page/catalog').then((m) => m.PageCatalogModule),
|
||||
canActivate: [CanActivateProductGuard],
|
||||
},
|
||||
{
|
||||
path: ':processId/product',
|
||||
loadChildren: () =>
|
||||
import('@page/catalog').then((m) => m.PageCatalogModule),
|
||||
canActivate: [CanActivateProductWithProcessIdGuard],
|
||||
resolve: { processId: ProcessIdResolver },
|
||||
},
|
||||
{
|
||||
path: 'order',
|
||||
loadChildren: () =>
|
||||
import('@page/customer-order').then((m) => m.CustomerOrderModule),
|
||||
canActivate: [CanActivateCustomerOrdersGuard],
|
||||
},
|
||||
{
|
||||
path: ':processId/order',
|
||||
loadChildren: () =>
|
||||
import('@page/customer-order').then((m) => m.CustomerOrderModule),
|
||||
canActivate: [CanActivateCustomerOrdersWithProcessIdGuard],
|
||||
resolve: { processId: ProcessIdResolver },
|
||||
},
|
||||
{
|
||||
path: 'customer',
|
||||
loadChildren: () =>
|
||||
import('@page/customer').then((m) => m.CustomerModule),
|
||||
canActivate: [CanActivateCustomerGuard],
|
||||
},
|
||||
{
|
||||
path: ':processId/customer',
|
||||
loadChildren: () =>
|
||||
import('@page/customer').then((m) => m.CustomerModule),
|
||||
canActivate: [CanActivateCustomerWithProcessIdGuard],
|
||||
resolve: { processId: ProcessIdResolver },
|
||||
},
|
||||
{
|
||||
path: 'cart',
|
||||
loadChildren: () =>
|
||||
import('@page/checkout').then((m) => m.PageCheckoutModule),
|
||||
canActivate: [CanActivateCartGuard],
|
||||
},
|
||||
{
|
||||
path: ':processId/cart',
|
||||
loadChildren: () =>
|
||||
import('@page/checkout').then((m) => m.PageCheckoutModule),
|
||||
canActivate: [CanActivateCartWithProcessIdGuard],
|
||||
},
|
||||
{
|
||||
path: 'pickup-shelf',
|
||||
canActivate: [ProcessIdGuard],
|
||||
// NOTE: This is a workaround for the canActivate guard not being called
|
||||
loadChildren: () =>
|
||||
import('@page/pickup-shelf').then((m) => m.PickupShelfOutModule),
|
||||
},
|
||||
{
|
||||
path: ':processId/pickup-shelf',
|
||||
canActivate: [ActivateProcessIdGuard],
|
||||
loadChildren: () =>
|
||||
import('@page/pickup-shelf').then((m) => m.PickupShelfOutModule),
|
||||
},
|
||||
{ path: '**', redirectTo: 'dashboard', pathMatch: 'full' },
|
||||
],
|
||||
resolve: { section: CustomerSectionResolver },
|
||||
},
|
||||
{
|
||||
path: 'filiale',
|
||||
component: MainComponent,
|
||||
children: [
|
||||
{
|
||||
path: 'task-calendar',
|
||||
loadChildren: () =>
|
||||
import('@page/task-calendar').then(
|
||||
(m) => m.PageTaskCalendarModule,
|
||||
),
|
||||
canActivate: [CanActivateTaskCalendarGuard],
|
||||
},
|
||||
{
|
||||
path: 'pickup-shelf',
|
||||
canActivate: [ActivateProcessIdWithConfigKeyGuard('pickupShelf')],
|
||||
// NOTE: This is a workaround for the canActivate guard not being called
|
||||
loadChildren: () =>
|
||||
import('@page/pickup-shelf').then((m) => m.PickupShelfInModule),
|
||||
},
|
||||
{
|
||||
path: 'goods/in',
|
||||
loadChildren: () =>
|
||||
import('@page/goods-in').then((m) => m.GoodsInModule),
|
||||
canActivate: [CanActivateGoodsInGuard],
|
||||
},
|
||||
{
|
||||
path: 'remission',
|
||||
loadChildren: () =>
|
||||
import('@page/remission').then((m) => m.PageRemissionModule),
|
||||
canActivate: [CanActivateRemissionGuard],
|
||||
},
|
||||
{
|
||||
path: 'package-inspection',
|
||||
loadChildren: () =>
|
||||
import('@page/package-inspection').then(
|
||||
(m) => m.PackageInspectionModule,
|
||||
),
|
||||
canActivate: [CanActivatePackageInspectionGuard],
|
||||
},
|
||||
{
|
||||
path: 'assortment',
|
||||
loadChildren: () =>
|
||||
import('@page/assortment').then((m) => m.AssortmentModule),
|
||||
canActivate: [CanActivateAssortmentGuard],
|
||||
},
|
||||
{ path: '**', redirectTo: 'task-calendar', pathMatch: 'full' },
|
||||
],
|
||||
resolve: { section: BranchSectionResolver },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: ':tabId',
|
||||
component: MainComponent,
|
||||
resolve: { process: tabResolverFn, tab: tabResolverFn },
|
||||
canActivate: [IsAuthenticatedGuard],
|
||||
children: [
|
||||
{
|
||||
path: 'return',
|
||||
loadChildren: () =>
|
||||
import('@isa/oms/feature/return-search').then((m) => m.routes),
|
||||
},
|
||||
{
|
||||
path: 'remission',
|
||||
loadChildren: () =>
|
||||
import('@isa/remission/feature/remission-list').then((m) => m.routes),
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
if (isDevMode()) {
|
||||
routes.unshift({
|
||||
path: 'preview',
|
||||
component: PreviewComponent,
|
||||
});
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forRoot(routes), TokenLoginModule],
|
||||
exports: [RouterModule],
|
||||
providers: [provideScrollPositionRestoration()],
|
||||
})
|
||||
export class AppRoutingModule {}
|
||||
import { inject, isDevMode, NgModule } from '@angular/core';
|
||||
import { Location } from '@angular/common';
|
||||
import { RouterModule, Routes, Router } from '@angular/router';
|
||||
import {
|
||||
CanActivateCartGuard,
|
||||
CanActivateCartWithProcessIdGuard,
|
||||
CanActivateCustomerGuard,
|
||||
CanActivateCustomerOrdersGuard,
|
||||
CanActivateCustomerOrdersWithProcessIdGuard,
|
||||
CanActivateCustomerWithProcessIdGuard,
|
||||
CanActivateGoodsInGuard,
|
||||
CanActivateProductGuard,
|
||||
CanActivateProductWithProcessIdGuard,
|
||||
CanActivateRemissionGuard,
|
||||
CanActivateTaskCalendarGuard,
|
||||
IsAuthenticatedGuard,
|
||||
} from './guards';
|
||||
import { CanActivateAssortmentGuard } from './guards/can-activate-assortment.guard';
|
||||
import { CanActivatePackageInspectionGuard } from './guards/can-activate-package-inspection.guard';
|
||||
import { MainComponent } from './main.component';
|
||||
import { PreviewComponent } from './preview';
|
||||
import {
|
||||
BranchSectionResolver,
|
||||
CustomerSectionResolver,
|
||||
ProcessIdResolver,
|
||||
} from './resolvers';
|
||||
import { TokenLoginComponent, TokenLoginModule } from './token-login';
|
||||
import { ProcessIdGuard } from './guards/process-id.guard';
|
||||
import {
|
||||
ActivateProcessIdGuard,
|
||||
ActivateProcessIdWithConfigKeyGuard,
|
||||
} from './guards/activate-process-id.guard';
|
||||
import { MatomoRouteData } from 'ngx-matomo-client';
|
||||
import {
|
||||
tabResolverFn,
|
||||
TabService,
|
||||
TabNavigationService,
|
||||
processResolverFn,
|
||||
} from '@isa/core/tabs';
|
||||
import { provideScrollPositionRestoration } from '@isa/utils/scroll-position';
|
||||
|
||||
const routes: Routes = [
|
||||
{ path: '', redirectTo: 'kunde/dashboard', pathMatch: 'full' },
|
||||
{
|
||||
path: 'login',
|
||||
children: [
|
||||
{ path: ':token', component: TokenLoginComponent },
|
||||
{ path: '**', redirectTo: 'kunde', pathMatch: 'full' },
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
canActivate: [IsAuthenticatedGuard],
|
||||
children: [
|
||||
{
|
||||
path: 'kunde',
|
||||
component: MainComponent,
|
||||
children: [
|
||||
{
|
||||
path: 'dashboard',
|
||||
loadChildren: () =>
|
||||
import('@page/dashboard').then((m) => m.DashboardModule),
|
||||
data: {
|
||||
matomo: {
|
||||
title: 'Dashboard',
|
||||
} as MatomoRouteData,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'product',
|
||||
loadChildren: () =>
|
||||
import('@page/catalog').then((m) => m.PageCatalogModule),
|
||||
canActivate: [CanActivateProductGuard],
|
||||
},
|
||||
{
|
||||
path: ':processId/product',
|
||||
loadChildren: () =>
|
||||
import('@page/catalog').then((m) => m.PageCatalogModule),
|
||||
canActivate: [CanActivateProductWithProcessIdGuard],
|
||||
resolve: { processId: ProcessIdResolver },
|
||||
},
|
||||
{
|
||||
path: 'order',
|
||||
loadChildren: () =>
|
||||
import('@page/customer-order').then((m) => m.CustomerOrderModule),
|
||||
canActivate: [CanActivateCustomerOrdersGuard],
|
||||
},
|
||||
{
|
||||
path: ':processId/order',
|
||||
loadChildren: () =>
|
||||
import('@page/customer-order').then((m) => m.CustomerOrderModule),
|
||||
canActivate: [CanActivateCustomerOrdersWithProcessIdGuard],
|
||||
resolve: { processId: ProcessIdResolver },
|
||||
},
|
||||
{
|
||||
path: 'customer',
|
||||
loadChildren: () =>
|
||||
import('@page/customer').then((m) => m.CustomerModule),
|
||||
canActivate: [CanActivateCustomerGuard],
|
||||
},
|
||||
{
|
||||
path: ':processId/customer',
|
||||
loadChildren: () =>
|
||||
import('@page/customer').then((m) => m.CustomerModule),
|
||||
canActivate: [CanActivateCustomerWithProcessIdGuard],
|
||||
resolve: { processId: ProcessIdResolver },
|
||||
},
|
||||
{
|
||||
path: 'cart',
|
||||
loadChildren: () =>
|
||||
import('@page/checkout').then((m) => m.PageCheckoutModule),
|
||||
canActivate: [CanActivateCartGuard],
|
||||
},
|
||||
{
|
||||
path: ':processId/cart',
|
||||
loadChildren: () =>
|
||||
import('@page/checkout').then((m) => m.PageCheckoutModule),
|
||||
canActivate: [CanActivateCartWithProcessIdGuard],
|
||||
},
|
||||
{
|
||||
path: 'pickup-shelf',
|
||||
canActivate: [ProcessIdGuard],
|
||||
// NOTE: This is a workaround for the canActivate guard not being called
|
||||
loadChildren: () =>
|
||||
import('@page/pickup-shelf').then((m) => m.PickupShelfOutModule),
|
||||
},
|
||||
{
|
||||
path: ':processId/pickup-shelf',
|
||||
canActivate: [ActivateProcessIdGuard],
|
||||
loadChildren: () =>
|
||||
import('@page/pickup-shelf').then((m) => m.PickupShelfOutModule),
|
||||
},
|
||||
{ path: '**', redirectTo: 'dashboard', pathMatch: 'full' },
|
||||
],
|
||||
resolve: { section: CustomerSectionResolver },
|
||||
},
|
||||
{
|
||||
path: 'filiale',
|
||||
component: MainComponent,
|
||||
children: [
|
||||
{
|
||||
path: 'task-calendar',
|
||||
loadChildren: () =>
|
||||
import('@page/task-calendar').then(
|
||||
(m) => m.PageTaskCalendarModule,
|
||||
),
|
||||
canActivate: [CanActivateTaskCalendarGuard],
|
||||
},
|
||||
{
|
||||
path: 'pickup-shelf',
|
||||
canActivate: [ActivateProcessIdWithConfigKeyGuard('pickupShelf')],
|
||||
// NOTE: This is a workaround for the canActivate guard not being called
|
||||
loadChildren: () =>
|
||||
import('@page/pickup-shelf').then((m) => m.PickupShelfInModule),
|
||||
},
|
||||
{
|
||||
path: 'goods/in',
|
||||
loadChildren: () =>
|
||||
import('@page/goods-in').then((m) => m.GoodsInModule),
|
||||
canActivate: [CanActivateGoodsInGuard],
|
||||
},
|
||||
{
|
||||
path: 'remission',
|
||||
loadChildren: () =>
|
||||
import('@page/remission').then((m) => m.PageRemissionModule),
|
||||
canActivate: [CanActivateRemissionGuard],
|
||||
},
|
||||
{
|
||||
path: 'package-inspection',
|
||||
loadChildren: () =>
|
||||
import('@page/package-inspection').then(
|
||||
(m) => m.PackageInspectionModule,
|
||||
),
|
||||
canActivate: [CanActivatePackageInspectionGuard],
|
||||
},
|
||||
{
|
||||
path: 'assortment',
|
||||
loadChildren: () =>
|
||||
import('@page/assortment').then((m) => m.AssortmentModule),
|
||||
canActivate: [CanActivateAssortmentGuard],
|
||||
},
|
||||
{ path: '**', redirectTo: 'task-calendar', pathMatch: 'full' },
|
||||
],
|
||||
resolve: { section: BranchSectionResolver },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: ':tabId',
|
||||
component: MainComponent,
|
||||
resolve: { process: processResolverFn, tab: tabResolverFn },
|
||||
canActivate: [IsAuthenticatedGuard],
|
||||
children: [
|
||||
{
|
||||
path: 'reward',
|
||||
loadChildren: () =>
|
||||
import('@isa/checkout/feature/reward-catalog').then((m) => m.routes),
|
||||
},
|
||||
{
|
||||
path: 'return',
|
||||
loadChildren: () =>
|
||||
import('@isa/oms/feature/return-search').then((m) => m.routes),
|
||||
},
|
||||
{
|
||||
path: 'remission',
|
||||
children: [
|
||||
{
|
||||
path: 'return-receipt',
|
||||
loadChildren: () =>
|
||||
import(
|
||||
'@isa/remission/feature/remission-return-receipt-list'
|
||||
).then((m) => m.routes),
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
loadChildren: () =>
|
||||
import('@isa/remission/feature/remission-list').then(
|
||||
(m) => m.routes,
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
if (isDevMode()) {
|
||||
routes.unshift({
|
||||
path: 'preview',
|
||||
component: PreviewComponent,
|
||||
});
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
RouterModule.forRoot(routes, {
|
||||
bindToComponentInputs: true,
|
||||
enableTracing: false,
|
||||
}),
|
||||
TokenLoginModule,
|
||||
],
|
||||
exports: [RouterModule],
|
||||
providers: [provideScrollPositionRestoration()],
|
||||
})
|
||||
export class AppRoutingModule {
|
||||
constructor() {
|
||||
// Loading TabNavigationService to ensure tab state is synced with tab location
|
||||
inject(TabNavigationService);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,233 +1,253 @@
|
||||
import {
|
||||
HTTP_INTERCEPTORS,
|
||||
provideHttpClient,
|
||||
withInterceptorsFromDi,
|
||||
} from '@angular/common/http';
|
||||
import {
|
||||
ErrorHandler,
|
||||
Injector,
|
||||
LOCALE_ID,
|
||||
NgModule,
|
||||
inject,
|
||||
provideAppInitializer,
|
||||
} from '@angular/core';
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
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 { CoreCommandModule } from '@core/command';
|
||||
|
||||
import { AppRoutingModule } from './app-routing.module';
|
||||
import { AppComponent } from './app.component';
|
||||
import { CoreApplicationModule } from '@core/application';
|
||||
import { AppStoreModule } from './app-store.module';
|
||||
import { ServiceWorkerModule } from '@angular/service-worker';
|
||||
import { environment } from '../environments/environment';
|
||||
import { AppSwaggerModule } from './app-swagger.module';
|
||||
import { AppDomainModule } from './app-domain.module';
|
||||
import { UiModalModule } from '@ui/modal';
|
||||
import {
|
||||
NotificationsHubModule,
|
||||
NOTIFICATIONS_HUB_OPTIONS,
|
||||
} from '@hub/notifications';
|
||||
import { SignalRHubOptions } from '@core/signalr';
|
||||
import { CoreBreadcrumbModule } from '@core/breadcrumb';
|
||||
import { UiCommonModule } from '@ui/common';
|
||||
import { registerLocaleData } from '@angular/common';
|
||||
|
||||
import localeDe from '@angular/common/locales/de';
|
||||
import localeDeExtra from '@angular/common/locales/extra/de';
|
||||
import { HttpErrorInterceptor } from './interceptors';
|
||||
import { CoreLoggerModule, LOG_PROVIDER } from '@core/logger';
|
||||
import { IsaLogProvider } from './providers';
|
||||
import { IsaErrorHandler } from './providers/isa.error-handler';
|
||||
import {
|
||||
ScanAdapterModule,
|
||||
ScanAdapterService,
|
||||
ScanditScanAdapterModule,
|
||||
} from '@adapter/scan';
|
||||
import { RootStateService } from './store/root-state.service';
|
||||
import * as Commands from './commands';
|
||||
import { PreviewComponent } from './preview';
|
||||
import { NativeContainerService } from '@external/native-container';
|
||||
import { ShellModule } from '@shared/shell';
|
||||
import { MainComponent } from './main.component';
|
||||
import { IconModule } from '@shared/components/icon';
|
||||
import { NgIconsModule } from '@ng-icons/core';
|
||||
import {
|
||||
matClose,
|
||||
matWifi,
|
||||
matWifiOff,
|
||||
} from '@ng-icons/material-icons/baseline';
|
||||
import { NetworkStatusService } from './services/network-status.service';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import { provideMatomo } from 'ngx-matomo-client';
|
||||
import { withRouter, withRouteData } from 'ngx-matomo-client';
|
||||
import {
|
||||
provideLogging,
|
||||
withLogLevel,
|
||||
LogLevel,
|
||||
withSink,
|
||||
ConsoleLogSink,
|
||||
} from '@isa/core/logging';
|
||||
|
||||
registerLocaleData(localeDe, localeDeExtra);
|
||||
registerLocaleData(localeDe, 'de', localeDeExtra);
|
||||
|
||||
export function _appInitializerFactory(config: Config, injector: Injector) {
|
||||
return async () => {
|
||||
const statusElement = document.querySelector('#init-status');
|
||||
const laoderElement = document.querySelector('#init-loader');
|
||||
|
||||
try {
|
||||
let online = false;
|
||||
const networkStatus = injector.get(NetworkStatusService);
|
||||
while (!online) {
|
||||
online = await firstValueFrom(networkStatus.online$);
|
||||
|
||||
if (!online) {
|
||||
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.';
|
||||
await new Promise((resolve) => setTimeout(resolve, 250));
|
||||
}
|
||||
}
|
||||
|
||||
statusElement.innerHTML = 'Konfigurationen werden geladen...';
|
||||
|
||||
statusElement.innerHTML = 'Scanner wird initialisiert...';
|
||||
const scanAdapter = injector.get(ScanAdapterService);
|
||||
await scanAdapter.init();
|
||||
|
||||
statusElement.innerHTML = 'Authentifizierung wird geprüft...';
|
||||
|
||||
const auth = injector.get(AuthService);
|
||||
try {
|
||||
await auth.init();
|
||||
} catch (error) {
|
||||
statusElement.innerHTML = 'Authentifizierung wird durchgeführt...';
|
||||
const strategy = injector.get(LoginStrategy);
|
||||
await strategy.login();
|
||||
}
|
||||
|
||||
statusElement.innerHTML = 'App wird initialisiert...';
|
||||
const state = injector.get(RootStateService);
|
||||
await state.init();
|
||||
|
||||
statusElement.innerHTML = 'Native Container wird initialisiert...';
|
||||
const nativeContainer = injector.get(NativeContainerService);
|
||||
await nativeContainer.init();
|
||||
} catch (error) {
|
||||
laoderElement.remove();
|
||||
statusElement.classList.add('text-xl');
|
||||
statusElement.innerHTML +=
|
||||
'⚡<br><br><b>Fehler bei der Initialisierung</b><br><br>Bitte prüfen Sie die Netzwerkverbindung (WLAN).<br><br>';
|
||||
|
||||
const reload = document.createElement('button');
|
||||
reload.classList.add(
|
||||
'bg-brand',
|
||||
'text-white',
|
||||
'p-2',
|
||||
'rounded',
|
||||
'cursor-pointer',
|
||||
);
|
||||
reload.innerHTML = 'App neu laden';
|
||||
reload.onclick = () => window.location.reload();
|
||||
statusElement.appendChild(reload);
|
||||
|
||||
const preLabel = document.createElement('div');
|
||||
preLabel.classList.add('mt-12');
|
||||
preLabel.innerHTML = 'Fehlermeldung:';
|
||||
|
||||
statusElement.appendChild(preLabel);
|
||||
|
||||
const pre = document.createElement('pre');
|
||||
pre.classList.add('mt-4', 'text-wrap');
|
||||
pre.innerHTML = error.message;
|
||||
|
||||
statusElement.appendChild(pre);
|
||||
|
||||
console.error('Error during app initialization', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function _notificationsHubOptionsFactory(
|
||||
config: Config,
|
||||
auth: AuthService,
|
||||
): SignalRHubOptions {
|
||||
const options = { ...config.get('hubs').notifications };
|
||||
options.httpOptions.accessTokenFactory = () => auth.getToken();
|
||||
return options;
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
declarations: [AppComponent, MainComponent],
|
||||
bootstrap: [AppComponent],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
BrowserAnimationsModule,
|
||||
ShellModule.forRoot(),
|
||||
AppRoutingModule,
|
||||
AppSwaggerModule,
|
||||
AppDomainModule,
|
||||
CoreBreadcrumbModule.forRoot(),
|
||||
CoreCommandModule.forRoot(Object.values(Commands)),
|
||||
CoreLoggerModule.forRoot(),
|
||||
AppStoreModule,
|
||||
PreviewComponent,
|
||||
AuthModule.forRoot(),
|
||||
CoreApplicationModule.forRoot(),
|
||||
UiModalModule.forRoot(),
|
||||
UiCommonModule.forRoot(),
|
||||
NotificationsHubModule.forRoot(),
|
||||
ServiceWorkerModule.register('ngsw-worker.js', {
|
||||
enabled: environment.production,
|
||||
registrationStrategy: 'registerWhenStable:30000',
|
||||
}),
|
||||
ScanAdapterModule.forRoot(),
|
||||
ScanditScanAdapterModule.forRoot(),
|
||||
PlatformModule,
|
||||
IconModule.forRoot(),
|
||||
NgIconsModule.withIcons({ matWifiOff, matClose, matWifi }),
|
||||
],
|
||||
providers: [
|
||||
provideAppInitializer(() => {
|
||||
const initializerFn = _appInitializerFactory(
|
||||
inject(Config),
|
||||
inject(Injector),
|
||||
);
|
||||
return initializerFn();
|
||||
}),
|
||||
{
|
||||
provide: NOTIFICATIONS_HUB_OPTIONS,
|
||||
useFactory: _notificationsHubOptionsFactory,
|
||||
deps: [Config, AuthService],
|
||||
},
|
||||
{
|
||||
provide: HTTP_INTERCEPTORS,
|
||||
useClass: HttpErrorInterceptor,
|
||||
multi: true,
|
||||
},
|
||||
{
|
||||
provide: LOG_PROVIDER,
|
||||
useClass: IsaLogProvider,
|
||||
multi: true,
|
||||
},
|
||||
{
|
||||
provide: ErrorHandler,
|
||||
useClass: IsaErrorHandler,
|
||||
},
|
||||
{ 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)),
|
||||
],
|
||||
})
|
||||
export class AppModule {}
|
||||
import {
|
||||
HTTP_INTERCEPTORS,
|
||||
provideHttpClient,
|
||||
withInterceptorsFromDi,
|
||||
} from '@angular/common/http';
|
||||
import {
|
||||
DEFAULT_CURRENCY_CODE,
|
||||
ErrorHandler,
|
||||
Injector,
|
||||
LOCALE_ID,
|
||||
NgModule,
|
||||
inject,
|
||||
provideAppInitializer,
|
||||
} from '@angular/core';
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
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 { CoreCommandModule } from '@core/command';
|
||||
|
||||
import { AppRoutingModule } from './app-routing.module';
|
||||
import { AppComponent } from './app.component';
|
||||
import {
|
||||
ApplicationService,
|
||||
ApplicationServiceAdapter,
|
||||
CoreApplicationModule,
|
||||
} from '@core/application';
|
||||
import { AppStoreModule } from './app-store.module';
|
||||
import { ServiceWorkerModule } from '@angular/service-worker';
|
||||
import { environment } from '../environments/environment';
|
||||
import { AppSwaggerModule } from './app-swagger.module';
|
||||
import { AppDomainModule } from './app-domain.module';
|
||||
import { UiModalModule } from '@ui/modal';
|
||||
import {
|
||||
NotificationsHubModule,
|
||||
NOTIFICATIONS_HUB_OPTIONS,
|
||||
} from '@hub/notifications';
|
||||
import { SignalRHubOptions } from '@core/signalr';
|
||||
import { CoreBreadcrumbModule } from '@core/breadcrumb';
|
||||
import { UiCommonModule } from '@ui/common';
|
||||
import { registerLocaleData } from '@angular/common';
|
||||
|
||||
import localeDe from '@angular/common/locales/de';
|
||||
import localeDeExtra from '@angular/common/locales/extra/de';
|
||||
import { HttpErrorInterceptor } from './interceptors';
|
||||
import { CoreLoggerModule, LOG_PROVIDER } from '@core/logger';
|
||||
import { IsaLogProvider } from './providers';
|
||||
import { IsaErrorHandler } from './providers/isa.error-handler';
|
||||
import {
|
||||
ScanAdapterModule,
|
||||
ScanAdapterService,
|
||||
ScanditScanAdapterModule,
|
||||
} from '@adapter/scan';
|
||||
import { RootStateService } from './store/root-state.service';
|
||||
import * as Commands from './commands';
|
||||
import { PreviewComponent } from './preview';
|
||||
import { NativeContainerService } from '@external/native-container';
|
||||
import { ShellModule } from '@shared/shell';
|
||||
import { MainComponent } from './main.component';
|
||||
import { IconModule } from '@shared/components/icon';
|
||||
import { NgIconsModule } from '@ng-icons/core';
|
||||
import {
|
||||
matClose,
|
||||
matWifi,
|
||||
matWifiOff,
|
||||
} from '@ng-icons/material-icons/baseline';
|
||||
import { NetworkStatusService } from './services/network-status.service';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import { provideMatomo } from 'ngx-matomo-client';
|
||||
import { withRouter, withRouteData } from 'ngx-matomo-client';
|
||||
import {
|
||||
provideLogging,
|
||||
withLogLevel,
|
||||
LogLevel,
|
||||
withSink,
|
||||
ConsoleLogSink,
|
||||
} from '@isa/core/logging';
|
||||
import { IDBStorageProvider, UserStorageProvider } from '@isa/core/storage';
|
||||
|
||||
registerLocaleData(localeDe, localeDeExtra);
|
||||
registerLocaleData(localeDe, 'de', localeDeExtra);
|
||||
|
||||
export function _appInitializerFactory(config: Config, injector: Injector) {
|
||||
return async () => {
|
||||
const statusElement = document.querySelector('#init-status');
|
||||
const laoderElement = document.querySelector('#init-loader');
|
||||
|
||||
try {
|
||||
let online = false;
|
||||
const networkStatus = injector.get(NetworkStatusService);
|
||||
while (!online) {
|
||||
online = await firstValueFrom(networkStatus.online$);
|
||||
|
||||
if (!online) {
|
||||
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.';
|
||||
await new Promise((resolve) => setTimeout(resolve, 250));
|
||||
}
|
||||
}
|
||||
|
||||
statusElement.innerHTML = 'Konfigurationen werden geladen...';
|
||||
|
||||
statusElement.innerHTML = 'Scanner wird initialisiert...';
|
||||
const scanAdapter = injector.get(ScanAdapterService);
|
||||
await scanAdapter.init();
|
||||
|
||||
statusElement.innerHTML = 'Authentifizierung wird geprüft...';
|
||||
|
||||
const auth = injector.get(AuthService);
|
||||
try {
|
||||
await auth.init();
|
||||
} catch (error) {
|
||||
statusElement.innerHTML = 'Authentifizierung wird durchgeführt...';
|
||||
const strategy = injector.get(LoginStrategy);
|
||||
await strategy.login();
|
||||
}
|
||||
|
||||
statusElement.innerHTML = 'App wird initialisiert...';
|
||||
const state = injector.get(RootStateService);
|
||||
await state.init();
|
||||
|
||||
statusElement.innerHTML = 'Native Container wird initialisiert...';
|
||||
const nativeContainer = injector.get(NativeContainerService);
|
||||
await nativeContainer.init();
|
||||
|
||||
statusElement.innerHTML = 'Datenbank wird initialisiert...';
|
||||
await injector.get(IDBStorageProvider).init();
|
||||
|
||||
statusElement.innerHTML = 'Benutzerzustand wird geladen...';
|
||||
await injector.get(UserStorageProvider).init();
|
||||
} catch (error) {
|
||||
laoderElement.remove();
|
||||
statusElement.classList.add('text-xl');
|
||||
statusElement.innerHTML +=
|
||||
'⚡<br><br><b>Fehler bei der Initialisierung</b><br><br>Bitte prüfen Sie die Netzwerkverbindung (WLAN).<br><br>';
|
||||
|
||||
const reload = document.createElement('button');
|
||||
reload.classList.add(
|
||||
'bg-brand',
|
||||
'text-white',
|
||||
'p-2',
|
||||
'rounded',
|
||||
'cursor-pointer',
|
||||
);
|
||||
reload.innerHTML = 'App neu laden';
|
||||
reload.onclick = () => window.location.reload();
|
||||
statusElement.appendChild(reload);
|
||||
|
||||
const preLabel = document.createElement('div');
|
||||
preLabel.classList.add('mt-12');
|
||||
preLabel.innerHTML = 'Fehlermeldung:';
|
||||
|
||||
statusElement.appendChild(preLabel);
|
||||
|
||||
const pre = document.createElement('pre');
|
||||
pre.classList.add('mt-4', 'text-wrap');
|
||||
pre.innerHTML = error.message;
|
||||
|
||||
statusElement.appendChild(pre);
|
||||
|
||||
console.error('Error during app initialization', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function _notificationsHubOptionsFactory(
|
||||
config: Config,
|
||||
auth: AuthService,
|
||||
): SignalRHubOptions {
|
||||
const options = { ...config.get('hubs').notifications };
|
||||
options.httpOptions.accessTokenFactory = () => auth.getToken();
|
||||
return options;
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
declarations: [AppComponent, MainComponent],
|
||||
bootstrap: [AppComponent],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
BrowserAnimationsModule,
|
||||
ShellModule.forRoot(),
|
||||
AppRoutingModule,
|
||||
AppSwaggerModule,
|
||||
AppDomainModule,
|
||||
CoreBreadcrumbModule.forRoot(),
|
||||
CoreCommandModule.forRoot(Object.values(Commands)),
|
||||
CoreLoggerModule.forRoot(),
|
||||
AppStoreModule,
|
||||
PreviewComponent,
|
||||
AuthModule.forRoot(),
|
||||
CoreApplicationModule.forRoot(),
|
||||
UiModalModule.forRoot(),
|
||||
UiCommonModule.forRoot(),
|
||||
NotificationsHubModule.forRoot(),
|
||||
ServiceWorkerModule.register('ngsw-worker.js', {
|
||||
enabled: environment.production,
|
||||
registrationStrategy: 'registerWhenStable:30000',
|
||||
}),
|
||||
ScanAdapterModule.forRoot(),
|
||||
ScanditScanAdapterModule.forRoot(),
|
||||
PlatformModule,
|
||||
IconModule.forRoot(),
|
||||
NgIconsModule.withIcons({ matWifiOff, matClose, matWifi }),
|
||||
],
|
||||
providers: [
|
||||
provideAppInitializer(() => {
|
||||
const initializerFn = _appInitializerFactory(
|
||||
inject(Config),
|
||||
inject(Injector),
|
||||
);
|
||||
return initializerFn();
|
||||
}),
|
||||
{
|
||||
provide: NOTIFICATIONS_HUB_OPTIONS,
|
||||
useFactory: _notificationsHubOptionsFactory,
|
||||
deps: [Config, AuthService],
|
||||
},
|
||||
{
|
||||
provide: HTTP_INTERCEPTORS,
|
||||
useClass: HttpErrorInterceptor,
|
||||
multi: true,
|
||||
},
|
||||
{
|
||||
provide: LOG_PROVIDER,
|
||||
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 { ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
|
||||
import { ApplicationProcess, ApplicationService } from '@core/application';
|
||||
import { BreadcrumbService } from '@core/breadcrumb';
|
||||
import { first } from 'rxjs/operators';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class CanActivateProductWithProcessIdGuard {
|
||||
constructor(
|
||||
private readonly _applicationService: ApplicationService,
|
||||
private readonly _breadcrumbService: BreadcrumbService,
|
||||
) {}
|
||||
|
||||
async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
|
||||
const process = await this._applicationService.getProcessById$(+route.params.processId).pipe(first()).toPromise();
|
||||
|
||||
// if (!(process?.type === 'cart')) {
|
||||
// // TODO:
|
||||
// // Anderer Prozesstyp mit gleicher Id - Was soll gemacht werden?
|
||||
// return false;
|
||||
// }
|
||||
|
||||
if (!process) {
|
||||
const processes = await this._applicationService.getProcesses$('customer').pipe(first()).toPromise();
|
||||
await this._applicationService.createProcess({
|
||||
id: +route.params.processId,
|
||||
type: 'cart',
|
||||
section: 'customer',
|
||||
name: `Vorgang ${this.processNumber(processes.filter((process) => process.type === 'cart'))}`,
|
||||
});
|
||||
}
|
||||
|
||||
await this.removeBreadcrumbWithSameProcessId(route);
|
||||
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) {
|
||||
const crumbs = await this._breadcrumbService.getBreadcrumbByKey$(+route.params.processId).pipe(first()).toPromise();
|
||||
|
||||
// Entferne alle Crumbs die nichts mit der Artikelsuche zu tun haben
|
||||
if (crumbs.length > 1) {
|
||||
const crumbsToRemove = crumbs.filter((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;
|
||||
}
|
||||
|
||||
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)
|
||||
// ----------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
// for (let missingNumber = 1; missingNumber < Math.max(...processNumbers); missingNumber++) {
|
||||
// if (!processNumbers.find((number) => number === missingNumber)) {
|
||||
// return missingNumber;
|
||||
// }
|
||||
// }
|
||||
return Math.max(...processNumbers) + 1;
|
||||
}
|
||||
}
|
||||
import { Injectable } from '@angular/core';
|
||||
import { ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
|
||||
import { ApplicationProcess, ApplicationService } from '@core/application';
|
||||
import { BreadcrumbService } from '@core/breadcrumb';
|
||||
import { first } from 'rxjs/operators';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class CanActivateProductWithProcessIdGuard {
|
||||
constructor(
|
||||
private readonly _applicationService: ApplicationService,
|
||||
private readonly _breadcrumbService: BreadcrumbService,
|
||||
) {}
|
||||
|
||||
async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
|
||||
const processId = +route.params.processId;
|
||||
const process = await this._applicationService
|
||||
.getProcessById$(processId)
|
||||
.pipe(first())
|
||||
.toPromise();
|
||||
|
||||
// if (!(process?.type === 'cart')) {
|
||||
// // TODO:
|
||||
// // Anderer Prozesstyp mit gleicher Id - Was soll gemacht werden?
|
||||
// return false;
|
||||
// }
|
||||
|
||||
if (!process) {
|
||||
await this._applicationService.createCustomerProcess(processId);
|
||||
}
|
||||
|
||||
await this.removeBreadcrumbWithSameProcessId(route);
|
||||
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) {
|
||||
const crumbs = await this._breadcrumbService
|
||||
.getBreadcrumbByKey$(+route.params.processId)
|
||||
.pipe(first())
|
||||
.toPromise();
|
||||
|
||||
// Entferne alle Crumbs die nichts mit der Artikelsuche zu tun haben
|
||||
if (crumbs.length > 1) {
|
||||
const crumbsToRemove = crumbs.filter(
|
||||
(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;
|
||||
}
|
||||
|
||||
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)
|
||||
// ----------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
// for (let missingNumber = 1; missingNumber < Math.max(...processNumbers); missingNumber++) {
|
||||
// if (!processNumbers.find((number) => number === missingNumber)) {
|
||||
// return missingNumber;
|
||||
// }
|
||||
// }
|
||||
return Math.max(...processNumbers) + 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
import { HttpErrorResponse } from '@angular/common/http';
|
||||
import { ErrorHandler, Injectable } from '@angular/core';
|
||||
import { AuthService } from '@core/auth';
|
||||
import { DialogModel, UiDialogModalComponent, UiErrorModalComponent, UiModalService } from '@ui/modal';
|
||||
import { IsaLogProvider } from './isa.log-provider';
|
||||
import { LogLevel } from '@core/logger';
|
||||
import { HttpErrorResponse } from "@angular/common/http";
|
||||
import { ErrorHandler, Injectable } from "@angular/core";
|
||||
import { AuthService } from "@core/auth";
|
||||
import {
|
||||
DialogModel,
|
||||
UiDialogModalComponent,
|
||||
UiErrorModalComponent,
|
||||
UiModalService,
|
||||
} from "@ui/modal";
|
||||
import { IsaLogProvider } from "./isa.log-provider";
|
||||
import { LogLevel } from "@core/logger";
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
@Injectable({ providedIn: "root" })
|
||||
export class IsaErrorHandler implements ErrorHandler {
|
||||
constructor(
|
||||
private _modal: UiModalService,
|
||||
@@ -17,7 +22,7 @@ export class IsaErrorHandler implements ErrorHandler {
|
||||
console.error(error);
|
||||
|
||||
// Bei Klick auf Abbrechen auf der Login Seite erneut zur Login Seite weiterleiten
|
||||
if (error?.type === 'token_error') {
|
||||
if (error?.type === "token_error") {
|
||||
this._authService.login();
|
||||
return;
|
||||
}
|
||||
@@ -26,11 +31,14 @@ export class IsaErrorHandler implements ErrorHandler {
|
||||
await this._modal
|
||||
.open({
|
||||
content: UiDialogModalComponent,
|
||||
title: 'Sitzung abgelaufen',
|
||||
title: "Sitzung abgelaufen",
|
||||
data: {
|
||||
handleCommand: false,
|
||||
content: 'Sie waren zu lange nicht in der ISA aktiv. Bitte melden Sie sich erneut an',
|
||||
actions: [{ command: 'CLOSE', selected: true, label: 'Erneut anmelden' }],
|
||||
content:
|
||||
"Sie waren zu lange nicht in der ISA aktiv. Bitte melden Sie sich erneut an",
|
||||
actions: [
|
||||
{ command: "CLOSE", selected: true, label: "Erneut anmelden" },
|
||||
],
|
||||
} as DialogModel,
|
||||
})
|
||||
.afterClosed$.toPromise();
|
||||
@@ -39,7 +47,11 @@ export class IsaErrorHandler implements ErrorHandler {
|
||||
return;
|
||||
}
|
||||
|
||||
this._isaLogProvider.log(LogLevel.ERROR, 'Client Error', error);
|
||||
try {
|
||||
this._isaLogProvider.log(LogLevel.ERROR, "Client Error", error);
|
||||
} catch (logError) {
|
||||
console.error("Error logging to IsaLogProvider:", logError);
|
||||
}
|
||||
|
||||
// this._modal.open({
|
||||
// content: UiErrorModalComponent,
|
||||
|
||||
@@ -1,28 +1,36 @@
|
||||
import { Injectable, Injector } from '@angular/core';
|
||||
import { LogLevel, LogProvider } from '@core/logger';
|
||||
import { UserStateService } from '@generated/swagger/isa-api';
|
||||
import { environment } from '../../environments/environment';
|
||||
import { Injectable } from "@angular/core";
|
||||
import { LogLevel, LogProvider } from "@core/logger";
|
||||
import { UserStateService } from "@generated/swagger/isa-api";
|
||||
import { environment } from "../../environments/environment";
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
@Injectable({ providedIn: "root" })
|
||||
export class IsaLogProvider implements LogProvider {
|
||||
static InfoService: UserStateService | undefined;
|
||||
|
||||
constructor() {}
|
||||
|
||||
log(logLevel: LogLevel, message: string, error: Error, ...optionalParams: any[]): void {
|
||||
if (!environment.production && (logLevel === LogLevel.WARN || logLevel === LogLevel.ERROR)) {
|
||||
IsaLogProvider.InfoService?.UserStateSaveLog({
|
||||
logType: logLevel,
|
||||
message: message,
|
||||
content: JSON.stringify({
|
||||
error: error?.name,
|
||||
message: error?.message,
|
||||
stack: error?.stack,
|
||||
data: optionalParams,
|
||||
}),
|
||||
})
|
||||
.toPromise()
|
||||
.catch(() => {});
|
||||
log(
|
||||
logLevel: LogLevel,
|
||||
message: string,
|
||||
error: Error,
|
||||
...optionalParams: any[]
|
||||
): void {
|
||||
try {
|
||||
if (
|
||||
!environment.production &&
|
||||
(logLevel === LogLevel.WARN || logLevel === LogLevel.ERROR)
|
||||
) {
|
||||
IsaLogProvider.InfoService?.UserStateSaveLog({
|
||||
logType: logLevel,
|
||||
message: message,
|
||||
content: JSON.stringify({
|
||||
error: error?.name,
|
||||
message: error?.message,
|
||||
stack: error?.stack,
|
||||
data: optionalParams,
|
||||
}),
|
||||
}).toPromise();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error logging to InfoService:", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { ActivatedRouteSnapshot } from '@angular/router';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class ProcessIdResolver {
|
||||
resolve(route: ActivatedRouteSnapshot): Observable<number> | Promise<number> | number {
|
||||
return route.params.processId;
|
||||
}
|
||||
}
|
||||
|
||||
import { Injectable } from '@angular/core';
|
||||
import { ActivatedRouteSnapshot } from '@angular/router';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class ProcessIdResolver {
|
||||
resolve(
|
||||
route: ActivatedRouteSnapshot,
|
||||
): Observable<number> | Promise<number> | number {
|
||||
return route.params.processId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,129 +1,124 @@
|
||||
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', {
|
||||
...state,
|
||||
version: packageInfo.version,
|
||||
sub: this._authService.getClaimByKey('sub'),
|
||||
});
|
||||
}),
|
||||
)
|
||||
.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);
|
||||
}
|
||||
}
|
||||
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))
|
||||
.subscribe((state) => {
|
||||
const data = {
|
||||
...state,
|
||||
version: packageInfo.version,
|
||||
sub: this._authService.getClaimByKey('sub'),
|
||||
};
|
||||
RootStateService.SaveToLocalStorageRaw(JSON.stringify(data));
|
||||
return this.#storage.set('state', data);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.service';
|
||||
export * from './application.service-adapter';
|
||||
export * from './defs';
|
||||
export * from './store';
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { LogLevel } from './log-level';
|
||||
import { LogLevel } from "./log-level";
|
||||
|
||||
export interface LogProvider {
|
||||
log(logLevel: LogLevel, message: string, ...optionalParams: any[]): void;
|
||||
|
||||
@@ -1,21 +1,22 @@
|
||||
import { enableProdMode, isDevMode } from '@angular/core';
|
||||
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
|
||||
import { CONFIG_DATA } from '@isa/core/config';
|
||||
import { setDefaultOptions } from 'date-fns';
|
||||
import { de } from 'date-fns/locale';
|
||||
import * as moment from 'moment';
|
||||
import { enableProdMode, isDevMode } from "@angular/core";
|
||||
import { platformBrowserDynamic } from "@angular/platform-browser-dynamic";
|
||||
import { CONFIG_DATA } from "@isa/core/config";
|
||||
import { setDefaultOptions } from "date-fns";
|
||||
import { de } from "date-fns/locale";
|
||||
import * as moment from "moment";
|
||||
import "moment/locale/de";
|
||||
|
||||
setDefaultOptions({ locale: de });
|
||||
moment.locale('de');
|
||||
moment.locale("de");
|
||||
|
||||
import { AppModule } from './app/app.module';
|
||||
import { AppModule } from "./app/app.module";
|
||||
|
||||
if (!isDevMode()) {
|
||||
enableProdMode();
|
||||
}
|
||||
|
||||
async function bootstrap() {
|
||||
const configRes = await fetch('/config/config.json');
|
||||
const configRes = await fetch("/config/config.json");
|
||||
|
||||
const config = await configRes.json();
|
||||
|
||||
|
||||
@@ -12,7 +12,13 @@ import {
|
||||
ShoppingCartItemDTO,
|
||||
} from '@generated/swagger/checkout-api';
|
||||
import { DomainCheckoutService } from '@domain/checkout';
|
||||
import { catchError, mergeMap, switchMap, tap, withLatestFrom } from 'rxjs/operators';
|
||||
import {
|
||||
catchError,
|
||||
mergeMap,
|
||||
switchMap,
|
||||
tap,
|
||||
withLatestFrom,
|
||||
} from 'rxjs/operators';
|
||||
import {
|
||||
BranchService,
|
||||
DisplayOrderDTO,
|
||||
@@ -40,7 +46,10 @@ export interface KulturpassOrderModalState {
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class KulturpassOrderModalStore extends ComponentStore<KulturpassOrderModalState> implements OnStoreInit {
|
||||
export class KulturpassOrderModalStore
|
||||
extends ComponentStore<KulturpassOrderModalState>
|
||||
implements OnStoreInit
|
||||
{
|
||||
private _checkoutService = inject(DomainCheckoutService);
|
||||
private _branchService = inject(BranchService);
|
||||
private _authService = inject(AuthService);
|
||||
@@ -87,23 +96,33 @@ export class KulturpassOrderModalStore extends ComponentStore<KulturpassOrderMod
|
||||
|
||||
readonly order$ = this.select((state) => state.order);
|
||||
|
||||
readonly updateCheckout = this.updater((state, checkout: CheckoutDTO) => ({ ...state, checkout }));
|
||||
readonly updateCheckout = this.updater((state, checkout: CheckoutDTO) => ({
|
||||
...state,
|
||||
checkout,
|
||||
}));
|
||||
|
||||
readonly updateOrder = this.updater((state, order: OrderDTO) => ({ ...state, order }));
|
||||
readonly updateOrder = this.updater((state, order: OrderDTO) => ({
|
||||
...state,
|
||||
order,
|
||||
}));
|
||||
|
||||
readonly fetchShoppingCart$ = this.select((state) => state.fetchShoppingCart);
|
||||
|
||||
readonly updateFetchShoppingCart = this.updater((state, fetchShoppingCart: boolean) => ({
|
||||
...state,
|
||||
fetchShoppingCart,
|
||||
}));
|
||||
readonly updateFetchShoppingCart = this.updater(
|
||||
(state, fetchShoppingCart: boolean) => ({
|
||||
...state,
|
||||
fetchShoppingCart,
|
||||
}),
|
||||
);
|
||||
|
||||
readonly ordering$ = this.select((state) => state.ordering);
|
||||
|
||||
loadBranch = this.effect(($) =>
|
||||
$.pipe(
|
||||
switchMap(() =>
|
||||
this._branchService.BranchGetBranches({}).pipe(tapResponse(this.handleBranchResponse, this.handleBranchError)),
|
||||
this._branchService
|
||||
.BranchGetBranches({})
|
||||
.pipe(tapResponse(this.handleBranchResponse, this.handleBranchError)),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -111,31 +130,45 @@ export class KulturpassOrderModalStore extends ComponentStore<KulturpassOrderMod
|
||||
handleBranchResponse = (res: ResponseArgsOfIEnumerableOfBranchDTO) => {
|
||||
const branchNumber = this._authService.getClaimByKey('branch_no');
|
||||
|
||||
this.patchState({ branch: res.result.find((b) => b.branchNumber === branchNumber) });
|
||||
this.patchState({
|
||||
branch: res.result.find((b) => b.branchNumber === branchNumber),
|
||||
});
|
||||
};
|
||||
|
||||
handleBranchError = (err) => {
|
||||
this._modal.error('Fehler beim Laden der Filiale', err);
|
||||
};
|
||||
|
||||
createShoppingCart = this.effect((orderItemListItem$: Observable<OrderItemListItemDTO>) =>
|
||||
orderItemListItem$.pipe(
|
||||
tap((orderItemListItem) => {
|
||||
this.patchState({ orderItemListItem });
|
||||
this.updateFetchShoppingCart(true);
|
||||
}),
|
||||
switchMap((orderItemListItem) =>
|
||||
this._checkoutService
|
||||
.getShoppingCart({ processId: this.processId })
|
||||
.pipe(tapResponse(this.handleCreateShoppingCartResponse, this.handleCreateShoppingCartError)),
|
||||
createShoppingCart = this.effect(
|
||||
(orderItemListItem$: Observable<OrderItemListItemDTO>) =>
|
||||
orderItemListItem$.pipe(
|
||||
tap((orderItemListItem) => {
|
||||
this.patchState({ orderItemListItem });
|
||||
this.updateFetchShoppingCart(true);
|
||||
}),
|
||||
switchMap((orderItemListItem) =>
|
||||
this._checkoutService
|
||||
.getShoppingCart({ processId: this.processId })
|
||||
.pipe(
|
||||
tapResponse(
|
||||
this.handleCreateShoppingCartResponse,
|
||||
this.handleCreateShoppingCartError,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
handleCreateShoppingCartResponse = (res: ShoppingCartDTO) => {
|
||||
this.patchState({ shoppingCart: res });
|
||||
this._checkoutService.setBuyer({ processId: this.processId, buyer: this.order.buyer });
|
||||
this._checkoutService.setPayer({ processId: this.processId, payer: this.order.billing?.data });
|
||||
this._checkoutService.setBuyer({
|
||||
processId: this.processId,
|
||||
buyer: this.order.buyer,
|
||||
});
|
||||
this._checkoutService.setPayer({
|
||||
processId: this.processId,
|
||||
payer: this.order.billing?.data,
|
||||
});
|
||||
|
||||
this.updateFetchShoppingCart(false);
|
||||
};
|
||||
@@ -154,7 +187,9 @@ export class KulturpassOrderModalStore extends ComponentStore<KulturpassOrderMod
|
||||
processId: this.processId,
|
||||
items: [add],
|
||||
})
|
||||
.pipe(tapResponse(this.handleAddItemResponse, this.handleAddItemError)),
|
||||
.pipe(
|
||||
tapResponse(this.handleAddItemResponse, this.handleAddItemError),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -174,7 +209,12 @@ export class KulturpassOrderModalStore extends ComponentStore<KulturpassOrderMod
|
||||
shoppingCartItemId: change.id,
|
||||
update: { quantity: change.quantity },
|
||||
})
|
||||
.pipe(tapResponse(this.handleQuantityChangeResponse, this.handleQuantityChangeError)),
|
||||
.pipe(
|
||||
tapResponse(
|
||||
this.handleQuantityChangeResponse,
|
||||
this.handleQuantityChangeError,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -206,7 +246,10 @@ export class KulturpassOrderModalStore extends ComponentStore<KulturpassOrderMod
|
||||
this.onOrderSuccess(res.result.item1[0], res.result.item2);
|
||||
};
|
||||
|
||||
onOrderSuccess = (displayOrder: DisplayOrderDTO, action: KeyValueDTOOfStringAndString[]) => {};
|
||||
onOrderSuccess = (
|
||||
displayOrder: DisplayOrderDTO,
|
||||
action: KeyValueDTOOfStringAndString[],
|
||||
) => {};
|
||||
|
||||
handleOrderError = (err: any) => {
|
||||
this._modal.error('Fehler beim Bestellen', err);
|
||||
@@ -215,8 +258,9 @@ export class KulturpassOrderModalStore extends ComponentStore<KulturpassOrderMod
|
||||
|
||||
itemQuantityByCatalogProductNumber(catalogProductNumber: string) {
|
||||
return (
|
||||
this.shoppingCart?.items?.find((i) => getCatalogProductNumber(i?.data) === catalogProductNumber)?.data
|
||||
?.quantity ?? 0
|
||||
this.shoppingCart?.items?.find(
|
||||
(i) => getCatalogProductNumber(i?.data) === catalogProductNumber,
|
||||
)?.data?.quantity ?? 0
|
||||
);
|
||||
}
|
||||
|
||||
@@ -227,7 +271,11 @@ export class KulturpassOrderModalStore extends ComponentStore<KulturpassOrderMod
|
||||
.canAddItemsKulturpass([item?.product])
|
||||
.pipe(
|
||||
tapResponse(
|
||||
(results) => this.handleCanAddItemResponse({ item, result: results?.find((_) => true) }),
|
||||
(results) =>
|
||||
this.handleCanAddItemResponse({
|
||||
item,
|
||||
result: results?.find((_) => true),
|
||||
}),
|
||||
this.handleCanAddItemError,
|
||||
),
|
||||
),
|
||||
@@ -235,14 +283,23 @@ export class KulturpassOrderModalStore extends ComponentStore<KulturpassOrderMod
|
||||
),
|
||||
);
|
||||
|
||||
handleCanAddItemResponse = ({ item, result }: { item: ItemDTO; result: KulturPassResult }) => {
|
||||
handleCanAddItemResponse = ({
|
||||
item,
|
||||
result,
|
||||
}: {
|
||||
item: ItemDTO;
|
||||
result: KulturPassResult;
|
||||
}) => {
|
||||
if (result?.canAdd) {
|
||||
this.addItemToShoppingCart(item);
|
||||
} else {
|
||||
this._modal.open({
|
||||
content: UiMessageModalComponent,
|
||||
title: 'Artikel nicht förderfähig',
|
||||
data: { message: result?.message, closeAction: 'ohne Artikel fortfahren' },
|
||||
data: {
|
||||
message: result?.message,
|
||||
closeAction: 'ohne Artikel fortfahren',
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -254,14 +311,18 @@ export class KulturpassOrderModalStore extends ComponentStore<KulturpassOrderMod
|
||||
addItemToShoppingCart = this.effect((item$: Observable<ItemDTO>) =>
|
||||
item$.pipe(
|
||||
mergeMap((item) => {
|
||||
const takeAwayAvailability$ = this._availabilityService.getTakeAwayAvailability({
|
||||
item: {
|
||||
ean: item.product.ean,
|
||||
itemId: item.id,
|
||||
price: item.catalogAvailability.price,
|
||||
},
|
||||
quantity: this.itemQuantityByCatalogProductNumber(getCatalogProductNumber(item)) + 1,
|
||||
});
|
||||
const takeAwayAvailability$ =
|
||||
this._availabilityService.getTakeAwayAvailability({
|
||||
item: {
|
||||
ean: item.product.ean,
|
||||
itemId: item.id,
|
||||
price: item.catalogAvailability.price,
|
||||
},
|
||||
quantity:
|
||||
this.itemQuantityByCatalogProductNumber(
|
||||
getCatalogProductNumber(item),
|
||||
) + 1,
|
||||
});
|
||||
|
||||
const deliveryAvailability$ = this._availabilityService
|
||||
.getDeliveryAvailability({
|
||||
@@ -270,7 +331,10 @@ export class KulturpassOrderModalStore extends ComponentStore<KulturpassOrderMod
|
||||
itemId: item.id,
|
||||
price: item.catalogAvailability.price,
|
||||
},
|
||||
quantity: this.itemQuantityByCatalogProductNumber(getCatalogProductNumber(item)) + 1,
|
||||
quantity:
|
||||
this.itemQuantityByCatalogProductNumber(
|
||||
getCatalogProductNumber(item),
|
||||
) + 1,
|
||||
})
|
||||
.pipe(
|
||||
catchError((err) => {
|
||||
@@ -279,7 +343,10 @@ export class KulturpassOrderModalStore extends ComponentStore<KulturpassOrderMod
|
||||
);
|
||||
|
||||
return zip(takeAwayAvailability$, deliveryAvailability$).pipe(
|
||||
tapResponse(this.handleAddItemToShoppingCartResponse2(item), this.handleAddItemToShoppingCartError),
|
||||
tapResponse(
|
||||
this.handleAddItemToShoppingCartResponse2(item),
|
||||
this.handleAddItemToShoppingCartError,
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
@@ -287,20 +354,27 @@ export class KulturpassOrderModalStore extends ComponentStore<KulturpassOrderMod
|
||||
|
||||
handleAddItemToShoppingCartResponse2 =
|
||||
(item: ItemDTO) =>
|
||||
([takeAwayAvailability, deliveryAvailability]: [AvailabilityDTO, AvailabilityDTO]) => {
|
||||
([takeAwayAvailability, deliveryAvailability]: [
|
||||
AvailabilityDTO,
|
||||
AvailabilityDTO,
|
||||
]) => {
|
||||
let isPriceMaintained = item?.catalogAvailability?.priceMaintained;
|
||||
let onlinePrice = -1;
|
||||
|
||||
if (deliveryAvailability) {
|
||||
isPriceMaintained = isPriceMaintained ?? deliveryAvailability['priceMaintained'] ?? false;
|
||||
isPriceMaintained =
|
||||
isPriceMaintained ?? deliveryAvailability['priceMaintained'] ?? false;
|
||||
onlinePrice = deliveryAvailability?.price?.value?.value ?? -1;
|
||||
}
|
||||
|
||||
// Preis und priceMaintained werden immer erst vom Katalog genommen. Bei nicht Verfügbarkeit greifen die anderen Availabilities
|
||||
const offlinePrice =
|
||||
item?.catalogAvailability?.price?.value?.value ?? takeAwayAvailability?.price?.value?.value ?? -1;
|
||||
item?.catalogAvailability?.price?.value?.value ??
|
||||
takeAwayAvailability?.price?.value?.value ??
|
||||
-1;
|
||||
const availability = takeAwayAvailability;
|
||||
availability.price = item?.catalogAvailability?.price ?? takeAwayAvailability?.price;
|
||||
availability.price =
|
||||
item?.catalogAvailability?.price ?? takeAwayAvailability?.price;
|
||||
|
||||
/**
|
||||
* Onlinepreis ist niedliger als der Offlinepreis
|
||||
@@ -314,7 +388,11 @@ export class KulturpassOrderModalStore extends ComponentStore<KulturpassOrderMod
|
||||
* wenn der Artikel Preisgebunden ist, wird der Ladenpreis verwendet
|
||||
*/
|
||||
|
||||
if (!!deliveryAvailability && onlinePrice < offlinePrice && !isPriceMaintained) {
|
||||
if (
|
||||
!!deliveryAvailability &&
|
||||
onlinePrice < offlinePrice &&
|
||||
!isPriceMaintained
|
||||
) {
|
||||
availability.price = deliveryAvailability.price;
|
||||
}
|
||||
|
||||
@@ -333,10 +411,13 @@ export class KulturpassOrderModalStore extends ComponentStore<KulturpassOrderMod
|
||||
},
|
||||
},
|
||||
promotion: {
|
||||
points: 0,
|
||||
value: 0,
|
||||
},
|
||||
itemType: item.type,
|
||||
product: { catalogProductNumber: getCatalogProductNumber(item), ...item.product },
|
||||
product: {
|
||||
catalogProductNumber: getCatalogProductNumber(item),
|
||||
...item.product,
|
||||
},
|
||||
};
|
||||
|
||||
this.addItem(addToShoppingCartDTO);
|
||||
@@ -346,15 +427,28 @@ export class KulturpassOrderModalStore extends ComponentStore<KulturpassOrderMod
|
||||
this._modal.error('Fehler beim Hinzufügen des Artikels', err);
|
||||
};
|
||||
|
||||
setAvailability = this.updater((state, data: { catalogProductNumber: string; availability: AvailabilityDTO }) => {
|
||||
return { ...state, availabilities: { ...state.availabilities, [data.catalogProductNumber]: data.availability } };
|
||||
});
|
||||
setAvailability = this.updater(
|
||||
(
|
||||
state,
|
||||
data: { catalogProductNumber: string; availability: AvailabilityDTO },
|
||||
) => {
|
||||
return {
|
||||
...state,
|
||||
availabilities: {
|
||||
...state.availabilities,
|
||||
[data.catalogProductNumber]: data.availability,
|
||||
},
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
getAvailability(catalogProductNumber: string): AvailabilityDTO | undefined {
|
||||
return this.get((state) => state.availabilities[catalogProductNumber]);
|
||||
}
|
||||
|
||||
getAvailability$(catalogProductNumber: string): Observable<AvailabilityDTO | undefined> {
|
||||
getAvailability$(
|
||||
catalogProductNumber: string,
|
||||
): Observable<AvailabilityDTO | undefined> {
|
||||
return this.select((state) => state.availabilities[catalogProductNumber]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,11 @@ import {
|
||||
} from './purchase-options.helpers';
|
||||
import { Observable } from 'rxjs';
|
||||
import { first, switchMap } from 'rxjs/operators';
|
||||
import { DEFAULT_PRICE_DTO, DEFAULT_PRICE_VALUE, DEFAULT_VAT_VALUE } from '../constants';
|
||||
import {
|
||||
DEFAULT_PRICE_DTO,
|
||||
DEFAULT_PRICE_VALUE,
|
||||
DEFAULT_VAT_VALUE,
|
||||
} from '../constants';
|
||||
import {
|
||||
AddToShoppingCartDTO,
|
||||
EntityDTOContainerOfDestinationDTO,
|
||||
@@ -103,7 +107,9 @@ export class PurchaseOptionsStore extends ComponentStore<PurchaseOptionsState> {
|
||||
return this.get(Selectors.getPurchaseOptionsInAvailabilities);
|
||||
}
|
||||
|
||||
getPurchaseOptionsInAvailabilities$ = this.select(Selectors.getPurchaseOptionsInAvailabilities);
|
||||
getPurchaseOptionsInAvailabilities$ = this.select(
|
||||
Selectors.getPurchaseOptionsInAvailabilities,
|
||||
);
|
||||
|
||||
get itemsForList() {
|
||||
return this.get(Selectors.getItemsForList);
|
||||
@@ -160,29 +166,48 @@ export class PurchaseOptionsStore extends ComponentStore<PurchaseOptionsState> {
|
||||
});
|
||||
}
|
||||
|
||||
addFetchingAvailability = this.updater((state, fetchState: FetchingAvailability) => {
|
||||
return {
|
||||
...state,
|
||||
fetchingAvailabilities: [...state.fetchingAvailabilities, fetchState],
|
||||
};
|
||||
});
|
||||
addFetchingAvailability = this.updater(
|
||||
(state, fetchState: FetchingAvailability) => {
|
||||
return {
|
||||
...state,
|
||||
fetchingAvailabilities: [...state.fetchingAvailabilities, fetchState],
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
removeFetchingAvailability = this.updater((state, fetchState: FetchingAvailability) => {
|
||||
return {
|
||||
...state,
|
||||
fetchingAvailabilities: state.fetchingAvailabilities.filter((f) => f.id !== fetchState.id),
|
||||
};
|
||||
});
|
||||
removeFetchingAvailability = this.updater(
|
||||
(state, fetchState: FetchingAvailability) => {
|
||||
return {
|
||||
...state,
|
||||
fetchingAvailabilities: state.fetchingAvailabilities.filter(
|
||||
(f) => f.id !== fetchState.id,
|
||||
),
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
async initialize({ items, processId, type, inStoreBranch, pickupBranch }: PurchaseOptionsModalData) {
|
||||
const selectedBranch = await this._service.getSelectedBranchForProcess(processId).toPromise();
|
||||
async initialize({
|
||||
items,
|
||||
processId,
|
||||
type,
|
||||
inStoreBranch,
|
||||
pickupBranch,
|
||||
}: PurchaseOptionsModalData) {
|
||||
const selectedBranch = await this._service
|
||||
.getSelectedBranchForProcess(processId)
|
||||
.toPromise();
|
||||
const defaultBranch = await this._service.fetchDefaultBranch().toPromise();
|
||||
const customerFeatures = await this._service.getCustomerFeatures(processId).toPromise();
|
||||
const customerFeatures = await this._service
|
||||
.getCustomerFeatures(processId)
|
||||
.toPromise();
|
||||
|
||||
this.patchState({
|
||||
processId: processId,
|
||||
type: type,
|
||||
items: items.map((item) => ({ ...item, quantity: item['quantity'] ?? 1 })),
|
||||
items: items.map((item) => ({
|
||||
...item,
|
||||
quantity: item['quantity'] ?? 1,
|
||||
})),
|
||||
defaultBranch: selectedBranch ?? defaultBranch,
|
||||
pickupBranch: pickupBranch ?? selectedBranch,
|
||||
inStoreBranch: inStoreBranch ?? selectedBranch,
|
||||
@@ -255,7 +280,8 @@ export class PurchaseOptionsStore extends ComponentStore<PurchaseOptionsState> {
|
||||
const items = await this._catalogService
|
||||
.searchByEans({ eans: this.items.map((item) => item.product.ean) })
|
||||
.toPromise();
|
||||
if (items.result.length > 0) await this._addCatalogueAvailabilities(items.result);
|
||||
if (items.result.length > 0)
|
||||
await this._addCatalogueAvailabilities(items.result);
|
||||
} else {
|
||||
await this._addCatalogueAvailabilities(items as ItemDTO[]);
|
||||
}
|
||||
@@ -264,7 +290,9 @@ export class PurchaseOptionsStore extends ComponentStore<PurchaseOptionsState> {
|
||||
// #4813 - Function updated - "this.updater" didn't work, so "this.patchState" gets used instead
|
||||
// Added firstDayOfSale for catalogAvailability via ShoppingCart Route
|
||||
private async _addCatalogueAvailabilities(items: ItemDTO[]) {
|
||||
const currentAvailabilities = await this.availabilities$.pipe(first()).toPromise();
|
||||
const currentAvailabilities = await this.availabilities$
|
||||
.pipe(first())
|
||||
.toPromise();
|
||||
const availabilities = [...currentAvailabilities];
|
||||
items.forEach((item) => {
|
||||
const catalogAvailability = item.catalogAvailability;
|
||||
@@ -272,7 +300,10 @@ export class PurchaseOptionsStore extends ComponentStore<PurchaseOptionsState> {
|
||||
purchaseOption: 'catalog',
|
||||
itemId: item.id,
|
||||
ean: item.product.ean, // Hier brauchen wir zusätzlich die EAN - siehe purchase-options-list-item.component.ts: "get isEVT()"
|
||||
data: { price: catalogAvailability?.price, firstDayOfSale: catalogAvailability?.firstDayOfSale },
|
||||
data: {
|
||||
price: catalogAvailability?.price,
|
||||
firstDayOfSale: catalogAvailability?.firstDayOfSale,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -287,8 +318,14 @@ export class PurchaseOptionsStore extends ComponentStore<PurchaseOptionsState> {
|
||||
|
||||
const id = uniqueId('availability_');
|
||||
try {
|
||||
this.addFetchingAvailability({ id, itemId: itemData.sourceId, purchaseOption: 'pickup' });
|
||||
const res = await this._service.fetchPickupAvailability(itemData, itemData.quantity, branch).toPromise();
|
||||
this.addFetchingAvailability({
|
||||
id,
|
||||
itemId: itemData.sourceId,
|
||||
purchaseOption: 'pickup',
|
||||
});
|
||||
const res = await this._service
|
||||
.fetchPickupAvailability(itemData, itemData.quantity, branch)
|
||||
.toPromise();
|
||||
|
||||
const availability: Availability = {
|
||||
itemId: itemData.sourceId,
|
||||
@@ -301,7 +338,11 @@ export class PurchaseOptionsStore extends ComponentStore<PurchaseOptionsState> {
|
||||
console.error('_loadPickupAvailability', err);
|
||||
}
|
||||
|
||||
this.removeFetchingAvailability({ id, itemId: itemData.sourceId, purchaseOption: 'pickup' });
|
||||
this.removeFetchingAvailability({
|
||||
id,
|
||||
itemId: itemData.sourceId,
|
||||
purchaseOption: 'pickup',
|
||||
});
|
||||
}
|
||||
|
||||
private async _loadInStoreAvailability(itemData: ItemData) {
|
||||
@@ -310,8 +351,14 @@ export class PurchaseOptionsStore extends ComponentStore<PurchaseOptionsState> {
|
||||
|
||||
const id = uniqueId('availability_');
|
||||
try {
|
||||
this.addFetchingAvailability({ id, itemId: itemData.sourceId, purchaseOption: 'in-store' });
|
||||
const res = await this._service.fetchInStoreAvailability(itemData, itemData.quantity, branch).toPromise();
|
||||
this.addFetchingAvailability({
|
||||
id,
|
||||
itemId: itemData.sourceId,
|
||||
purchaseOption: 'in-store',
|
||||
});
|
||||
const res = await this._service
|
||||
.fetchInStoreAvailability(itemData, itemData.quantity, branch)
|
||||
.toPromise();
|
||||
|
||||
const availability: Availability = {
|
||||
itemId: itemData.sourceId,
|
||||
@@ -324,14 +371,24 @@ export class PurchaseOptionsStore extends ComponentStore<PurchaseOptionsState> {
|
||||
console.error('_loadInStoreAvailability', err);
|
||||
}
|
||||
|
||||
this.removeFetchingAvailability({ id, itemId: itemData.sourceId, purchaseOption: 'in-store' });
|
||||
this.removeFetchingAvailability({
|
||||
id,
|
||||
itemId: itemData.sourceId,
|
||||
purchaseOption: 'in-store',
|
||||
});
|
||||
}
|
||||
|
||||
private async _loadDeliveryAvailability(itemData: ItemData) {
|
||||
const id = uniqueId('availability_');
|
||||
try {
|
||||
this.addFetchingAvailability({ id, itemId: itemData.sourceId, purchaseOption: 'delivery' });
|
||||
const res = await this._service.fetchDeliveryAvailability(itemData, itemData.quantity).toPromise();
|
||||
this.addFetchingAvailability({
|
||||
id,
|
||||
itemId: itemData.sourceId,
|
||||
purchaseOption: 'delivery',
|
||||
});
|
||||
const res = await this._service
|
||||
.fetchDeliveryAvailability(itemData, itemData.quantity)
|
||||
.toPromise();
|
||||
const availability: Availability = {
|
||||
itemId: itemData.sourceId,
|
||||
purchaseOption: 'delivery',
|
||||
@@ -343,14 +400,24 @@ export class PurchaseOptionsStore extends ComponentStore<PurchaseOptionsState> {
|
||||
console.error('_loadDeliveryAvailability', error);
|
||||
}
|
||||
|
||||
this.removeFetchingAvailability({ id, itemId: itemData.sourceId, purchaseOption: 'delivery' });
|
||||
this.removeFetchingAvailability({
|
||||
id,
|
||||
itemId: itemData.sourceId,
|
||||
purchaseOption: 'delivery',
|
||||
});
|
||||
}
|
||||
|
||||
private async _loadDigDeliveryAvailability(itemData: ItemData) {
|
||||
const id = uniqueId('availability_');
|
||||
try {
|
||||
this.addFetchingAvailability({ id, itemId: itemData.sourceId, purchaseOption: 'dig-delivery' });
|
||||
const res = await this._service.fetchDigDeliveryAvailability(itemData, itemData.quantity).toPromise();
|
||||
this.addFetchingAvailability({
|
||||
id,
|
||||
itemId: itemData.sourceId,
|
||||
purchaseOption: 'dig-delivery',
|
||||
});
|
||||
const res = await this._service
|
||||
.fetchDigDeliveryAvailability(itemData, itemData.quantity)
|
||||
.toPromise();
|
||||
|
||||
const availability: Availability = {
|
||||
itemId: itemData.sourceId,
|
||||
@@ -363,14 +430,24 @@ export class PurchaseOptionsStore extends ComponentStore<PurchaseOptionsState> {
|
||||
console.error('_loadDigDeliveryAvailability', error);
|
||||
}
|
||||
|
||||
this.removeFetchingAvailability({ id, itemId: itemData.sourceId, purchaseOption: 'dig-delivery' });
|
||||
this.removeFetchingAvailability({
|
||||
id,
|
||||
itemId: itemData.sourceId,
|
||||
purchaseOption: 'dig-delivery',
|
||||
});
|
||||
}
|
||||
|
||||
private async _loadB2bDeliveryAvailability(itemData: ItemData) {
|
||||
const id = uniqueId('availability_');
|
||||
try {
|
||||
this.addFetchingAvailability({ id, itemId: itemData.sourceId, purchaseOption: 'b2b-delivery' });
|
||||
const res = await this._service.fetchB2bDeliveryAvailability(itemData, itemData.quantity).toPromise();
|
||||
this.addFetchingAvailability({
|
||||
id,
|
||||
itemId: itemData.sourceId,
|
||||
purchaseOption: 'b2b-delivery',
|
||||
});
|
||||
const res = await this._service
|
||||
.fetchB2bDeliveryAvailability(itemData, itemData.quantity)
|
||||
.toPromise();
|
||||
|
||||
const availability: Availability = {
|
||||
itemId: itemData.sourceId,
|
||||
@@ -383,14 +460,24 @@ export class PurchaseOptionsStore extends ComponentStore<PurchaseOptionsState> {
|
||||
console.error('_loadB2bDeliveryAvailability', error);
|
||||
}
|
||||
|
||||
this.removeFetchingAvailability({ id, itemId: itemData.sourceId, purchaseOption: 'b2b-delivery' });
|
||||
this.removeFetchingAvailability({
|
||||
id,
|
||||
itemId: itemData.sourceId,
|
||||
purchaseOption: 'b2b-delivery',
|
||||
});
|
||||
}
|
||||
|
||||
private async _loadDownloadAvailability(itemData: ItemData) {
|
||||
const id = uniqueId('availability_');
|
||||
try {
|
||||
this.addFetchingAvailability({ id, itemId: itemData.sourceId, purchaseOption: 'download' });
|
||||
const res = await this._service.fetchDownloadAvailability(itemData).toPromise();
|
||||
this.addFetchingAvailability({
|
||||
id,
|
||||
itemId: itemData.sourceId,
|
||||
purchaseOption: 'download',
|
||||
});
|
||||
const res = await this._service
|
||||
.fetchDownloadAvailability(itemData)
|
||||
.toPromise();
|
||||
|
||||
const availability: Availability = {
|
||||
itemId: itemData.sourceId,
|
||||
@@ -403,7 +490,11 @@ export class PurchaseOptionsStore extends ComponentStore<PurchaseOptionsState> {
|
||||
console.error('_loadDownloadAvailability', error);
|
||||
}
|
||||
|
||||
this.removeFetchingAvailability({ id, itemId: itemData.sourceId, purchaseOption: 'download' });
|
||||
this.removeFetchingAvailability({
|
||||
id,
|
||||
itemId: itemData.sourceId,
|
||||
purchaseOption: 'download',
|
||||
});
|
||||
}
|
||||
|
||||
private _checkAndSetAvailability(availability: Availability) {
|
||||
@@ -411,7 +502,9 @@ export class PurchaseOptionsStore extends ComponentStore<PurchaseOptionsState> {
|
||||
const availabilities = this.availabilities;
|
||||
|
||||
const index = availabilities.findIndex(
|
||||
(a) => a.itemId === availability.itemId && a.purchaseOption === availability.purchaseOption,
|
||||
(a) =>
|
||||
a.itemId === availability.itemId &&
|
||||
a.purchaseOption === availability.purchaseOption,
|
||||
);
|
||||
|
||||
if (index > -1) {
|
||||
@@ -423,7 +516,11 @@ export class PurchaseOptionsStore extends ComponentStore<PurchaseOptionsState> {
|
||||
this.patchState({ availabilities });
|
||||
} else {
|
||||
let availabilities = this.availabilities.filter(
|
||||
(a) => !(a.itemId === availability.itemId && a.purchaseOption === availability.purchaseOption),
|
||||
(a) =>
|
||||
!(
|
||||
a.itemId === availability.itemId &&
|
||||
a.purchaseOption === availability.purchaseOption
|
||||
),
|
||||
);
|
||||
this.patchState({ availabilities });
|
||||
}
|
||||
@@ -432,7 +529,11 @@ export class PurchaseOptionsStore extends ComponentStore<PurchaseOptionsState> {
|
||||
private _addCanAddResult(canAdd: CanAdd) {
|
||||
let canAddResults = this.canAddResults;
|
||||
canAddResults = canAddResults.filter(
|
||||
(c) => !(c.itemId === canAdd.itemId && c.purchaseOption === canAdd.purchaseOption),
|
||||
(c) =>
|
||||
!(
|
||||
c.itemId === canAdd.itemId &&
|
||||
c.purchaseOption === canAdd.purchaseOption
|
||||
),
|
||||
);
|
||||
canAddResults.push(canAdd);
|
||||
this.patchState({ canAddResults });
|
||||
@@ -474,7 +575,10 @@ export class PurchaseOptionsStore extends ComponentStore<PurchaseOptionsState> {
|
||||
const b2bDeliveryAvailability = this.availabilities.find(
|
||||
(a) => a.itemId === item.id && a.purchaseOption === 'b2b-delivery',
|
||||
);
|
||||
deliveryAvailability = deliveryAvailability || digDeliveryAvailability || b2bDeliveryAvailability;
|
||||
deliveryAvailability =
|
||||
deliveryAvailability ||
|
||||
digDeliveryAvailability ||
|
||||
b2bDeliveryAvailability;
|
||||
if (deliveryAvailability) {
|
||||
payloads['Versand'].push(
|
||||
mapToItemPayload({
|
||||
@@ -487,7 +591,9 @@ export class PurchaseOptionsStore extends ComponentStore<PurchaseOptionsState> {
|
||||
}
|
||||
|
||||
// Get Abholung availability
|
||||
let pickupAvailability = this.availabilities.find((a) => a.itemId === item.id && a.purchaseOption === 'pickup');
|
||||
let pickupAvailability = this.availabilities.find(
|
||||
(a) => a.itemId === item.id && a.purchaseOption === 'pickup',
|
||||
);
|
||||
if (pickupAvailability) {
|
||||
payloads['Abholung'].push(
|
||||
mapToItemPayload({
|
||||
@@ -519,7 +625,9 @@ export class PurchaseOptionsStore extends ComponentStore<PurchaseOptionsState> {
|
||||
const itemPayloads = payloads[key];
|
||||
if (itemPayloads.length > 0) {
|
||||
try {
|
||||
const res = await this._service.fetchCanAdd(this.processId, key, itemPayloads).toPromise();
|
||||
const res = await this._service
|
||||
.fetchCanAdd(this.processId, key, itemPayloads)
|
||||
.toPromise();
|
||||
res.forEach((canAdd) => {
|
||||
const item = itemPayloads.find((i) => i.id === canAdd.id);
|
||||
this._addCanAddResult({
|
||||
@@ -549,7 +657,9 @@ export class PurchaseOptionsStore extends ComponentStore<PurchaseOptionsState> {
|
||||
if (value && !selectedItemIds.includes(itemId)) {
|
||||
this.patchState({ selectedItemIds: [...selectedItemIds, itemId] });
|
||||
} else if (!value && selectedItemIds.includes(itemId)) {
|
||||
this.patchState({ selectedItemIds: selectedItemIds.filter((id) => id !== itemId) });
|
||||
this.patchState({
|
||||
selectedItemIds: selectedItemIds.filter((id) => id !== itemId),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -561,7 +671,9 @@ export class PurchaseOptionsStore extends ComponentStore<PurchaseOptionsState> {
|
||||
|
||||
canAddItem(itemId: number): boolean {
|
||||
const purchaseOption = this.purchaseOption;
|
||||
const canAdd = this.canAddResults.find((c) => c.itemId === itemId && c.purchaseOption === purchaseOption);
|
||||
const canAdd = this.canAddResults.find(
|
||||
(c) => c.itemId === itemId && c.purchaseOption === purchaseOption,
|
||||
);
|
||||
|
||||
if (canAdd) {
|
||||
return canAdd.canAdd;
|
||||
@@ -574,20 +686,40 @@ export class PurchaseOptionsStore extends ComponentStore<PurchaseOptionsState> {
|
||||
this.patchState({ selectedItemIds: [] });
|
||||
}
|
||||
|
||||
getItemsThatHaveAnAvailabilityForPurchaseOption(purchaseOption: PurchaseOption): Item[] {
|
||||
return this.get(Selectors.getItemsThatHaveAnAvailabilityForPurchaseOption(purchaseOption));
|
||||
getItemsThatHaveAnAvailabilityForPurchaseOption(
|
||||
purchaseOption: PurchaseOption,
|
||||
): Item[] {
|
||||
return this.get(
|
||||
Selectors.getItemsThatHaveAnAvailabilityForPurchaseOption(purchaseOption),
|
||||
);
|
||||
}
|
||||
|
||||
getItemsThatHaveAnAvailabilityForPurchaseOption$(purchaseOption: PurchaseOption): Observable<Item[]> {
|
||||
return this.select(Selectors.getItemsThatHaveAnAvailabilityForPurchaseOption(purchaseOption));
|
||||
getItemsThatHaveAnAvailabilityForPurchaseOption$(
|
||||
purchaseOption: PurchaseOption,
|
||||
): Observable<Item[]> {
|
||||
return this.select(
|
||||
Selectors.getItemsThatHaveAnAvailabilityForPurchaseOption(purchaseOption),
|
||||
);
|
||||
}
|
||||
|
||||
getItemsThatHaveAnAvailabilityAndCanAddForPurchaseOption(purchaseOption: PurchaseOption): Item[] {
|
||||
return this.get(Selectors.getItemsThatHaveAnAvailabilityAndCanAddForPurchaseOption(purchaseOption));
|
||||
getItemsThatHaveAnAvailabilityAndCanAddForPurchaseOption(
|
||||
purchaseOption: PurchaseOption,
|
||||
): Item[] {
|
||||
return this.get(
|
||||
Selectors.getItemsThatHaveAnAvailabilityAndCanAddForPurchaseOption(
|
||||
purchaseOption,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
getItemsThatHaveAnAvailabilityAndCanAddForPurchaseOption$(purchaseOption: PurchaseOption): Observable<Item[]> {
|
||||
return this.select(Selectors.getItemsThatHaveAnAvailabilityAndCanAddForPurchaseOption(purchaseOption));
|
||||
getItemsThatHaveAnAvailabilityAndCanAddForPurchaseOption$(
|
||||
purchaseOption: PurchaseOption,
|
||||
): Observable<Item[]> {
|
||||
return this.select(
|
||||
Selectors.getItemsThatHaveAnAvailabilityAndCanAddForPurchaseOption(
|
||||
purchaseOption,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
getAvailabilitiesForItem(itemId: number): Availability[] {
|
||||
@@ -679,15 +811,21 @@ export class PurchaseOptionsStore extends ComponentStore<PurchaseOptionsState> {
|
||||
}
|
||||
|
||||
getPrice$(itemId: number) {
|
||||
return this.purchaseOption$.pipe(switchMap((po) => this.getPriceForPurchaseOption$(itemId, po)));
|
||||
return this.purchaseOption$.pipe(
|
||||
switchMap((po) => this.getPriceForPurchaseOption$(itemId, po)),
|
||||
);
|
||||
}
|
||||
|
||||
getPriceForPurchaseOption(itemId: number, purchaseOption: PurchaseOption) {
|
||||
return this.get(Selectors.getPriceForPurchaseOption(itemId, purchaseOption));
|
||||
return this.get(
|
||||
Selectors.getPriceForPurchaseOption(itemId, purchaseOption),
|
||||
);
|
||||
}
|
||||
|
||||
getPriceForPurchaseOption$(itemId: number, purchaseOption: PurchaseOption) {
|
||||
return this.select(Selectors.getPriceForPurchaseOption(itemId, purchaseOption));
|
||||
return this.select(
|
||||
Selectors.getPriceForPurchaseOption(itemId, purchaseOption),
|
||||
);
|
||||
}
|
||||
|
||||
getCanEditPrice(itemId: number) {
|
||||
@@ -748,27 +886,51 @@ export class PurchaseOptionsStore extends ComponentStore<PurchaseOptionsState> {
|
||||
}
|
||||
|
||||
getCanAddResultForItemAndCurrentPurchaseOption(itemId: number) {
|
||||
return this.get(Selectors.getCanAddResultForItemAndCurrentPurchaseOption(itemId));
|
||||
return this.get(
|
||||
Selectors.getCanAddResultForItemAndCurrentPurchaseOption(itemId),
|
||||
);
|
||||
}
|
||||
|
||||
getCanAddResultForItemAndCurrentPurchaseOption$(itemId: number) {
|
||||
return this.select(Selectors.getCanAddResultForItemAndCurrentPurchaseOption(itemId));
|
||||
return this.select(
|
||||
Selectors.getCanAddResultForItemAndCurrentPurchaseOption(itemId),
|
||||
);
|
||||
}
|
||||
|
||||
getAvailabilityWithPurchaseOption(itemId: number, purchaseOption: PurchaseOption) {
|
||||
return this.get(Selectors.getAvailabilityWithPurchaseOption(itemId, purchaseOption));
|
||||
getAvailabilityWithPurchaseOption(
|
||||
itemId: number,
|
||||
purchaseOption: PurchaseOption,
|
||||
) {
|
||||
return this.get(
|
||||
Selectors.getAvailabilityWithPurchaseOption(itemId, purchaseOption),
|
||||
);
|
||||
}
|
||||
|
||||
getAvailabilityWithPurchaseOption$(itemId: number, purchaseOption: PurchaseOption) {
|
||||
return this.select(Selectors.getAvailabilityWithPurchaseOption(itemId, purchaseOption));
|
||||
getAvailabilityWithPurchaseOption$(
|
||||
itemId: number,
|
||||
purchaseOption: PurchaseOption,
|
||||
) {
|
||||
return this.select(
|
||||
Selectors.getAvailabilityWithPurchaseOption(itemId, purchaseOption),
|
||||
);
|
||||
}
|
||||
|
||||
getCanAddResultWithPurchaseOption(itemId: number, purchaseOption: PurchaseOption) {
|
||||
return this.get(Selectors.getCanAddResultWithPurchaseOption(itemId, purchaseOption));
|
||||
getCanAddResultWithPurchaseOption(
|
||||
itemId: number,
|
||||
purchaseOption: PurchaseOption,
|
||||
) {
|
||||
return this.get(
|
||||
Selectors.getCanAddResultWithPurchaseOption(itemId, purchaseOption),
|
||||
);
|
||||
}
|
||||
|
||||
getCanAddResultWithPurchaseOption$(itemId: number, purchaseOption: PurchaseOption) {
|
||||
return this.select(Selectors.getCanAddResultWithPurchaseOption(itemId, purchaseOption));
|
||||
getCanAddResultWithPurchaseOption$(
|
||||
itemId: number,
|
||||
purchaseOption: PurchaseOption,
|
||||
) {
|
||||
return this.select(
|
||||
Selectors.getCanAddResultWithPurchaseOption(itemId, purchaseOption),
|
||||
);
|
||||
}
|
||||
|
||||
addItemsToShoppingCart() {
|
||||
@@ -777,10 +939,16 @@ export class PurchaseOptionsStore extends ComponentStore<PurchaseOptionsState> {
|
||||
}
|
||||
}
|
||||
|
||||
getAddToShoppingCartDTOForItem(itemId: number, purchaseOption: PurchaseOption): AddToShoppingCartDTO {
|
||||
getAddToShoppingCartDTOForItem(
|
||||
itemId: number,
|
||||
purchaseOption: PurchaseOption,
|
||||
): AddToShoppingCartDTO {
|
||||
const item = this.items.find((i) => i.id === itemId);
|
||||
const price = this.getPriceForPurchaseOption(itemId, this.purchaseOption);
|
||||
const availability = this.getAvailabilityWithPurchaseOption(itemId, purchaseOption);
|
||||
const availability = this.getAvailabilityWithPurchaseOption(
|
||||
itemId,
|
||||
purchaseOption,
|
||||
);
|
||||
|
||||
if (!isItemDTO(item, this.type)) {
|
||||
throw new Error('Invalid item');
|
||||
@@ -802,8 +970,12 @@ export class PurchaseOptionsStore extends ComponentStore<PurchaseOptionsState> {
|
||||
availability: { ...availability.data, price },
|
||||
destination,
|
||||
itemType: item.type,
|
||||
product: { ...item.product, catalogProductNumber: item?.product?.catalogProductNumber ?? String(item.id) },
|
||||
promotion: { points: item.promoPoints },
|
||||
product: {
|
||||
...item.product,
|
||||
catalogProductNumber:
|
||||
item?.product?.catalogProductNumber ?? String(item.id),
|
||||
},
|
||||
promotion: { value: item.promoPoints },
|
||||
// retailPrice: {
|
||||
// value: price.value.value,
|
||||
// currency: price.value.currency,
|
||||
@@ -813,10 +985,16 @@ export class PurchaseOptionsStore extends ComponentStore<PurchaseOptionsState> {
|
||||
};
|
||||
}
|
||||
|
||||
getUpdateShoppingCartItemDTOForItem(itemId: number, purchaseOption: PurchaseOption): UpdateShoppingCartItemDTO {
|
||||
getUpdateShoppingCartItemDTOForItem(
|
||||
itemId: number,
|
||||
purchaseOption: PurchaseOption,
|
||||
): UpdateShoppingCartItemDTO {
|
||||
const item = this.items.find((i) => i.id === itemId);
|
||||
const price = this.getPriceForPurchaseOption(itemId, this.purchaseOption);
|
||||
const availability = this.getAvailabilityWithPurchaseOption(itemId, purchaseOption);
|
||||
const availability = this.getAvailabilityWithPurchaseOption(
|
||||
itemId,
|
||||
purchaseOption,
|
||||
);
|
||||
|
||||
if (!isShoppingCartItemDTO(item, this.type)) {
|
||||
throw new Error('Invalid item');
|
||||
@@ -856,7 +1034,9 @@ export class PurchaseOptionsStore extends ComponentStore<PurchaseOptionsState> {
|
||||
const payloads = this.selectedItemIds.map((itemId) =>
|
||||
this.getAddToShoppingCartDTOForItem(itemId, purchaseOption),
|
||||
);
|
||||
await this._service.addItemToShoppingCart(this.processId, payloads).toPromise();
|
||||
await this._service
|
||||
.addItemToShoppingCart(this.processId, payloads)
|
||||
.toPromise();
|
||||
} else if (type === 'update') {
|
||||
const payloads = this.selectedItemIds.map((itemId) =>
|
||||
this.getUpdateShoppingCartItemDTOForItem(itemId, purchaseOption),
|
||||
@@ -864,8 +1044,13 @@ export class PurchaseOptionsStore extends ComponentStore<PurchaseOptionsState> {
|
||||
|
||||
for (const itemId of this.selectedItemIds) {
|
||||
const item = this.items.find((i) => i.id === itemId);
|
||||
const payload = this.getUpdateShoppingCartItemDTOForItem(itemId, purchaseOption);
|
||||
await this._service.updateItemInShoppingCart(this.processId, item.id, payload).toPromise();
|
||||
const payload = this.getUpdateShoppingCartItemDTOForItem(
|
||||
itemId,
|
||||
purchaseOption,
|
||||
);
|
||||
await this._service
|
||||
.updateItemInShoppingCart(this.processId, item.id, payload)
|
||||
.toPromise();
|
||||
}
|
||||
} else {
|
||||
throw new Error('Invalid type');
|
||||
|
||||
@@ -22,12 +22,21 @@ import { UiErrorModalComponent, UiModalService } from '@ui/modal';
|
||||
import { CacheService } from '@core/cache';
|
||||
import { isEqual } from 'lodash';
|
||||
import { BehaviorSubject, combineLatest, Subscription } from 'rxjs';
|
||||
import { debounceTime, first, map, switchMap, withLatestFrom } from 'rxjs/operators';
|
||||
import {
|
||||
debounceTime,
|
||||
first,
|
||||
map,
|
||||
switchMap,
|
||||
withLatestFrom,
|
||||
} from 'rxjs/operators';
|
||||
import { ArticleSearchService } from '../article-search.store';
|
||||
import { AddedToCartModalComponent } from './added-to-cart-modal/added-to-cart-modal.component';
|
||||
import { SearchResultItemComponent } from './search-result-item.component';
|
||||
import { ProductCatalogNavigationService } from '@shared/services/navigation';
|
||||
import { Filter, FilterInputGroupMainComponent } from '@shared/components/filter';
|
||||
import {
|
||||
Filter,
|
||||
FilterInputGroupMainComponent,
|
||||
} from '@shared/components/filter';
|
||||
import { DomainAvailabilityService, ItemData } from '@domain/availability';
|
||||
import { asapScheduler } from 'rxjs';
|
||||
import { ShellService } from '@shared/shell';
|
||||
@@ -39,8 +48,11 @@ import { ShellService } from '@shared/shell';
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: false,
|
||||
})
|
||||
export class ArticleSearchResultsComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||
@ViewChildren(SearchResultItemComponent) listItems: QueryList<SearchResultItemComponent>;
|
||||
export class ArticleSearchResultsComponent
|
||||
implements OnInit, OnDestroy, AfterViewInit
|
||||
{
|
||||
@ViewChildren(SearchResultItemComponent)
|
||||
listItems: QueryList<SearchResultItemComponent>;
|
||||
@ViewChild(CdkVirtualScrollViewport, { static: false })
|
||||
scrollContainer: CdkVirtualScrollViewport;
|
||||
|
||||
@@ -59,7 +71,9 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy, AfterVi
|
||||
|
||||
selectedItems$ = combineLatest([this.results$, this.selectedItemIds$]).pipe(
|
||||
map(([items, selectedItemIds]) => {
|
||||
return items?.filter((item) => selectedItemIds?.find((selectedItemId) => item.id === selectedItemId));
|
||||
return items?.filter((item) =>
|
||||
selectedItemIds?.find((selectedItemId) => item.id === selectedItemId),
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -81,7 +95,10 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy, AfterVi
|
||||
return this._environment.matchDesktopLarge();
|
||||
}
|
||||
|
||||
hasFilter$ = combineLatest([this.searchService.filter$, this.searchService.defaultSettings$]).pipe(
|
||||
hasFilter$ = combineLatest([
|
||||
this.searchService.filter$,
|
||||
this.searchService.defaultSettings$,
|
||||
]).pipe(
|
||||
map(([filter, defaultFilter]) => {
|
||||
const filterQueryParams = filter?.getQueryParams();
|
||||
return !isEqual(
|
||||
@@ -100,11 +117,15 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy, AfterVi
|
||||
}
|
||||
|
||||
get filterQueryParams() {
|
||||
return this.cleanupQueryParams(this.searchService?.filter?.getQueryParams());
|
||||
return this.cleanupQueryParams(
|
||||
this.searchService?.filter?.getQueryParams(),
|
||||
);
|
||||
}
|
||||
|
||||
get primaryOutletActive$() {
|
||||
return this._environment.matchDesktop$.pipe(map((matches) => matches && this.route.outlet === 'primary'));
|
||||
return this._environment.matchDesktop$.pipe(
|
||||
map((matches) => matches && this.route.outlet === 'primary'),
|
||||
);
|
||||
}
|
||||
|
||||
private readonly SCROLL_INDEX_TOKEN = 'CATALOG_RESULTS_LIST_SCROLL_INDEX';
|
||||
@@ -129,28 +150,42 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy, AfterVi
|
||||
|
||||
ngOnInit() {
|
||||
this.subscriptions.add(
|
||||
combineLatest([this.application.activatedProcessId$, this.route.queryParams])
|
||||
combineLatest([
|
||||
this.application.activatedProcessId$,
|
||||
this.route.queryParams,
|
||||
])
|
||||
.pipe(
|
||||
debounceTime(0),
|
||||
switchMap(([processId, queryParams]) =>
|
||||
this.application
|
||||
.getSelectedBranch$(processId)
|
||||
.pipe(map((selectedBranch) => ({ processId, queryParams, selectedBranch }))),
|
||||
this.application.getSelectedBranch$(processId).pipe(
|
||||
map((selectedBranch) => ({
|
||||
processId,
|
||||
queryParams,
|
||||
selectedBranch,
|
||||
})),
|
||||
),
|
||||
),
|
||||
)
|
||||
.subscribe(async ({ processId, queryParams, selectedBranch }) => {
|
||||
const processChanged = processId !== this.searchService.processId;
|
||||
|
||||
const branchChanged = selectedBranch?.id !== this.searchService?.selectedBranch?.id;
|
||||
const branchChanged =
|
||||
selectedBranch?.id !== this.searchService?.selectedBranch?.id;
|
||||
|
||||
if (processChanged) {
|
||||
if (!!this.searchService.processId && this.searchService.filter instanceof Filter) {
|
||||
if (
|
||||
!!this.searchService.processId &&
|
||||
this.searchService.filter instanceof Filter
|
||||
) {
|
||||
this.cacheCurrentData(
|
||||
this.searchService.processId,
|
||||
this.searchService.filter.getQueryParams(),
|
||||
this.searchService?.selectedBranch?.id,
|
||||
);
|
||||
this.updateBreadcrumbs(this.searchService.processId, this.searchService.filter.getQueryParams());
|
||||
this.updateBreadcrumbs(
|
||||
this.searchService.processId,
|
||||
this.searchService.filter.getQueryParams(),
|
||||
);
|
||||
}
|
||||
this.searchService.setProcess(processId);
|
||||
}
|
||||
@@ -169,9 +204,20 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy, AfterVi
|
||||
this.scrollToItem(await this._getScrollIndexFromCache());
|
||||
}
|
||||
|
||||
if (!isEqual(cleanQueryParams, this.cleanupQueryParams(this.searchService.filter.getQueryParams()))) {
|
||||
if (
|
||||
!isEqual(
|
||||
cleanQueryParams,
|
||||
this.cleanupQueryParams(
|
||||
this.searchService.filter.getQueryParams(),
|
||||
),
|
||||
)
|
||||
) {
|
||||
await this.searchService.setDefaultFilter(queryParams);
|
||||
const data = await this.getCachedData(processId, queryParams, selectedBranch?.id);
|
||||
const data = await this.getCachedData(
|
||||
processId,
|
||||
queryParams,
|
||||
selectedBranch?.id,
|
||||
);
|
||||
|
||||
if (data.items?.length > 0) {
|
||||
this.searchService.setItems(data.items);
|
||||
@@ -179,21 +225,29 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy, AfterVi
|
||||
}
|
||||
if (
|
||||
data.items?.length === 0 &&
|
||||
this.route?.parent?.children?.find((childRoute) => childRoute?.outlet === 'side')?.snapshot?.routeConfig
|
||||
?.path !== 'filter'
|
||||
this.route?.parent?.children?.find(
|
||||
(childRoute) => childRoute?.outlet === 'side',
|
||||
)?.snapshot?.routeConfig?.path !== 'filter'
|
||||
) {
|
||||
this.search({ clear: true });
|
||||
} else {
|
||||
const selectedItemIds: Array<string> = queryParams?.selected_item_ids?.split(',') ?? [];
|
||||
const selectedItemIds: Array<string> =
|
||||
queryParams?.selected_item_ids?.split(',') ?? [];
|
||||
for (const id of selectedItemIds) {
|
||||
if (id) {
|
||||
this.searchService.setSelected({ selected: true, itemId: Number(id) });
|
||||
this.searchService.setSelected({
|
||||
selected: true,
|
||||
itemId: Number(id),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const process = await this.application.getProcessById$(processId).pipe(first()).toPromise();
|
||||
const process = await this.application
|
||||
.getProcessById$(processId)
|
||||
.pipe(first())
|
||||
.toPromise();
|
||||
if (process) {
|
||||
await this.updateBreadcrumbs(processId, queryParams);
|
||||
await this.createBreadcrumb(processId, queryParams);
|
||||
@@ -240,7 +294,10 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy, AfterVi
|
||||
})
|
||||
.navigate();
|
||||
}
|
||||
} else if (searchCompleted?.clear || this.route.outlet === 'primary') {
|
||||
} else if (
|
||||
searchCompleted?.clear ||
|
||||
this.route.outlet === 'primary'
|
||||
) {
|
||||
const ean = this.route?.snapshot?.params?.ean;
|
||||
|
||||
if (ean) {
|
||||
@@ -253,7 +310,9 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy, AfterVi
|
||||
.navigate();
|
||||
} else {
|
||||
await this._navigationService
|
||||
.getArticleSearchResultsPath(processId, { queryParams: params })
|
||||
.getArticleSearchResultsPath(processId, {
|
||||
queryParams: params,
|
||||
})
|
||||
.navigate();
|
||||
}
|
||||
}
|
||||
@@ -266,7 +325,9 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy, AfterVi
|
||||
this.searchService.searchStarted.subscribe(async (options) => {
|
||||
if (!options?.clear) {
|
||||
const queryParams = {
|
||||
...this.cleanupQueryParams(this.searchService.filter.getQueryParams()),
|
||||
...this.cleanupQueryParams(
|
||||
this.searchService.filter.getQueryParams(),
|
||||
),
|
||||
main_qs: this.sharedFilterInputGroupMain?.uiInput?.value,
|
||||
};
|
||||
|
||||
@@ -281,11 +342,19 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy, AfterVi
|
||||
}
|
||||
|
||||
private _addScrollIndexToCache(index: number): void {
|
||||
this.cache.set<number>({ processId: this.getProcessId(), token: this.SCROLL_INDEX_TOKEN }, index);
|
||||
this.cache.set<number>(
|
||||
{ processId: this.getProcessId(), token: this.SCROLL_INDEX_TOKEN },
|
||||
index,
|
||||
);
|
||||
}
|
||||
|
||||
private async _getScrollIndexFromCache(): Promise<number> {
|
||||
return (await this.cache.get<number>({ processId: this.getProcessId(), token: this.SCROLL_INDEX_TOKEN })) ?? 0;
|
||||
return (
|
||||
(await this.cache.get<number>({
|
||||
processId: this.getProcessId(),
|
||||
token: this.SCROLL_INDEX_TOKEN,
|
||||
})) ?? 0
|
||||
);
|
||||
}
|
||||
|
||||
async scrollToItem(i?: number) {
|
||||
@@ -303,12 +372,14 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy, AfterVi
|
||||
}
|
||||
|
||||
scrolledIndexChange(index: number) {
|
||||
const completeListFetched = this.searchService.items.length === this.searchService.hits;
|
||||
const completeListFetched =
|
||||
this.searchService.items.length === this.searchService.hits;
|
||||
|
||||
if (
|
||||
index &&
|
||||
!completeListFetched &&
|
||||
this.searchService.items.length <= this.scrollContainer?.getRenderedRange()?.end
|
||||
this.searchService.items.length <=
|
||||
this.scrollContainer?.getRenderedRange()?.end
|
||||
) {
|
||||
this.search({ clear: false });
|
||||
}
|
||||
@@ -326,7 +397,10 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy, AfterVi
|
||||
this.searchService.filter.getQueryParams(),
|
||||
this.searchService?.selectedBranch?.id,
|
||||
);
|
||||
await this.updateBreadcrumbs(this.searchService.processId, this.searchService.filter.getQueryParams());
|
||||
await this.updateBreadcrumbs(
|
||||
this.searchService.processId,
|
||||
this.searchService.filter.getQueryParams(),
|
||||
);
|
||||
|
||||
this.unselectAll();
|
||||
}
|
||||
@@ -345,7 +419,15 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy, AfterVi
|
||||
return clean;
|
||||
}
|
||||
|
||||
search({ filter, clear = false, orderBy = false }: { filter?: Filter; clear?: boolean; orderBy?: boolean }) {
|
||||
search({
|
||||
filter,
|
||||
clear = false,
|
||||
orderBy = false,
|
||||
}: {
|
||||
filter?: Filter;
|
||||
clear?: boolean;
|
||||
orderBy?: boolean;
|
||||
}) {
|
||||
if (filter) {
|
||||
this.sharedFilterInputGroupMain.cancelAutocomplete();
|
||||
}
|
||||
@@ -354,19 +436,28 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy, AfterVi
|
||||
}
|
||||
|
||||
getDetailsPath(itemId: number) {
|
||||
return this._navigationService.getArticleDetailsPath({ processId: this.application.activatedProcessId, itemId })
|
||||
.path;
|
||||
return this._navigationService.getArticleDetailsPath({
|
||||
processId: this.application.activatedProcessId,
|
||||
itemId,
|
||||
}).path;
|
||||
}
|
||||
|
||||
async updateBreadcrumbs(
|
||||
processId: number = this.searchService.processId,
|
||||
queryParams: Record<string, string> = this.searchService.filter?.getQueryParams(),
|
||||
queryParams: Record<
|
||||
string,
|
||||
string
|
||||
> = this.searchService.filter?.getQueryParams(),
|
||||
) {
|
||||
const selected_item_ids = this.searchService?.selectedItemIds?.toString();
|
||||
|
||||
if (queryParams) {
|
||||
const crumbs = await this.breadcrumb
|
||||
.getBreadcrumbsByKeyAndTags$(processId, ['catalog', 'filter', 'results'])
|
||||
.getBreadcrumbsByKeyAndTags$(processId, [
|
||||
'catalog',
|
||||
'filter',
|
||||
'results',
|
||||
])
|
||||
.pipe(first())
|
||||
.toPromise();
|
||||
|
||||
@@ -382,13 +473,18 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy, AfterVi
|
||||
}
|
||||
}
|
||||
|
||||
async createBreadcrumb(processId: number, queryParams: Record<string, string>) {
|
||||
async createBreadcrumb(
|
||||
processId: number,
|
||||
queryParams: Record<string, string>,
|
||||
) {
|
||||
if (queryParams) {
|
||||
const name = queryParams.main_qs ? queryParams.main_qs : 'Alle Artikel';
|
||||
await this.breadcrumb.addBreadcrumbIfNotExists({
|
||||
key: processId,
|
||||
name,
|
||||
path: this._navigationService.getArticleSearchResultsPath(processId, { queryParams }).path,
|
||||
path: this._navigationService.getArticleSearchResultsPath(processId, {
|
||||
queryParams,
|
||||
}).path,
|
||||
params: queryParams,
|
||||
section: 'customer',
|
||||
tags: ['catalog', 'filter', 'results'],
|
||||
@@ -405,8 +501,16 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy, AfterVi
|
||||
crumbs?.forEach((crumb) => this.breadcrumb.removeBreadcrumb(crumb.id));
|
||||
}
|
||||
|
||||
cacheCurrentData(processId: number, params: Record<string, string> = {}, branchId: number) {
|
||||
const qparams = this.cleanupQueryParams({ ...params, processId: String(processId), branchId: String(branchId) });
|
||||
cacheCurrentData(
|
||||
processId: number,
|
||||
params: Record<string, string> = {},
|
||||
branchId: number,
|
||||
) {
|
||||
const qparams = this.cleanupQueryParams({
|
||||
...params,
|
||||
processId: String(processId),
|
||||
branchId: String(branchId),
|
||||
});
|
||||
|
||||
this.cache.set(qparams, {
|
||||
items: this.searchService.items,
|
||||
@@ -414,8 +518,16 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy, AfterVi
|
||||
});
|
||||
}
|
||||
|
||||
async getCachedData(processId: number, params: Record<string, string> = {}, branchId: number) {
|
||||
const qparams = this.cleanupQueryParams({ ...params, processId: String(processId), branchId: String(branchId) });
|
||||
async getCachedData(
|
||||
processId: number,
|
||||
params: Record<string, string> = {},
|
||||
branchId: number,
|
||||
) {
|
||||
const qparams = this.cleanupQueryParams({
|
||||
...params,
|
||||
processId: String(processId),
|
||||
branchId: String(branchId),
|
||||
});
|
||||
const cacheData = await this.cache.get<{
|
||||
items: ItemDTO[];
|
||||
hits: number;
|
||||
@@ -452,7 +564,12 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy, AfterVi
|
||||
}
|
||||
|
||||
unselectAll() {
|
||||
this.listItems.forEach((listItem) => this.searchService.setSelected({ selected: false, itemId: listItem.item.id }));
|
||||
this.listItems.forEach((listItem) =>
|
||||
this.searchService.setSelected({
|
||||
selected: false,
|
||||
itemId: listItem.item.id,
|
||||
}),
|
||||
);
|
||||
this.searchService.patchState({ selectedItemIds: [] });
|
||||
}
|
||||
|
||||
@@ -474,39 +591,46 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy, AfterVi
|
||||
availability: {
|
||||
availabilityType: item?.catalogAvailability?.status,
|
||||
price: item?.catalogAvailability?.price,
|
||||
supplierProductNumber: item?.ids?.dig ? String(item?.ids?.dig) : item?.product?.supplierProductNumber,
|
||||
supplierProductNumber: item?.ids?.dig
|
||||
? String(item?.ids?.dig)
|
||||
: item?.product?.supplierProductNumber,
|
||||
},
|
||||
product: {
|
||||
catalogProductNumber: String(item?.id),
|
||||
...item?.product,
|
||||
},
|
||||
itemType: item?.type,
|
||||
promotion: { points: item?.promoPoints },
|
||||
promotion: { value: item?.promoPoints },
|
||||
};
|
||||
}
|
||||
|
||||
async addItemsToCart(item?: ItemDTO) {
|
||||
const selectedItems = !item ? await this.selectedItems$.pipe(first()).toPromise() : [item];
|
||||
const selectedItems = !item
|
||||
? await this.selectedItems$.pipe(first()).toPromise()
|
||||
: [item];
|
||||
const items: AddToShoppingCartDTO[] = [];
|
||||
|
||||
const canAddItemsPayload = [];
|
||||
|
||||
for (const item of selectedItems) {
|
||||
const isDownload = item?.product?.format === 'EB' || item?.product?.format === 'DL';
|
||||
const isDownload =
|
||||
item?.product?.format === 'EB' || item?.product?.format === 'DL';
|
||||
const price = item?.catalogAvailability?.price;
|
||||
const shoppingCartItem: AddToShoppingCartDTO = {
|
||||
quantity: 1,
|
||||
availability: {
|
||||
availabilityType: item?.catalogAvailability?.status,
|
||||
price,
|
||||
supplierProductNumber: item?.ids?.dig ? String(item.ids?.dig) : item?.product?.supplierProductNumber,
|
||||
supplierProductNumber: item?.ids?.dig
|
||||
? String(item.ids?.dig)
|
||||
: item?.product?.supplierProductNumber,
|
||||
},
|
||||
product: {
|
||||
catalogProductNumber: String(item?.id),
|
||||
...item?.product,
|
||||
},
|
||||
itemType: item.type,
|
||||
promotion: { points: item?.promoPoints },
|
||||
promotion: { value: item?.promoPoints },
|
||||
};
|
||||
|
||||
if (isDownload) {
|
||||
@@ -519,9 +643,14 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy, AfterVi
|
||||
.getDownloadAvailability({ item: downloadItem })
|
||||
.pipe(first())
|
||||
.toPromise();
|
||||
shoppingCartItem.destination = { data: { target: 16, logistician: downloadAvailability?.logistician } };
|
||||
shoppingCartItem.destination = {
|
||||
data: { target: 16, logistician: downloadAvailability?.logistician },
|
||||
};
|
||||
if (downloadAvailability) {
|
||||
shoppingCartItem.availability = { ...shoppingCartItem.availability, ...downloadAvailability };
|
||||
shoppingCartItem.availability = {
|
||||
...shoppingCartItem.availability,
|
||||
...downloadAvailability,
|
||||
};
|
||||
}
|
||||
canAddItemsPayload.push({
|
||||
availabilities: [{ ...item.catalogAvailability, format: 'DL' }],
|
||||
@@ -546,7 +675,10 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy, AfterVi
|
||||
if (response) {
|
||||
const cantAdd = (response as any)?.filter((r) => r.status >= 2);
|
||||
if (cantAdd?.length > 0) {
|
||||
this.openModal({ itemLength: cantAdd.length, canAddMessage: cantAdd[0].message });
|
||||
this.openModal({
|
||||
itemLength: cantAdd.length,
|
||||
canAddMessage: cantAdd[0].message,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -571,7 +703,15 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy, AfterVi
|
||||
}
|
||||
}
|
||||
|
||||
openModal({ itemLength, canAddMessage, error }: { itemLength: number; canAddMessage?: string; error?: Error }) {
|
||||
openModal({
|
||||
itemLength,
|
||||
canAddMessage,
|
||||
error,
|
||||
}: {
|
||||
itemLength: number;
|
||||
canAddMessage?: string;
|
||||
error?: Error;
|
||||
}) {
|
||||
const modal = this._uiModal.open({
|
||||
title:
|
||||
!error && !canAddMessage
|
||||
|
||||
@@ -114,15 +114,21 @@ export class CheckoutDummyStore extends ComponentStore<CheckoutDummyState> {
|
||||
|
||||
readonly processId$ = this._application.activatedProcessId$;
|
||||
|
||||
readonly customer$ = this.processId$.pipe(switchMap((processId) => this._checkoutService.getBuyer({ processId })));
|
||||
readonly customer$ = this.processId$.pipe(
|
||||
switchMap((processId) => this._checkoutService.getBuyer({ processId })),
|
||||
);
|
||||
|
||||
readonly customerFeatures$ = this.processId$.pipe(
|
||||
switchMap((processId) => this._checkoutService.getCustomerFeatures({ processId })),
|
||||
switchMap((processId) =>
|
||||
this._checkoutService.getCustomerFeatures({ processId }),
|
||||
),
|
||||
);
|
||||
|
||||
readonly customerFilter$ = this.customerFeatures$.pipe(
|
||||
withLatestFrom(this.processId$),
|
||||
switchMap(([customerFeatures, processId]) => this._checkoutService.canSetCustomer({ processId, customerFeatures })),
|
||||
switchMap(([customerFeatures, processId]) =>
|
||||
this._checkoutService.canSetCustomer({ processId, customerFeatures }),
|
||||
),
|
||||
map((res) => res.filter),
|
||||
);
|
||||
|
||||
@@ -169,7 +175,11 @@ export class CheckoutDummyStore extends ComponentStore<CheckoutDummyState> {
|
||||
tapResponse(
|
||||
(res) => {
|
||||
const item = res.result[0];
|
||||
if (!!item && item?.product?.format !== 'EB' && item?.product?.format !== 'DL') {
|
||||
if (
|
||||
!!item &&
|
||||
item?.product?.format !== 'EB' &&
|
||||
item?.product?.format !== 'DL'
|
||||
) {
|
||||
this.patchState({
|
||||
item: res.result[0],
|
||||
message: '',
|
||||
@@ -229,12 +239,22 @@ export class CheckoutDummyStore extends ComponentStore<CheckoutDummyState> {
|
||||
updateCart = this.effect((cb$: Observable<Function>) =>
|
||||
cb$.pipe(
|
||||
tap((_) => this.patchState({ fetching: true })),
|
||||
withLatestFrom(this.processId$, this.addToCartItem$, this.shoppingCartItemId$),
|
||||
withLatestFrom(
|
||||
this.processId$,
|
||||
this.addToCartItem$,
|
||||
this.shoppingCartItemId$,
|
||||
),
|
||||
switchMap(([cb, processId, newItem, shoppingCartItemId]) => {
|
||||
const availability = newItem.availability;
|
||||
const quantity = newItem.quantity;
|
||||
const destination = newItem.destination;
|
||||
return this.updateCartRequest({ processId, shoppingCartItemId, availability, quantity, destination }).pipe(
|
||||
return this.updateCartRequest({
|
||||
processId,
|
||||
shoppingCartItemId,
|
||||
availability,
|
||||
quantity,
|
||||
destination,
|
||||
}).pipe(
|
||||
tapResponse(
|
||||
(res) => {
|
||||
this.patchState({
|
||||
@@ -270,7 +290,10 @@ export class CheckoutDummyStore extends ComponentStore<CheckoutDummyState> {
|
||||
}
|
||||
|
||||
addToCartRequest(processId: number, newItem: AddToShoppingCartDTO) {
|
||||
return this._checkoutService.addItemToShoppingCart({ processId, items: [newItem] });
|
||||
return this._checkoutService.addItemToShoppingCart({
|
||||
processId,
|
||||
items: [newItem],
|
||||
});
|
||||
}
|
||||
|
||||
updateCartRequest({
|
||||
@@ -297,7 +320,11 @@ export class CheckoutDummyStore extends ComponentStore<CheckoutDummyState> {
|
||||
});
|
||||
}
|
||||
|
||||
async createAddToCartItem(control: UntypedFormGroup, branch: BranchDTO, update?: boolean) {
|
||||
async createAddToCartItem(
|
||||
control: UntypedFormGroup,
|
||||
branch: BranchDTO,
|
||||
update?: boolean,
|
||||
) {
|
||||
let item: ItemDTO;
|
||||
const quantity = Number(control.get('quantity').value);
|
||||
const price = this._createPriceDTO(control);
|
||||
@@ -305,7 +332,11 @@ export class CheckoutDummyStore extends ComponentStore<CheckoutDummyState> {
|
||||
// Check if item exists or ean inside the control changed in the meantime
|
||||
if (!!this.item && this.item.product.ean === control.get('ean').value) {
|
||||
item = this.item;
|
||||
promoPoints = await this._getPromoPoints({ itemId: item.id, quantity, price: price.value.value });
|
||||
promoPoints = await this._getPromoPoints({
|
||||
itemId: item.id,
|
||||
quantity,
|
||||
price: price.value.value,
|
||||
});
|
||||
} else {
|
||||
item = undefined;
|
||||
}
|
||||
@@ -316,21 +347,33 @@ export class CheckoutDummyStore extends ComponentStore<CheckoutDummyState> {
|
||||
quantity,
|
||||
availability,
|
||||
product,
|
||||
promotion: item ? { points: promoPoints } : undefined,
|
||||
promotion: item ? { value: promoPoints } : undefined,
|
||||
destination: {
|
||||
data: { target: 1, targetBranch: { id: branch?.id } },
|
||||
},
|
||||
itemType: this.item?.type ?? this.get((s) => s.shoppingCartItem)?.itemType,
|
||||
itemType:
|
||||
this.item?.type ?? this.get((s) => s.shoppingCartItem)?.itemType,
|
||||
};
|
||||
|
||||
if (update) {
|
||||
const existingItem = this.get((s) => s.shoppingCartItem);
|
||||
this.patchState({ addToCartItem: newItem, shoppingCartItemId: existingItem?.id });
|
||||
this.patchState({
|
||||
addToCartItem: newItem,
|
||||
shoppingCartItemId: existingItem?.id,
|
||||
});
|
||||
}
|
||||
this.patchState({ addToCartItem: newItem });
|
||||
}
|
||||
|
||||
private async _getPromoPoints({ itemId, quantity, price }: { itemId: number; quantity: number; price: number }) {
|
||||
private async _getPromoPoints({
|
||||
itemId,
|
||||
quantity,
|
||||
price,
|
||||
}: {
|
||||
itemId: number;
|
||||
quantity: number;
|
||||
price: number;
|
||||
}) {
|
||||
let points: number;
|
||||
try {
|
||||
points = await this._catalogService
|
||||
@@ -371,7 +414,13 @@ export class CheckoutDummyStore extends ComponentStore<CheckoutDummyState> {
|
||||
};
|
||||
}
|
||||
|
||||
private _createAvailabilityDTO({ price, control }: { price: PriceDTO; control: UntypedFormGroup }): AvailabilityDTO {
|
||||
private _createAvailabilityDTO({
|
||||
price,
|
||||
control,
|
||||
}: {
|
||||
price: PriceDTO;
|
||||
control: UntypedFormGroup;
|
||||
}): AvailabilityDTO {
|
||||
return {
|
||||
availabilityType: 1024,
|
||||
supplier: {
|
||||
@@ -383,7 +432,13 @@ export class CheckoutDummyStore extends ComponentStore<CheckoutDummyState> {
|
||||
};
|
||||
}
|
||||
|
||||
private _createProductDTO({ item, control }: { item?: ItemDTO; control: UntypedFormGroup }): ProductDTO {
|
||||
private _createProductDTO({
|
||||
item,
|
||||
control,
|
||||
}: {
|
||||
item?: ItemDTO;
|
||||
control: UntypedFormGroup;
|
||||
}): ProductDTO {
|
||||
const formValues: Partial<ProductDTO> = {
|
||||
ean: control.get('ean').value,
|
||||
name: control.get('name').value,
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
<shared-loader [loading]="fetching$ | async" background="true" spinnerSize="32">
|
||||
<div class="overflow-scroll max-h-[calc(100vh-15rem)]">
|
||||
<div class="customer-details-header grid grid-flow-row pb-6">
|
||||
<div class="customer-details-header-actions flex flex-row justify-end pt-4 px-4">
|
||||
<div
|
||||
class="customer-details-header-actions flex flex-row justify-end pt-4 px-4"
|
||||
>
|
||||
<page-customer-menu
|
||||
[customerId]="customerId$ | async"
|
||||
[processId]="processId$ | async"
|
||||
@@ -16,8 +18,12 @@
|
||||
<p>Sind Ihre Kundendaten korrekt?</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="customer-details-customer-type flex flex-row justify-between items-center bg-surface-2 text-surface-2-content h-14">
|
||||
<div class="pl-4 font-bold grid grid-flow-col justify-start items-center gap-2">
|
||||
<div
|
||||
class="customer-details-customer-type flex flex-row justify-between items-center bg-surface-2 text-surface-2-content h-14"
|
||||
>
|
||||
<div
|
||||
class="pl-4 font-bold grid grid-flow-col justify-start items-center gap-2"
|
||||
>
|
||||
<shared-icon [icon]="customerType$ | async"></shared-icon>
|
||||
<span>
|
||||
{{ customerType$ | async }}
|
||||
@@ -30,19 +36,22 @@
|
||||
[queryParams]="editRoute.queryParams"
|
||||
[queryParamsHandling]="'merge'"
|
||||
class="btn btn-label font-bold text-brand"
|
||||
>
|
||||
>
|
||||
Bearbeiten
|
||||
</a>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="customer-details-customer-main-data px-5 py-3 grid grid-flow-row gap-3">
|
||||
<div
|
||||
class="customer-details-customer-main-data px-5 py-3 grid grid-flow-row gap-3"
|
||||
>
|
||||
<div class="flex flex-row">
|
||||
<div class="data-label">Erstellungsdatum</div>
|
||||
@if (created$ | async; as created) {
|
||||
<div class="data-value">
|
||||
{{ created | date: 'dd.MM.yyyy' }} | {{ created | date: 'HH:mm' }} Uhr
|
||||
{{ created | date: 'dd.MM.yyyy' }} |
|
||||
{{ created | date: 'HH:mm' }} Uhr
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@@ -136,7 +145,9 @@
|
||||
@if (!(isBusinessKonto$ | async)) {
|
||||
<div class="customer-details-customer-main-row">
|
||||
<div class="data-label">Geburtstag</div>
|
||||
<div class="data-value">{{ dateOfBirth$ | async | date: 'dd.MM.yyyy' }}</div>
|
||||
<div class="data-value">
|
||||
{{ dateOfBirth$ | async | date: 'dd.MM.yyyy' }}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@if (!(isBusinessKonto$ | async) && (organisationName$ | async)) {
|
||||
@@ -162,24 +173,41 @@
|
||||
</div>
|
||||
</shared-loader>
|
||||
|
||||
@if (shoppingCartHasNoItems$ | async) {
|
||||
<button
|
||||
type="button"
|
||||
(click)="continue()"
|
||||
class="text-white text-lg bg-brand rounded-full px-5 py-3 absolute top-[calc(100vh-19.375rem)] left-1/2 -translate-x-1/2 font-bold disabled:bg-inactive-branch"
|
||||
[disabled]="showLoader$ | async"
|
||||
@if (!isRewardTab()) {
|
||||
@if (shoppingCartHasNoItems$ | async) {
|
||||
<button
|
||||
type="button"
|
||||
(click)="continue()"
|
||||
class="text-white text-lg bg-brand rounded-full px-5 py-3 absolute top-[calc(100vh-19.375rem)] left-1/2 -translate-x-1/2 font-bold disabled:bg-inactive-branch"
|
||||
[disabled]="showLoader$ | async"
|
||||
>
|
||||
<shared-loader [loading]="showLoader$ | async" spinnerSize="32">Weiter zur Artikelsuche</shared-loader>
|
||||
</button>
|
||||
}
|
||||
<shared-loader [loading]="showLoader$ | async" spinnerSize="32"
|
||||
>Weiter zur Artikelsuche</shared-loader
|
||||
>
|
||||
</button>
|
||||
}
|
||||
|
||||
@if (shoppingCartHasItems$ | async) {
|
||||
@if (shoppingCartHasItems$ | async) {
|
||||
<button
|
||||
type="button"
|
||||
(click)="continue()"
|
||||
class="text-white text-lg bg-brand rounded-full px-5 py-3 absolute top-[calc(100vh-19.375rem)] left-1/2 -translate-x-1/2 font-bold disabled:bg-inactive-branch"
|
||||
[disabled]="showLoader$ | async"
|
||||
>
|
||||
<shared-loader [loading]="showLoader$ | async" spinnerSize="32"
|
||||
>Weiter zum Warenkorb</shared-loader
|
||||
>
|
||||
</button>
|
||||
}
|
||||
} @else {
|
||||
<button
|
||||
type="button"
|
||||
(click)="continue()"
|
||||
class="text-white text-lg bg-brand rounded-full px-5 py-3 absolute top-[calc(100vh-19.375rem)] left-1/2 -translate-x-1/2 font-bold disabled:bg-inactive-branch"
|
||||
[disabled]="showLoader$ | async"
|
||||
class="w-60 text-white text-lg bg-brand rounded-full px-5 py-3 absolute top-[calc(100vh-19.375rem)] left-1/2 -translate-x-1/2 font-bold disabled:bg-inactive-branch"
|
||||
[disabled]="!(hasKundenkarte$ | async)"
|
||||
>
|
||||
<shared-loader [loading]="showLoader$ | async" spinnerSize="32"
|
||||
>Auswählen</shared-loader
|
||||
>
|
||||
<shared-loader [loading]="showLoader$ | async" spinnerSize="32">Weiter zum Warenkorb</shared-loader>
|
||||
</button>
|
||||
}
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
import { Component, ChangeDetectionStrategy, OnInit, OnDestroy, inject } from '@angular/core';
|
||||
import {
|
||||
Component,
|
||||
ChangeDetectionStrategy,
|
||||
OnInit,
|
||||
OnDestroy,
|
||||
inject,
|
||||
linkedSignal,
|
||||
} from '@angular/core';
|
||||
import { Subject, combineLatest } from 'rxjs';
|
||||
import { first, map, switchMap, takeUntil } from 'rxjs/operators';
|
||||
import { CustomerSearchNavigation } from '@shared/services/navigation';
|
||||
@@ -19,12 +26,18 @@ import {
|
||||
import { UiModalService } from '@ui/modal';
|
||||
import { ComponentStore } from '@ngrx/component-store';
|
||||
import { ApplicationService } from '@core/application';
|
||||
import { CheckoutNavigationService, ProductCatalogNavigationService } from '@shared/services/navigation';
|
||||
import {
|
||||
CheckoutNavigationService,
|
||||
ProductCatalogNavigationService,
|
||||
} from '@shared/services/navigation';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { log, logAsync } from '@utils/common';
|
||||
import { CrmCustomerService } from '@domain/crm';
|
||||
import { MessageModalComponent, MessageModalData } from '@modal/message';
|
||||
import { GenderSettingsService } from '@shared/services/gender';
|
||||
import { injectTab } from '@isa/core/tabs';
|
||||
import { toSignal } from '@angular/core/rxjs-interop';
|
||||
import { SelectedCustomerFacade } from '@isa/crm/data-access';
|
||||
|
||||
export interface CustomerDetailsViewMainState {
|
||||
isBusy: boolean;
|
||||
@@ -57,62 +70,105 @@ export class CustomerDetailsViewMainComponent
|
||||
private _onDestroy$ = new Subject<void>();
|
||||
|
||||
customerService = inject(CrmCustomerService);
|
||||
selectCustomerFacade = inject(SelectedCustomerFacade);
|
||||
tab = injectTab();
|
||||
|
||||
fetching$ = combineLatest([this._store.fetchingCustomer$, this._store.fetchingCustomerList$]).pipe(
|
||||
fetching$ = combineLatest([
|
||||
this._store.fetchingCustomer$,
|
||||
this._store.fetchingCustomerList$,
|
||||
]).pipe(
|
||||
map(([fetchingCustomer, fetchingList]) => fetchingCustomer || fetchingList),
|
||||
);
|
||||
|
||||
isBusy$ = this.select((s) => s.isBusy);
|
||||
|
||||
showLoader$ = combineLatest([this.fetching$, this.isBusy$]).pipe(map(([fetching, isBusy]) => fetching || isBusy));
|
||||
showLoader$ = combineLatest([this.fetching$, this.isBusy$]).pipe(
|
||||
map(([fetching, isBusy]) => fetching || isBusy),
|
||||
);
|
||||
|
||||
processId$ = this._store.processId$;
|
||||
processIdSignal = toSignal(this.processId$);
|
||||
|
||||
customerId$ = this._store.customerId$;
|
||||
|
||||
historyRoute$ = combineLatest([this.processId$, this.customerId$]).pipe(
|
||||
map(([processId, customerId]) => this._navigation.historyRoute({ processId, customerId })),
|
||||
map(([processId, customerId]) =>
|
||||
this._navigation.historyRoute({ processId, customerId }),
|
||||
),
|
||||
);
|
||||
|
||||
ordersRoute$ = combineLatest([this.processId$, this.customerId$]).pipe(
|
||||
map(([processId, customerId]) => this._navigation.ordersRoute({ processId, customerId })),
|
||||
map(([processId, customerId]) =>
|
||||
this._navigation.ordersRoute({ processId, customerId }),
|
||||
),
|
||||
);
|
||||
|
||||
isB2b$ = this._store.isBusinessKonto$;
|
||||
|
||||
editRoute$ = combineLatest([this.processId$, this.customerId$, this.isB2b$]).pipe(
|
||||
map(([processId, customerId, isB2b]) => this._navigation.editRoute({ processId, customerId, isB2b })),
|
||||
);
|
||||
|
||||
showEditButton$ = combineLatest([this._store.isOnlinekonto$, this._store.isBestellungOhneKonto$]).pipe(
|
||||
map(([isOnlinekonto, isBestellungOhneKonto]) => !isOnlinekonto && !isBestellungOhneKonto),
|
||||
);
|
||||
|
||||
hasKundenkarte$ = combineLatest([this._store.isKundenkarte$, this._store.isOnlineKontoMitKundenkarte$]).pipe(
|
||||
map(([isKundenkarte, isOnlineKontoMitKundenkarte]) => isKundenkarte || isOnlineKontoMitKundenkarte),
|
||||
);
|
||||
|
||||
kundenkarteRoute$ = combineLatest([this.hasKundenkarte$, this.processId$, this.customerId$]).pipe(
|
||||
map(([hasKundenkarte, processId, customerId]) =>
|
||||
hasKundenkarte ? this._navigation.kundenkarteRoute({ processId, customerId }) : undefined,
|
||||
editRoute$ = combineLatest([
|
||||
this.processId$,
|
||||
this.customerId$,
|
||||
this.isB2b$,
|
||||
]).pipe(
|
||||
map(([processId, customerId, isB2b]) =>
|
||||
this._navigation.editRoute({ processId, customerId, isB2b }),
|
||||
),
|
||||
);
|
||||
|
||||
customerType$ = this._store.select((s) => s.customer?.features?.find((f) => f.enabled)?.description);
|
||||
showEditButton$ = combineLatest([
|
||||
this._store.isOnlinekonto$,
|
||||
this._store.isBestellungOhneKonto$,
|
||||
]).pipe(
|
||||
map(
|
||||
([isOnlinekonto, isBestellungOhneKonto]) =>
|
||||
!isOnlinekonto && !isBestellungOhneKonto,
|
||||
),
|
||||
);
|
||||
|
||||
hasKundenkarte$ = combineLatest([
|
||||
this._store.isKundenkarte$,
|
||||
this._store.isOnlineKontoMitKundenkarte$,
|
||||
]).pipe(
|
||||
map(
|
||||
([isKundenkarte, isOnlineKontoMitKundenkarte]) =>
|
||||
isKundenkarte || isOnlineKontoMitKundenkarte,
|
||||
),
|
||||
);
|
||||
|
||||
kundenkarteRoute$ = combineLatest([
|
||||
this.hasKundenkarte$,
|
||||
this.processId$,
|
||||
this.customerId$,
|
||||
]).pipe(
|
||||
map(([hasKundenkarte, processId, customerId]) =>
|
||||
hasKundenkarte
|
||||
? this._navigation.kundenkarteRoute({ processId, customerId })
|
||||
: undefined,
|
||||
),
|
||||
);
|
||||
|
||||
customerType$ = this._store.select(
|
||||
(s) => s.customer?.features?.find((f) => f.enabled)?.description,
|
||||
);
|
||||
|
||||
created$ = this._store.select((s) => s.customer?.created);
|
||||
|
||||
customerNumber$ = this._store.select((s) => s.customer?.customerNumber);
|
||||
|
||||
customerNumberDig$ = this._store.select(
|
||||
(s) => s.customer?.linkedRecords?.find((r) => r.repository === 'dig')?.number,
|
||||
(s) =>
|
||||
s.customer?.linkedRecords?.find((r) => r.repository === 'dig')?.number,
|
||||
);
|
||||
|
||||
customerNumberBeeline$ = this._store.select(
|
||||
(s) => s.customer?.linkedRecords?.find((r) => r.repository === 'beeline')?.number,
|
||||
(s) =>
|
||||
s.customer?.linkedRecords?.find((r) => r.repository === 'beeline')
|
||||
?.number,
|
||||
);
|
||||
|
||||
gender$ = this._store.select((s) => this._genderSettings.getGenderByValue(s.customer?.gender));
|
||||
gender$ = this._store.select((s) =>
|
||||
this._genderSettings.getGenderByValue(s.customer?.gender),
|
||||
);
|
||||
|
||||
title$ = this._store.select((s) => s.customer?.title);
|
||||
|
||||
@@ -134,7 +190,9 @@ export class CustomerDetailsViewMainComponent
|
||||
|
||||
info$ = this._store.select((s) => s.customer?.address?.info);
|
||||
|
||||
landline$ = this._store.select((s) => s.customer?.communicationDetails?.phone);
|
||||
landline$ = this._store.select(
|
||||
(s) => s.customer?.communicationDetails?.phone,
|
||||
);
|
||||
|
||||
mobile$ = this._store.select((s) => s.customer?.communicationDetails?.mobile);
|
||||
|
||||
@@ -148,7 +206,9 @@ export class CustomerDetailsViewMainComponent
|
||||
|
||||
shoppingCartHasItems$ = this.select((s) => s.shoppingCart?.items?.length > 0);
|
||||
|
||||
shoppingCartHasNoItems$ = this.shoppingCartHasItems$.pipe(map((hasItems) => !hasItems));
|
||||
shoppingCartHasNoItems$ = this.shoppingCartHasItems$.pipe(
|
||||
map((hasItems) => !hasItems),
|
||||
);
|
||||
|
||||
dateOfBirth$ = this._store.select((s) => s.customer?.dateOfBirth);
|
||||
|
||||
@@ -199,8 +259,12 @@ export class CustomerDetailsViewMainComponent
|
||||
firstName: customer.firstName,
|
||||
lastName: customer.lastName,
|
||||
dateOfBirth: customer.dateOfBirth,
|
||||
communicationDetails: customer.communicationDetails ? { ...customer.communicationDetails } : undefined,
|
||||
organisation: customer.organisation ? { ...customer.organisation } : undefined,
|
||||
communicationDetails: customer.communicationDetails
|
||||
? { ...customer.communicationDetails }
|
||||
: undefined,
|
||||
organisation: customer.organisation
|
||||
? { ...customer.organisation }
|
||||
: undefined,
|
||||
address: customer.address ? { ...customer.address } : undefined,
|
||||
};
|
||||
}
|
||||
@@ -213,12 +277,18 @@ export class CustomerDetailsViewMainComponent
|
||||
return this._activatedRoute.snapshot;
|
||||
}
|
||||
|
||||
isOnlineOrCustomerCardUser$ = combineLatest([this.customerType$, this.hasKundenkarte$]).pipe(
|
||||
map(([type, hasCard]) => type === 'webshop' || hasCard),
|
||||
);
|
||||
isOnlineOrCustomerCardUser$ = combineLatest([
|
||||
this.customerType$,
|
||||
this.hasKundenkarte$,
|
||||
]).pipe(map(([type, hasCard]) => type === 'webshop' || hasCard));
|
||||
|
||||
constructor() {
|
||||
super({ isBusy: false, shoppingCart: undefined, shippingAddress: undefined, payer: undefined });
|
||||
super({
|
||||
isBusy: false,
|
||||
shoppingCart: undefined,
|
||||
shippingAddress: undefined,
|
||||
payer: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
setIsBusy(isBusy: boolean) {
|
||||
@@ -235,6 +305,11 @@ export class CustomerDetailsViewMainComponent
|
||||
this.patchState({ payer });
|
||||
}
|
||||
|
||||
isRewardTab = linkedSignal(() => {
|
||||
const tab = this.tab();
|
||||
return tab?.metadata?.['context'] === 'reward';
|
||||
});
|
||||
|
||||
ngOnInit() {
|
||||
this.processId$
|
||||
.pipe(
|
||||
@@ -254,16 +329,18 @@ export class CustomerDetailsViewMainComponent
|
||||
// Dies geschieht bereits in der customer-search.component.ts immer wenn eine Navigation ausgeführt wird (sprich für Fälle in denen ein neuer Kunde gesetzt wird).
|
||||
// Falls aber nach exakt dem gleichen Kunden gesucht wird, muss man diesen hier manuell erneut setzen, ansonsten bleibt dieser im Store auf undefined.
|
||||
// Da dies nur für die Detailansicht relevant ist, wird hier auch auf hits 1 überprüft, ansonsten befindet man sich sowieso in der Trefferliste.
|
||||
this._store.customerListResponse$.pipe(takeUntil(this._onDestroy$)).subscribe(([result]) => {
|
||||
if (result.hits === 1) {
|
||||
const customerId = result?.result[0].id;
|
||||
const selectedCustomerId = +this.snapshot.params.customerId;
|
||||
this._store.customerListResponse$
|
||||
.pipe(takeUntil(this._onDestroy$))
|
||||
.subscribe(([result]) => {
|
||||
if (result.hits === 1) {
|
||||
const customerId = result?.result[0].id;
|
||||
const selectedCustomerId = +this.snapshot.params.customerId;
|
||||
|
||||
if (customerId === selectedCustomerId) {
|
||||
this._store.selectCustomer({ customerId });
|
||||
if (customerId === selectedCustomerId) {
|
||||
this._store.selectCustomer({ customerId });
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
@@ -316,19 +393,34 @@ export class CustomerDetailsViewMainComponent
|
||||
|
||||
this._setShippingAddress();
|
||||
|
||||
if (this.shoppingCartHasItems) {
|
||||
// Navigation zum Warenkorb
|
||||
const path = this._checkoutNavigation.getCheckoutReviewPath(this.processId).path;
|
||||
this._router.navigate(path);
|
||||
// #5262 Damit der Prämienshop den selektierten Customer mitbekommt
|
||||
// Evtl. nicht mehr notwendig, wenn der neue TabService Adapter in Develop ist
|
||||
this.selectCustomerFacade.set(this.processId, this.customer.id);
|
||||
|
||||
if (this.isRewardTab()) {
|
||||
await this._router.navigate(['/', this.processId, 'reward']);
|
||||
} else {
|
||||
// Navigation zur Artikelsuche
|
||||
const path = this._catalogNavigation.getArticleSearchBasePath(this.processId).path;
|
||||
this._router.navigate(path);
|
||||
if (this.shoppingCartHasItems) {
|
||||
// Navigation zum Warenkorb
|
||||
const path = this._checkoutNavigation.getCheckoutReviewPath(
|
||||
this.processId,
|
||||
).path;
|
||||
this._router.navigate(path);
|
||||
} else {
|
||||
// Navigation zur Artikelsuche
|
||||
const path = this._catalogNavigation.getArticleSearchBasePath(
|
||||
this.processId,
|
||||
).path;
|
||||
this._router.navigate(path);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
} catch (error) {
|
||||
this._modalService.error('Warenkorb kann dem Kunden nicht zugewiesen werden', error);
|
||||
this._modalService.error(
|
||||
'Warenkorb kann dem Kunden nicht zugewiesen werden',
|
||||
error,
|
||||
);
|
||||
} finally {
|
||||
this.setIsBusy(false);
|
||||
}
|
||||
@@ -336,7 +428,10 @@ export class CustomerDetailsViewMainComponent
|
||||
|
||||
@logAsync
|
||||
_getCurrentBuyer() {
|
||||
return this._checkoutService.getBuyer({ processId: this.processId }).pipe(first()).toPromise();
|
||||
return this._checkoutService
|
||||
.getBuyer({ processId: this.processId })
|
||||
.pipe(first())
|
||||
.toPromise();
|
||||
}
|
||||
|
||||
@logAsync
|
||||
@@ -352,7 +447,9 @@ export class CustomerDetailsViewMainComponent
|
||||
return true;
|
||||
}
|
||||
|
||||
const required = await this.customerService.canUpgrade(this.customer.id).toPromise();
|
||||
const required = await this.customerService
|
||||
.canUpgrade(this.customer.id)
|
||||
.toPromise();
|
||||
|
||||
const upgradeableTo = res.create;
|
||||
const data: CantAddCustomerToCartData = {
|
||||
@@ -407,7 +504,10 @@ export class CustomerDetailsViewMainComponent
|
||||
processId: this.processId,
|
||||
customerId: this.customer.id,
|
||||
});
|
||||
this._router.navigate(nav.path, { queryParams: nav.queryParams, queryParamsHandling: 'merge' });
|
||||
this._router.navigate(nav.path, {
|
||||
queryParams: nav.queryParams,
|
||||
queryParamsHandling: 'merge',
|
||||
});
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -490,7 +590,9 @@ export class CustomerDetailsViewMainComponent
|
||||
async _updateNotifcationChannelsAsync(currentBuyer: BuyerDTO | undefined) {
|
||||
if (currentBuyer?.buyerNumber !== this.customer.customerNumber) {
|
||||
const notificationChannels =
|
||||
this.customer.notificationChannels === (3 as NotificationChannel) ? 1 : this.customer.notificationChannels;
|
||||
this.customer.notificationChannels === (3 as NotificationChannel)
|
||||
? 1
|
||||
: this.customer.notificationChannels;
|
||||
|
||||
this._checkoutService.setNotificationChannels({
|
||||
processId: this.processId,
|
||||
|
||||
@@ -1,39 +1,60 @@
|
||||
<div class="shared-branch-selector-input-container" (click)="branchInput.focus(); openComplete()">
|
||||
<button (click)="onClose($event)" type="button" class="shared-branch-selector-input-icon p-2">
|
||||
<shared-icon class="text-[#AEB7C1]" icon="magnify" [size]="32"></shared-icon>
|
||||
</button>
|
||||
<input
|
||||
#branchInput
|
||||
class="shared-branch-selector-input"
|
||||
[class.shared-branch-selector-opend]="autocompleteComponent?.opend"
|
||||
uiInput
|
||||
type="text"
|
||||
[placeholder]="placeholder"
|
||||
[ngModel]="query$ | async"
|
||||
(ngModelChange)="onQueryChange($event)"
|
||||
(keyup)="onKeyup($event)"
|
||||
/>
|
||||
@if ((query$ | async)?.length > 0) {
|
||||
<button class="shared-branch-selector-clear-input-icon pr-2" type="button" (click)="clear()">
|
||||
<shared-icon class="text-[#1F466C]" icon="close" [size]="32"></shared-icon>
|
||||
</button>
|
||||
}
|
||||
</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>
|
||||
<div
|
||||
class="shared-branch-selector-input-container"
|
||||
(click)="branchInput.focus(); openComplete()"
|
||||
>
|
||||
<button
|
||||
(click)="onClose($event)"
|
||||
type="button"
|
||||
class="shared-branch-selector-input-icon p-2"
|
||||
>
|
||||
<shared-icon
|
||||
class="text-[#AEB7C1]"
|
||||
icon="magnify"
|
||||
[size]="32"
|
||||
></shared-icon>
|
||||
</button>
|
||||
<input
|
||||
#branchInput
|
||||
class="shared-branch-selector-input"
|
||||
[class.shared-branch-selector-opend]="autocompleteComponent?.opend"
|
||||
uiInput
|
||||
type="text"
|
||||
[placeholder]="placeholder"
|
||||
[ngModel]="query$ | async"
|
||||
(ngModelChange)="onQueryChange($event)"
|
||||
(keyup)="onKeyup($event)"
|
||||
/>
|
||||
@if ((query$ | async)?.length > 0) {
|
||||
<button
|
||||
class="shared-branch-selector-clear-input-icon pr-2"
|
||||
type="button"
|
||||
(click)="clear()"
|
||||
>
|
||||
<shared-icon
|
||||
class="text-[#1F466C]"
|
||||
icon="close"
|
||||
[size]="32"
|
||||
></shared-icon>
|
||||
</button>
|
||||
}
|
||||
</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>
|
||||
|
||||
@@ -16,20 +16,20 @@ import {
|
||||
forwardRef,
|
||||
Optional,
|
||||
inject,
|
||||
} from '@angular/core';
|
||||
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
|
||||
import { UiAutocompleteComponent } from '@ui/autocomplete';
|
||||
import { UiFormControlDirective } from '@ui/form-control';
|
||||
import { containsElement } from '@utils/common';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { ScanAdapterService } from '@adapter/scan';
|
||||
import { injectCancelSearch } from '@shared/services/cancel-subject';
|
||||
import { EnvironmentService } from '@core/environment';
|
||||
} from "@angular/core";
|
||||
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from "@angular/forms";
|
||||
import { UiAutocompleteComponent } from "@ui/autocomplete";
|
||||
import { UiFormControlDirective } from "@ui/form-control";
|
||||
import { containsElement } from "@utils/common";
|
||||
import { Subscription } from "rxjs";
|
||||
import { ScanAdapterService } from "@adapter/scan";
|
||||
import { injectCancelSearch } from "@shared/services/cancel-subject";
|
||||
import { EnvironmentService } from "@core/environment";
|
||||
|
||||
@Component({
|
||||
selector: 'shared-searchbox',
|
||||
templateUrl: 'searchbox.component.html',
|
||||
styleUrls: ['searchbox.component.scss'],
|
||||
selector: "shared-searchbox",
|
||||
templateUrl: "searchbox.component.html",
|
||||
styleUrls: ["searchbox.component.scss"],
|
||||
providers: [
|
||||
{
|
||||
provide: NG_VALUE_ACCESSOR,
|
||||
@@ -49,9 +49,9 @@ export class SearchboxComponent
|
||||
cancelSearch = injectCancelSearch({ optional: true });
|
||||
|
||||
disabled: boolean;
|
||||
type = 'text';
|
||||
type = "text";
|
||||
|
||||
@ViewChild('input', { read: ElementRef, static: true })
|
||||
@ViewChild("input", { read: ElementRef, static: true })
|
||||
input: ElementRef;
|
||||
|
||||
@ContentChild(UiAutocompleteComponent)
|
||||
@@ -61,9 +61,9 @@ export class SearchboxComponent
|
||||
focusAfterViewInit = true;
|
||||
|
||||
@Input()
|
||||
placeholder = '';
|
||||
placeholder = "";
|
||||
|
||||
private _query = '';
|
||||
private _query = "";
|
||||
|
||||
@Input()
|
||||
get query() {
|
||||
@@ -94,7 +94,7 @@ export class SearchboxComponent
|
||||
scanner = false;
|
||||
|
||||
@Input()
|
||||
hint = '';
|
||||
hint = "";
|
||||
|
||||
@Input()
|
||||
autocompleteValueSelector: (item: any) => string = (item: any) => item;
|
||||
@@ -104,11 +104,11 @@ export class SearchboxComponent
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.setQuery('');
|
||||
this.setQuery("");
|
||||
this.cancelSearch();
|
||||
}
|
||||
|
||||
@HostBinding('class.autocomplete-opend')
|
||||
@HostBinding("class.autocomplete-opend")
|
||||
get autocompleteOpen() {
|
||||
return this.autocomplete?.opend;
|
||||
}
|
||||
@@ -213,13 +213,13 @@ export class SearchboxComponent
|
||||
}
|
||||
|
||||
clearHint() {
|
||||
this.hint = '';
|
||||
this.hint = "";
|
||||
this.focused.emit(true);
|
||||
this.cdr.markForCheck();
|
||||
}
|
||||
|
||||
onKeyup(event: KeyboardEvent) {
|
||||
if (event.key === 'Enter') {
|
||||
if (event.key === "Enter") {
|
||||
if (this.autocomplete?.opend && this.autocomplete?.activeItem) {
|
||||
this.setQuery(this.autocomplete?.activeItem?.item);
|
||||
this.autocomplete?.close();
|
||||
@@ -227,7 +227,7 @@ export class SearchboxComponent
|
||||
this.search.emit(this.query);
|
||||
|
||||
event.preventDefault();
|
||||
} else if (event.key === 'ArrowUp' || event.key === 'ArrowDown') {
|
||||
} else if (event.key === "ArrowUp" || event.key === "ArrowDown") {
|
||||
this.handleArrowUpDownEvent(event);
|
||||
}
|
||||
}
|
||||
@@ -242,7 +242,7 @@ export class SearchboxComponent
|
||||
}
|
||||
}
|
||||
|
||||
@HostListener('window:click', ['$event'])
|
||||
@HostListener("window:click", ["$event"])
|
||||
focusLost(event: MouseEvent) {
|
||||
if (
|
||||
this.autocomplete?.opend &&
|
||||
@@ -256,9 +256,11 @@ export class SearchboxComponent
|
||||
this.search.emit(this.query);
|
||||
}
|
||||
|
||||
@HostListener('focusout', ['$event'])
|
||||
@HostListener("focusout", ["$event"])
|
||||
onBlur() {
|
||||
this.onTouched();
|
||||
if (typeof this.onTouched === "function") {
|
||||
this.onTouched();
|
||||
}
|
||||
this.focused.emit(false);
|
||||
this.cdr.markForCheck();
|
||||
}
|
||||
|
||||
@@ -1,31 +1,38 @@
|
||||
<div
|
||||
class="tab-wrapper flex flex-row items-center justify-between border-b-[0.188rem] border-solid h-14"
|
||||
[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"
|
||||
[routerLink]="routerLink$ | async"
|
||||
[queryParams]="queryParams$ | async"
|
||||
(click)="scrollIntoView()"
|
||||
>
|
||||
<span class="truncate">
|
||||
{{ process?.name }}
|
||||
</span>
|
||||
@if (process?.type !== 'cart-checkout') {
|
||||
<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.active]="isActive$ | async"
|
||||
[routerLink]="getCheckoutPath((process$ | async)?.id)"
|
||||
(click)="$event?.preventDefault(); $event?.stopPropagation()"
|
||||
>
|
||||
<shared-icon icon="shopping-cart-bold" [size]="22"></shared-icon>
|
||||
<span class="shopping-cart-count-label ml-2">{{ cartItemCount$ | async }}</span>
|
||||
</button>
|
||||
}
|
||||
</a>
|
||||
<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>
|
||||
</button>
|
||||
</div>
|
||||
@if (process(); as p) {
|
||||
<div
|
||||
class="tab-wrapper flex flex-row items-center justify-between border-b-[0.188rem] border-solid h-14"
|
||||
[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"
|
||||
[href]="currentLocationUrlTree()?.toString()"
|
||||
(click)="navigateByUrl($event); scrollIntoView()"
|
||||
>
|
||||
<span class="truncate">
|
||||
{{ p.name }}
|
||||
</span>
|
||||
@if (showCart() && p.type === 'cart') {
|
||||
<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.active]="isActive$ | async"
|
||||
[routerLink]="getCheckoutPath((process$ | async)?.id)"
|
||||
(click)="$event?.preventDefault(); $event?.stopPropagation()"
|
||||
>
|
||||
<shared-icon icon="shopping-cart-bold" [size]="22"></shared-icon>
|
||||
<span class="shopping-cart-count-label ml-2">{{
|
||||
cartItemCount$ | async
|
||||
}}</span>
|
||||
</button>
|
||||
}
|
||||
</a>
|
||||
<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>
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -1,156 +1,208 @@
|
||||
import {
|
||||
Component,
|
||||
ChangeDetectionStrategy,
|
||||
Input,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
OnChanges,
|
||||
SimpleChanges,
|
||||
EventEmitter,
|
||||
Output,
|
||||
ElementRef,
|
||||
} from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { ApplicationProcess, ApplicationService } from '@core/application';
|
||||
import { Breadcrumb, BreadcrumbService } from '@core/breadcrumb';
|
||||
import { DomainCheckoutService } from '@domain/checkout';
|
||||
import { CheckoutNavigationService } from '@shared/services/navigation';
|
||||
import { BehaviorSubject, NEVER, Observable, combineLatest, isObservable } from 'rxjs';
|
||||
import { first, map, switchMap, tap } from 'rxjs/operators';
|
||||
|
||||
@Component({
|
||||
selector: 'shell-process-bar-item',
|
||||
templateUrl: 'process-bar-item.component.html',
|
||||
styleUrls: ['process-bar-item.component.css'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: false,
|
||||
})
|
||||
export class ShellProcessBarItemComponent implements OnInit, OnDestroy, OnChanges {
|
||||
private _process$ = new BehaviorSubject<ApplicationProcess>(undefined);
|
||||
|
||||
process$ = this._process$.asObservable();
|
||||
|
||||
@Input()
|
||||
process: ApplicationProcess;
|
||||
|
||||
@Output()
|
||||
closed = new EventEmitter();
|
||||
|
||||
activatedProcessId$ = this._app.activatedProcessId$;
|
||||
|
||||
latestBreadcrumb$: Observable<Breadcrumb> = NEVER;
|
||||
|
||||
routerLink$: Observable<string[] | any[]> = NEVER;
|
||||
|
||||
queryParams$: Observable<object> = NEVER;
|
||||
|
||||
isActive$: Observable<boolean> = NEVER;
|
||||
|
||||
showCloseButton$: Observable<boolean> = NEVER;
|
||||
|
||||
cartItemCount$: Observable<number> = NEVER;
|
||||
|
||||
constructor(
|
||||
private _breadcrumb: BreadcrumbService,
|
||||
private _app: ApplicationService,
|
||||
private _router: Router,
|
||||
private _checkout: DomainCheckoutService,
|
||||
private _checkoutNavigationService: CheckoutNavigationService,
|
||||
public _elRef: ElementRef<HTMLElement>,
|
||||
) {}
|
||||
|
||||
ngOnChanges({ process }: SimpleChanges): void {
|
||||
if (process) {
|
||||
this._process$.next(process.currentValue);
|
||||
}
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.initLatestBreadcrumb$();
|
||||
this.initRouterLink$();
|
||||
this.initQueryParams$();
|
||||
this.initIsActive$();
|
||||
this.initShowCloseButton$();
|
||||
this.initCartItemCount$();
|
||||
}
|
||||
|
||||
scrollIntoView() {
|
||||
setTimeout(() => this._elRef.nativeElement.scrollIntoView({ behavior: 'smooth', block: 'center' }), 0);
|
||||
}
|
||||
|
||||
getCheckoutPath(processId: number) {
|
||||
return this._checkoutNavigationService.getCheckoutReviewPath(processId).path;
|
||||
}
|
||||
|
||||
initLatestBreadcrumb$() {
|
||||
this.latestBreadcrumb$ = this.process$.pipe(
|
||||
switchMap((process) => this._breadcrumb.getLastActivatedBreadcrumbByKey$(process?.id)),
|
||||
);
|
||||
}
|
||||
|
||||
initRouterLink$() {
|
||||
this.routerLink$ = this.latestBreadcrumb$.pipe(
|
||||
map((breadcrumb) => (breadcrumb?.path instanceof Array ? breadcrumb.path : [breadcrumb?.path])),
|
||||
);
|
||||
}
|
||||
|
||||
initQueryParams$() {
|
||||
this.queryParams$ = this.latestBreadcrumb$.pipe(map((breadcrumb) => breadcrumb?.params));
|
||||
}
|
||||
|
||||
initIsActive$() {
|
||||
if (isObservable(this.activatedProcessId$) && isObservable(this.process$)) {
|
||||
this.isActive$ = combineLatest([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() {
|
||||
const breadcrumb = await this.getLatestBreadcrumbForSection();
|
||||
await this.navigate(breadcrumb);
|
||||
this._app.removeProcess(this.process.id);
|
||||
this.closed.emit();
|
||||
}
|
||||
|
||||
getLatestBreadcrumbForSection(): Promise<Breadcrumb> {
|
||||
return this._breadcrumb
|
||||
.getLatestBreadcrumbForSection('customer', (c) => c.key !== this.process?.id)
|
||||
.pipe(first())
|
||||
.toPromise();
|
||||
}
|
||||
|
||||
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']);
|
||||
}
|
||||
}
|
||||
}
|
||||
import {
|
||||
Component,
|
||||
ChangeDetectionStrategy,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
OnChanges,
|
||||
SimpleChanges,
|
||||
EventEmitter,
|
||||
Output,
|
||||
ElementRef,
|
||||
inject,
|
||||
computed,
|
||||
input,
|
||||
effect,
|
||||
} from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { ApplicationProcess, ApplicationService } from '@core/application';
|
||||
import { Breadcrumb } from '@core/breadcrumb';
|
||||
import { DomainCheckoutService } from '@domain/checkout';
|
||||
import { CheckoutNavigationService } from '@shared/services/navigation';
|
||||
import {
|
||||
BehaviorSubject,
|
||||
NEVER,
|
||||
Observable,
|
||||
combineLatest,
|
||||
isObservable,
|
||||
} from 'rxjs';
|
||||
import { map, switchMap, tap } from 'rxjs/operators';
|
||||
import { TabService } from '@isa/core/tabs';
|
||||
|
||||
@Component({
|
||||
selector: 'shell-process-bar-item',
|
||||
templateUrl: 'process-bar-item.component.html',
|
||||
styleUrls: ['process-bar-item.component.css'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: false,
|
||||
})
|
||||
export class ShellProcessBarItemComponent
|
||||
implements OnInit, OnDestroy, OnChanges
|
||||
{
|
||||
#tabService = inject(TabService);
|
||||
|
||||
tab = computed(() => this.#tabService.entityMap()[this.process().id]);
|
||||
|
||||
private _process$ = new BehaviorSubject<ApplicationProcess>(undefined);
|
||||
|
||||
process$ = this._process$.asObservable();
|
||||
|
||||
process = input.required<ApplicationProcess>();
|
||||
|
||||
@Output()
|
||||
closed = new EventEmitter();
|
||||
|
||||
showCart = computed(() => {
|
||||
const tab = this.tab();
|
||||
|
||||
const pdata = tab.metadata?.process_data as { count?: number };
|
||||
|
||||
if (!pdata) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return 'count' in pdata;
|
||||
});
|
||||
|
||||
currentLocationUrlTree = computed(() => {
|
||||
const tab = this.tab();
|
||||
const current = tab.location.locations[tab.location.current];
|
||||
|
||||
if (current?.url) {
|
||||
return this._router.parseUrl(current.url);
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
navigateByUrl(event: MouseEvent) {
|
||||
event?.preventDefault();
|
||||
this._router.navigateByUrl(this.currentLocationUrlTree());
|
||||
}
|
||||
|
||||
activatedProcessId$ = this._app.activatedProcessId$;
|
||||
|
||||
latestBreadcrumb$: Observable<Breadcrumb> = NEVER;
|
||||
|
||||
routerLink$: Observable<string[] | any[]> = NEVER;
|
||||
|
||||
queryParams$: Observable<object> = NEVER;
|
||||
|
||||
isActive$: Observable<boolean> = NEVER;
|
||||
|
||||
showCloseButton$: Observable<boolean> = NEVER;
|
||||
|
||||
cartItemCount$: Observable<number> = NEVER;
|
||||
|
||||
constructor(
|
||||
private _app: ApplicationService,
|
||||
private _router: Router,
|
||||
private _checkout: DomainCheckoutService,
|
||||
private _checkoutNavigationService: CheckoutNavigationService,
|
||||
public _elRef: ElementRef<HTMLElement>,
|
||||
) {}
|
||||
|
||||
ngOnChanges({ process }: SimpleChanges): void {
|
||||
if (process) {
|
||||
this._process$.next(process.currentValue);
|
||||
}
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.initRouterLink$();
|
||||
this.initQueryParams$();
|
||||
this.initIsActive$();
|
||||
this.initShowCloseButton$();
|
||||
this.initCartItemCount$();
|
||||
}
|
||||
|
||||
scrollIntoView() {
|
||||
setTimeout(
|
||||
() =>
|
||||
this._elRef.nativeElement.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'center',
|
||||
}),
|
||||
0,
|
||||
);
|
||||
}
|
||||
|
||||
getCheckoutPath(processId: number) {
|
||||
return this._checkoutNavigationService.getCheckoutReviewPath(processId)
|
||||
.path;
|
||||
}
|
||||
|
||||
initRouterLink$() {
|
||||
this.routerLink$ = this.latestBreadcrumb$.pipe(
|
||||
map((breadcrumb) =>
|
||||
breadcrumb?.path instanceof Array
|
||||
? breadcrumb.path
|
||||
: [breadcrumb?.path],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
initQueryParams$() {
|
||||
this.queryParams$ = this.latestBreadcrumb$.pipe(
|
||||
map((breadcrumb) => breadcrumb?.params),
|
||||
);
|
||||
}
|
||||
|
||||
initIsActive$() {
|
||||
if (isObservable(this.activatedProcessId$) && isObservable(this.process$)) {
|
||||
this.isActive$ = combineLatest([
|
||||
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,203 @@
|
||||
import { coerceArray } from '@angular/cdk/coercion';
|
||||
import { Component, ChangeDetectionStrategy, OnInit, ViewChild, ElementRef } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { ApplicationProcess, ApplicationService } from '@core/application';
|
||||
import { BreadcrumbService } from '@core/breadcrumb';
|
||||
import { DomainCheckoutService } from '@domain/checkout';
|
||||
import { injectOpenMessageModal } from '@modal/message';
|
||||
import { CustomerOrdersNavigationService, ProductCatalogNavigationService } from '@shared/services/navigation';
|
||||
import { NEVER, Observable, of } from 'rxjs';
|
||||
import { delay, first, map, switchMap } from 'rxjs/operators';
|
||||
|
||||
@Component({
|
||||
selector: 'shell-process-bar',
|
||||
templateUrl: 'process-bar.component.html',
|
||||
styleUrls: ['process-bar.component.css'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: false,
|
||||
})
|
||||
export class ShellProcessBarComponent implements OnInit {
|
||||
@ViewChild('processContainer')
|
||||
processContainer: ElementRef;
|
||||
|
||||
section$: Observable<'customer' | 'branch'> = NEVER;
|
||||
|
||||
processes$: Observable<ApplicationProcess[]> = NEVER;
|
||||
|
||||
showStartProcessText$: Observable<boolean> = NEVER;
|
||||
|
||||
hovered: boolean;
|
||||
showScrollArrows: boolean;
|
||||
showArrowLeft: boolean;
|
||||
showArrowRight: boolean;
|
||||
|
||||
trackByFn = (_: number, process: ApplicationProcess) => process.id;
|
||||
|
||||
openMessageModal = injectOpenMessageModal();
|
||||
|
||||
constructor(
|
||||
private _app: ApplicationService,
|
||||
private _router: Router,
|
||||
private _catalogNavigationService: ProductCatalogNavigationService,
|
||||
private _customerOrderNavigationService: CustomerOrdersNavigationService,
|
||||
private _checkoutService: DomainCheckoutService,
|
||||
private _breadcrumb: BreadcrumbService,
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.initSection$();
|
||||
this.initProcesses$();
|
||||
this.initShowStartProcessText$();
|
||||
this.checkScrollArrowVisibility();
|
||||
}
|
||||
|
||||
initSection$() {
|
||||
this.section$ = of('customer');
|
||||
}
|
||||
|
||||
initProcesses$() {
|
||||
this.processes$ = this.section$.pipe(switchMap((section) => this._app.getProcesses$(section)));
|
||||
}
|
||||
|
||||
initShowStartProcessText$() {
|
||||
this.showStartProcessText$ = this.processes$.pipe(map((processes) => processes.length === 0));
|
||||
}
|
||||
|
||||
async createProcess(target: string = 'product') {
|
||||
const process = await this.createCartProcess();
|
||||
this.navigateTo(target, process);
|
||||
|
||||
setTimeout(() => this.scrollToEnd(), 25);
|
||||
}
|
||||
|
||||
static REGEX_PROCESS_NAME = /^Vorgang \d+$/;
|
||||
|
||||
async createCartProcess() {
|
||||
return this._app.createCustomerProcess();
|
||||
}
|
||||
|
||||
async navigateTo(target: string, process: ApplicationProcess) {
|
||||
switch (target) {
|
||||
case 'product':
|
||||
await this._catalogNavigationService.getArticleSearchBasePath(process.id).navigate();
|
||||
break;
|
||||
case 'customer':
|
||||
await this._router.navigate(['/kunde', process.id, 'customer', 'search']);
|
||||
break;
|
||||
case 'goods-out':
|
||||
await this._router.navigate(['/kunde', process.id, 'goods', 'out']);
|
||||
break;
|
||||
case 'order':
|
||||
await this._customerOrderNavigationService.getCustomerOrdersBasePath(process.id).navigate();
|
||||
break;
|
||||
|
||||
default:
|
||||
await this._router.navigate(['/kunde', process.id, target]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
async closeAllProcesses() {
|
||||
const processes = await this.processes$.pipe(first()).toPromise();
|
||||
this.openMessageModal({
|
||||
title: 'Vorgänge schließen',
|
||||
message: `Sind Sie sich sicher, dass sie alle ${processes.length} Vorgänge schließen wollen?`,
|
||||
actions: [
|
||||
{ label: 'Abbrechen', value: false },
|
||||
{
|
||||
label: 'leere Warenkörbe',
|
||||
value: true,
|
||||
action: () => this.handleCloseEmptyCartProcesses(),
|
||||
},
|
||||
{
|
||||
label: 'Ja, alle',
|
||||
value: true,
|
||||
primary: true,
|
||||
action: () => this.handleCloseAllProcesses(),
|
||||
},
|
||||
],
|
||||
});
|
||||
this.checkScrollArrowVisibility();
|
||||
}
|
||||
|
||||
async handleCloseEmptyCartProcesses() {
|
||||
let processes = await this.processes$.pipe(first()).toPromise();
|
||||
for (const process of processes) {
|
||||
const cart = await this._checkoutService.getShoppingCart({ processId: process.id }).pipe(first()).toPromise();
|
||||
|
||||
if (cart?.items?.length === 0 || cart?.items === undefined) {
|
||||
this._app.removeProcess(process?.id);
|
||||
}
|
||||
|
||||
processes = await this.processes$.pipe(delay(1), first()).toPromise();
|
||||
|
||||
if (processes.length === 0) {
|
||||
this._router.navigate(['/kunde', 'dashboard']);
|
||||
} else {
|
||||
const lastest = processes.reduce(
|
||||
(prev, current) => (prev.activated > current.activated ? prev : current),
|
||||
processes[0],
|
||||
);
|
||||
const crumb = await this._breadcrumb.getLastActivatedBreadcrumbByKey$(lastest.id).pipe(first()).toPromise();
|
||||
if (crumb) {
|
||||
this._router.navigate(coerceArray(crumb.path), { queryParams: crumb.params });
|
||||
} else {
|
||||
this._router.navigate(['/kunde', lastest.id, 'product']);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async handleCloseAllProcesses() {
|
||||
const processes = await this.processes$.pipe(first()).toPromise();
|
||||
processes.forEach((process) => this._app.removeProcess(process?.id));
|
||||
this._router.navigate(['/kunde', 'dashboard']);
|
||||
}
|
||||
|
||||
onMouseWheel(event: any) {
|
||||
// Ermöglicht es, am Desktop die Prozessleiste mit dem Mausrad hoch/runter horizontal zu scrollen
|
||||
if (event.deltaY > 0) {
|
||||
this.processContainer.nativeElement.scrollLeft += 100;
|
||||
} else {
|
||||
this.processContainer.nativeElement.scrollLeft -= 100;
|
||||
}
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
scrollLeft() {
|
||||
this.processContainer.nativeElement.scrollLeft -= 100;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
import { coerceArray } from '@angular/cdk/coercion';
|
||||
import {
|
||||
Component,
|
||||
ChangeDetectionStrategy,
|
||||
OnInit,
|
||||
ViewChild,
|
||||
ElementRef,
|
||||
} from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { ApplicationProcess, ApplicationService } from '@core/application';
|
||||
import { BreadcrumbService } from '@core/breadcrumb';
|
||||
import { DomainCheckoutService } from '@domain/checkout';
|
||||
import { injectOpenMessageModal } from '@modal/message';
|
||||
import {
|
||||
CustomerOrdersNavigationService,
|
||||
ProductCatalogNavigationService,
|
||||
} from '@shared/services/navigation';
|
||||
import { NEVER, Observable, of } from 'rxjs';
|
||||
import { delay, first, map, switchMap } from 'rxjs/operators';
|
||||
|
||||
@Component({
|
||||
selector: 'shell-process-bar',
|
||||
templateUrl: 'process-bar.component.html',
|
||||
styleUrls: ['process-bar.component.css'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: false,
|
||||
})
|
||||
export class ShellProcessBarComponent implements OnInit {
|
||||
@ViewChild('processContainer')
|
||||
processContainer: ElementRef;
|
||||
|
||||
section$: Observable<'customer' | 'branch'> = NEVER;
|
||||
|
||||
processes$: Observable<ApplicationProcess[]> = NEVER;
|
||||
|
||||
showStartProcessText$: Observable<boolean> = NEVER;
|
||||
|
||||
hovered: boolean;
|
||||
showScrollArrows: boolean;
|
||||
showArrowLeft: boolean;
|
||||
showArrowRight: boolean;
|
||||
|
||||
trackByFn = (_: number, process: ApplicationProcess) => process.id;
|
||||
|
||||
openMessageModal = injectOpenMessageModal();
|
||||
|
||||
constructor(
|
||||
private _app: ApplicationService,
|
||||
private _router: Router,
|
||||
private _catalogNavigationService: ProductCatalogNavigationService,
|
||||
private _customerOrderNavigationService: CustomerOrdersNavigationService,
|
||||
private _checkoutService: DomainCheckoutService,
|
||||
private _breadcrumb: BreadcrumbService,
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.initSection$();
|
||||
this.initProcesses$();
|
||||
this.initShowStartProcessText$();
|
||||
this.checkScrollArrowVisibility();
|
||||
}
|
||||
|
||||
initSection$() {
|
||||
this.section$ = of(undefined);
|
||||
}
|
||||
|
||||
initProcesses$() {
|
||||
this.processes$ = this.section$.pipe(
|
||||
switchMap((section) => this._app.getProcesses$(section)),
|
||||
);
|
||||
}
|
||||
|
||||
initShowStartProcessText$() {
|
||||
this.showStartProcessText$ = this.processes$.pipe(
|
||||
map((processes) => processes.length === 0),
|
||||
);
|
||||
}
|
||||
|
||||
async createProcess(target = 'product') {
|
||||
// const process = await this.createCartProcess();
|
||||
this.navigateTo(target, Date.now());
|
||||
|
||||
setTimeout(() => this.scrollToEnd(), 25);
|
||||
}
|
||||
|
||||
static REGEX_PROCESS_NAME = /^Vorgang \d+$/;
|
||||
|
||||
async createCartProcess() {
|
||||
return this._app.createCustomerProcess();
|
||||
}
|
||||
|
||||
async navigateTo(target: string, processId: number) {
|
||||
switch (target) {
|
||||
case 'product':
|
||||
await this._catalogNavigationService
|
||||
.getArticleSearchBasePath(processId)
|
||||
.navigate();
|
||||
break;
|
||||
case 'customer':
|
||||
await this._router.navigate([
|
||||
'/kunde',
|
||||
processId,
|
||||
'customer',
|
||||
'search',
|
||||
]);
|
||||
break;
|
||||
case 'goods-out':
|
||||
await this._router.navigate(['/kunde', processId, 'goods', 'out']);
|
||||
break;
|
||||
case 'order':
|
||||
await this._customerOrderNavigationService
|
||||
.getCustomerOrdersBasePath(processId)
|
||||
.navigate();
|
||||
break;
|
||||
|
||||
default:
|
||||
await this._router.navigate(['/kunde', processId, target]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
async closeAllProcesses() {
|
||||
const processes = await this.processes$.pipe(first()).toPromise();
|
||||
this.openMessageModal({
|
||||
title: 'Vorgänge schließen',
|
||||
message: `Sind Sie sich sicher, dass sie alle ${processes.length} Vorgänge schließen wollen?`,
|
||||
actions: [
|
||||
{ label: 'Abbrechen', value: false },
|
||||
{
|
||||
label: 'leere Warenkörbe',
|
||||
value: true,
|
||||
action: () => this.handleCloseEmptyCartProcesses(),
|
||||
},
|
||||
{
|
||||
label: 'Ja, alle',
|
||||
value: true,
|
||||
primary: true,
|
||||
action: () => this.handleCloseAllProcesses(),
|
||||
},
|
||||
],
|
||||
});
|
||||
this.checkScrollArrowVisibility();
|
||||
}
|
||||
|
||||
async handleCloseEmptyCartProcesses() {
|
||||
let processes = await this.processes$.pipe(first()).toPromise();
|
||||
for (const process of processes) {
|
||||
const cart = await this._checkoutService
|
||||
.getShoppingCart({ processId: process.id })
|
||||
.pipe(first())
|
||||
.toPromise();
|
||||
|
||||
if (cart?.items?.length === 0 || cart?.items === undefined) {
|
||||
this._app.removeProcess(process?.id);
|
||||
}
|
||||
|
||||
processes = await this.processes$.pipe(delay(1), first()).toPromise();
|
||||
|
||||
this._router.navigate(['/kunde', 'dashboard']);
|
||||
}
|
||||
}
|
||||
|
||||
async handleCloseAllProcesses() {
|
||||
const processes = await this.processes$.pipe(first()).toPromise();
|
||||
processes.forEach((process) => this._app.removeProcess(process?.id));
|
||||
this._router.navigate(['/kunde', 'dashboard']);
|
||||
}
|
||||
|
||||
onMouseWheel(event: any) {
|
||||
// Ermöglicht es, am Desktop die Prozessleiste mit dem Mausrad hoch/runter horizontal zu scrollen
|
||||
if (event.deltaY > 0) {
|
||||
this.processContainer.nativeElement.scrollLeft += 100;
|
||||
} else {
|
||||
this.processContainer.nativeElement.scrollLeft -= 100;
|
||||
}
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
scrollLeft() {
|
||||
this.processContainer.nativeElement.scrollLeft -= 100;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,303 +1,357 @@
|
||||
<div class="side-menu-group">
|
||||
<span class="side-menu-group-label">Kunden</span>
|
||||
<nav class="side-menu-group-nav">
|
||||
<a
|
||||
class="side-menu-group-item"
|
||||
(click)="closeSideMenu(); resetBranch(); focusSearchBox()"
|
||||
[routerLink]="productRoutePath$ | async"
|
||||
sharedRegexRouterLinkActive="active"
|
||||
sharedRegexRouterLinkActiveTest="^\/kunde\/\d*\/product"
|
||||
(isActiveChange)="focusSearchBox()"
|
||||
>
|
||||
<div class="side-menu-group-item-icon">
|
||||
<shared-icon icon="import-contacts"></shared-icon>
|
||||
</div>
|
||||
<span class="side-menu-group-item-label">Artikelsuche</span>
|
||||
</a>
|
||||
|
||||
<div class="side-menu-group-sub-item-wrapper">
|
||||
@if (customerSearchRoute$ | async; as customerSearchRoute) {
|
||||
<a
|
||||
class="side-menu-group-item"
|
||||
(click)="closeSideMenu(); focusSearchBox()"
|
||||
[routerLink]="customerSearchRoute.path"
|
||||
[queryParams]="customerSearchRoute.queryParams"
|
||||
sharedRegexRouterLinkActive="active"
|
||||
sharedRegexRouterLinkActiveTest="^\/kunde\/\d*\/customer"
|
||||
(isActiveChange)="customerActive($event); focusSearchBox()"
|
||||
>
|
||||
<span class="side-menu-group-item-icon">
|
||||
<shared-icon icon="person"></shared-icon>
|
||||
</span>
|
||||
<span class="side-menu-group-item-label">Kunden</span>
|
||||
<button
|
||||
class="side-menu-group-arrow"
|
||||
[class.side-menu-item-rotate]="customerExpanded"
|
||||
(click)="
|
||||
$event.stopPropagation();
|
||||
$event.preventDefault();
|
||||
customerExpanded = !customerExpanded
|
||||
"
|
||||
>
|
||||
<shared-icon icon="keyboard-arrow-down"></shared-icon>
|
||||
</button>
|
||||
</a>
|
||||
}
|
||||
|
||||
<div class="side-menu-group-sub-items" [class.hidden]="!customerExpanded">
|
||||
@if (customerSearchRoute$ | async; as customerSearchRoute) {
|
||||
<a
|
||||
class="side-menu-group-item"
|
||||
(click)="closeSideMenu(); focusSearchBox()"
|
||||
[routerLink]="customerSearchRoute.path"
|
||||
[queryParams]="customerSearchRoute.queryParams"
|
||||
sharedRegexRouterLinkActive="active"
|
||||
sharedRegexRouterLinkActiveTest="^\/kunde\/\d*\/customer\/(\(search|search)"
|
||||
(isActiveChange)="focusSearchBox()"
|
||||
>
|
||||
<span class="side-menu-group-item-icon"></span>
|
||||
<span class="side-menu-group-item-label">Suchen</span>
|
||||
</a>
|
||||
}
|
||||
@if (customerCreateRoute$ | async; as customerCreateRoute) {
|
||||
<a
|
||||
class="side-menu-group-item"
|
||||
(click)="closeSideMenu()"
|
||||
[routerLink]="customerCreateRoute.path"
|
||||
[queryParams]="customerCreateRoute.queryParams"
|
||||
sharedRegexRouterLinkActive="active"
|
||||
sharedRegexRouterLinkActiveTest="^\/kunde\/\d*\/customer\/(\(create|create)"
|
||||
>
|
||||
<span class="side-menu-group-item-icon"></span>
|
||||
<span class="side-menu-group-item-label">Erfassen</span>
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a
|
||||
*ifRole="'Store'"
|
||||
class="side-menu-group-item"
|
||||
(click)="closeSideMenu(); focusSearchBox()"
|
||||
[routerLink]="pickUpShelfOutRoutePath$ | async"
|
||||
sharedRegexRouterLinkActive="active"
|
||||
sharedRegexRouterLinkActiveTest="^\/kunde\/\d*\/pickup-shelf"
|
||||
(isActiveChange)="focusSearchBox()"
|
||||
>
|
||||
<span class="side-menu-group-item-icon">
|
||||
<shared-icon icon="unarchive"></shared-icon>
|
||||
</span>
|
||||
<span class="side-menu-group-item-label">Warenausgabe</span>
|
||||
</a>
|
||||
<a
|
||||
*ifRole="'Store'"
|
||||
class="side-menu-group-item"
|
||||
(click)="closeSideMenu(); focusSearchBox()"
|
||||
[routerLink]="[
|
||||
'/',
|
||||
processService.activatedTab()?.id || processService.nextId(),
|
||||
'return',
|
||||
]"
|
||||
(isActiveChange)="focusSearchBox()"
|
||||
>
|
||||
<span class="side-menu-group-item-icon w-[2.375rem] h-12">
|
||||
<ng-icon name="isaNavigationReturn"></ng-icon>
|
||||
</span>
|
||||
<span class="side-menu-group-item-label">Retoure</span>
|
||||
</a>
|
||||
|
||||
<a
|
||||
*ifRole="'CallCenter'"
|
||||
class="side-menu-group-item"
|
||||
(click)="closeSideMenu(); resetBranch(); focusSearchBox()"
|
||||
[routerLink]="customerOrdersRoutePath$ | async"
|
||||
sharedRegexRouterLinkActive="active"
|
||||
sharedRegexRouterLinkActiveTest="^\/kunde\/\d*\/order"
|
||||
(isActiveChange)="focusSearchBox()"
|
||||
>
|
||||
<span class="side-menu-group-item-icon">
|
||||
<shared-icon icon="deployed-code"></shared-icon>
|
||||
</span>
|
||||
<span class="side-menu-group-item-label">Bestellungen</span>
|
||||
</a>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div class="side-menu-group" *ifRole="'Store'">
|
||||
<span class="side-menu-group-label">Filiale</span>
|
||||
<nav class="side-menu-group-nav">
|
||||
@if (taskCalenderNavigation$ | async; as taskCalenderNavigation) {
|
||||
<a
|
||||
class="side-menu-group-item"
|
||||
(click)="closeSideMenu(); focusSearchBox()"
|
||||
[routerLink]="taskCalenderNavigation.path"
|
||||
[queryParams]="taskCalenderNavigation.queryParams"
|
||||
routerLinkActive="active"
|
||||
(isActiveChange)="focusSearchBox()"
|
||||
>
|
||||
<span class="side-menu-group-item-icon">
|
||||
<shared-icon icon="event-available"></shared-icon>
|
||||
</span>
|
||||
<span class="side-menu-group-item-label">Kalender</span>
|
||||
</a>
|
||||
}
|
||||
@if (assortmentNavigation$ | async; as assortmentNavigation) {
|
||||
<a
|
||||
class="side-menu-group-item"
|
||||
(click)="closeSideMenu()"
|
||||
[routerLink]="assortmentNavigation.path"
|
||||
[queryParams]="assortmentNavigation.queryParams"
|
||||
routerLinkActive="active"
|
||||
>
|
||||
<span class="side-menu-group-item-icon">
|
||||
<shared-icon icon="shape-outline"></shared-icon>
|
||||
</span>
|
||||
<span class="side-menu-group-item-label">Sortiment</span>
|
||||
</a>
|
||||
}
|
||||
|
||||
<div class="side-menu-group-sub-item-wrapper">
|
||||
@if (pickUpShelfInRoutePath$ | async; as pickUpShelfInNavigation) {
|
||||
<a
|
||||
class="side-menu-group-item"
|
||||
(click)="closeSideMenu(); focusSearchBox()"
|
||||
[routerLink]="pickUpShelfInNavigation.path"
|
||||
[queryParams]="pickUpShelfInNavigation.queryParams"
|
||||
sharedRegexRouterLinkActive="active"
|
||||
sharedRegexRouterLinkActiveTest="^\/filiale\/(pickup-shelf|goods\/in)"
|
||||
(isActiveChange)="shelfActive($event); focusSearchBox()"
|
||||
>
|
||||
<span class="side-menu-group-item-icon">
|
||||
<shared-icon icon="isa-abholfach"></shared-icon>
|
||||
</span>
|
||||
<span class="side-menu-group-item-label">Abholfach</span>
|
||||
<button
|
||||
class="side-menu-group-arrow"
|
||||
[class.side-menu-item-rotate]="shelfExpanded"
|
||||
(click)="
|
||||
$event.stopPropagation();
|
||||
$event.preventDefault();
|
||||
shelfExpanded = !shelfExpanded
|
||||
"
|
||||
>
|
||||
<shared-icon icon="keyboard-arrow-down"></shared-icon>
|
||||
</button>
|
||||
</a>
|
||||
}
|
||||
|
||||
<div class="side-menu-group-sub-items" [class.hidden]="!shelfExpanded">
|
||||
@if (pickUpShelfInRoutePath$ | async; as pickUpShelfInListNavigation) {
|
||||
<a
|
||||
class="side-menu-group-item"
|
||||
(click)="closeSideMenu(); focusSearchBox()"
|
||||
[routerLink]="pickUpShelfInListNavigation.path"
|
||||
[queryParams]="pickUpShelfInListNavigation.queryParams"
|
||||
[class.has-child-view]="currentShelfView$ | async"
|
||||
sharedRegexRouterLinkActive="active"
|
||||
[sharedRegexRouterLinkActiveTest]="'^\/filiale\/pickup-shelf'"
|
||||
(isActiveChange)="shelfActive($event); focusSearchBox()"
|
||||
>
|
||||
<span class="side-menu-group-item-icon"></span>
|
||||
<span class="side-menu-group-item-label">Einbuchen</span>
|
||||
</a>
|
||||
}
|
||||
<a
|
||||
class="side-menu-group-item"
|
||||
(click)="closeSideMenu()"
|
||||
[routerLink]="['/filiale', 'goods', 'in', 'reservation']"
|
||||
[queryParams]="{ view: 'reservation' }"
|
||||
[class.active-child]="(currentShelfView$ | async) === 'reservation'"
|
||||
routerLinkActive="active"
|
||||
(isActiveChange)="shelfActive($event)"
|
||||
>
|
||||
<span class="side-menu-group-item-icon"></span>
|
||||
<span class="side-menu-group-item-label">Reservierung</span>
|
||||
</a>
|
||||
<a
|
||||
class="side-menu-group-item"
|
||||
(click)="closeSideMenu()"
|
||||
[routerLink]="['/filiale', 'goods', 'in', 'cleanup']"
|
||||
[queryParams]="{ view: 'cleanup' }"
|
||||
[class.active-child]="(currentShelfView$ | async) === 'cleanup'"
|
||||
routerLinkActive="active"
|
||||
(isActiveChange)="shelfActive($event)"
|
||||
>
|
||||
<span class="side-menu-group-item-icon"></span>
|
||||
<span class="side-menu-group-item-label">Ausräumen</span>
|
||||
</a>
|
||||
<a
|
||||
class="side-menu-group-item"
|
||||
(click)="closeSideMenu()"
|
||||
[routerLink]="['/filiale', 'goods', 'in', 'preview']"
|
||||
[queryParams]="{ view: 'remission' }"
|
||||
[class.active-child]="(currentShelfView$ | async) === 'remission'"
|
||||
routerLinkActive="active"
|
||||
(isActiveChange)="shelfActive($event)"
|
||||
>
|
||||
<span class="side-menu-group-item-icon"></span>
|
||||
<span class="side-menu-group-item-label">Remi-Vorschau</span>
|
||||
</a>
|
||||
<a
|
||||
class="side-menu-group-item"
|
||||
(click)="closeSideMenu()"
|
||||
[routerLink]="['/filiale', 'goods', 'in', 'list']"
|
||||
[queryParams]="{ view: 'wareneingangsliste' }"
|
||||
[class.active-child]="
|
||||
(currentShelfView$ | async) === 'wareneingangsliste'
|
||||
"
|
||||
routerLinkActive="active"
|
||||
(isActiveChange)="shelfActive($event)"
|
||||
>
|
||||
<span class="side-menu-group-item-icon"></span>
|
||||
<span class="side-menu-group-item-label">Fehlende</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (remissionNavigation$ | async; as remissionNavigation) {
|
||||
<a
|
||||
class="side-menu-group-item"
|
||||
(click)="closeSideMenu()"
|
||||
[routerLink]="remissionNavigation.path"
|
||||
[queryParams]="remissionNavigation.queryParams"
|
||||
routerLinkActive="active"
|
||||
>
|
||||
<span class="side-menu-group-item-icon">
|
||||
<shared-icon icon="assignment-return"></shared-icon>
|
||||
</span>
|
||||
<span class="side-menu-group-item-label">Remission</span>
|
||||
</a>
|
||||
}
|
||||
|
||||
@if (packageInspectionNavigation$ | async; as packageInspectionNavigation) {
|
||||
<a
|
||||
class="side-menu-group-item"
|
||||
(click)="closeSideMenu(); fetchAndOpenPackages()"
|
||||
[routerLink]="packageInspectionNavigation.path"
|
||||
[queryParams]="packageInspectionNavigation.queryParams"
|
||||
routerLinkActive="active"
|
||||
>
|
||||
<span class="side-menu-group-item-icon">
|
||||
<shared-icon icon="clipboard-check-outline"></shared-icon>
|
||||
</span>
|
||||
<span class="side-menu-group-item-label">Wareneingang</span>
|
||||
</a>
|
||||
}
|
||||
|
||||
<a
|
||||
class="side-menu-group-item"
|
||||
(click)="closeSideMenu(); focusSearchBox()"
|
||||
[routerLink]="[
|
||||
'/',
|
||||
processService.activatedTab()?.id || processService.nextId(),
|
||||
'remission',
|
||||
]"
|
||||
(isActiveChange)="focusSearchBox()"
|
||||
>
|
||||
<span class="side-menu-group-item-icon w-[2.375rem] h-12">
|
||||
<ng-icon name="isaNavigationRemission2"></ng-icon>
|
||||
</span>
|
||||
<span class="side-menu-group-item-label">Remission</span>
|
||||
</a>
|
||||
</nav>
|
||||
</div>
|
||||
<div class="side-menu-group">
|
||||
<span class="side-menu-group-label">Kunden</span>
|
||||
<nav class="side-menu-group-nav">
|
||||
<a
|
||||
class="side-menu-group-item"
|
||||
(click)="closeSideMenu(); resetBranch(); focusSearchBox()"
|
||||
[routerLink]="productRoutePath$ | async"
|
||||
sharedRegexRouterLinkActive="active"
|
||||
sharedRegexRouterLinkActiveTest="^\/kunde\/\d*\/product"
|
||||
(isActiveChange)="focusSearchBox()"
|
||||
>
|
||||
<div class="side-menu-group-item-icon">
|
||||
<shared-icon icon="import-contacts"></shared-icon>
|
||||
</div>
|
||||
<span class="side-menu-group-item-label">Artikelsuche</span>
|
||||
</a>
|
||||
|
||||
<div class="side-menu-group-sub-item-wrapper">
|
||||
@if (
|
||||
customerSearchRoute() || customerCreateRoute() || customerRewardRoute()
|
||||
) {
|
||||
<a
|
||||
class="side-menu-group-item"
|
||||
(click)="closeSideMenu(); focusSearchBox()"
|
||||
[routerLink]="customerSearchRoute().path"
|
||||
[queryParams]="customerSearchRoute().queryParams"
|
||||
sharedRegexRouterLinkActive="active"
|
||||
sharedRegexRouterLinkActiveTest="^(\/kunde\/\d*\/customer|\/\d*\/reward)"
|
||||
(isActiveChange)="customerActive($event); focusSearchBox()"
|
||||
>
|
||||
<span class="side-menu-group-item-icon">
|
||||
<shared-icon icon="person"></shared-icon>
|
||||
</span>
|
||||
<span class="side-menu-group-item-label">Kunden</span>
|
||||
<button
|
||||
class="side-menu-group-arrow"
|
||||
[class.side-menu-item-rotate]="customerExpanded()"
|
||||
(click)="
|
||||
$event.stopPropagation();
|
||||
$event.preventDefault();
|
||||
toggleCustomerExpanded()
|
||||
"
|
||||
>
|
||||
<shared-icon icon="keyboard-arrow-down"></shared-icon>
|
||||
</button>
|
||||
</a>
|
||||
}
|
||||
|
||||
<div
|
||||
class="side-menu-group-sub-items"
|
||||
[class.hidden]="!customerExpanded()"
|
||||
>
|
||||
@if (customerSearchRoute() || customerRewardRoute()) {
|
||||
<a
|
||||
class="side-menu-group-item"
|
||||
(click)="closeSideMenu(); focusSearchBox()"
|
||||
[routerLink]="customerSearchRoute().path"
|
||||
[queryParams]="customerSearchRoute().queryParams"
|
||||
sharedRegexRouterLinkActive="active"
|
||||
sharedRegexRouterLinkActiveTest="^\/kunde\/\d*\/customer\/(\(search|search)"
|
||||
(isActiveChange)="focusSearchBox()"
|
||||
>
|
||||
<span class="side-menu-group-item-icon"></span>
|
||||
<span class="side-menu-group-item-label">Suchen</span>
|
||||
</a>
|
||||
}
|
||||
@if (customerCreateRoute() || customerRewardRoute()) {
|
||||
<a
|
||||
class="side-menu-group-item"
|
||||
(click)="closeSideMenu()"
|
||||
[routerLink]="customerCreateRoute().path"
|
||||
[queryParams]="customerCreateRoute().queryParams"
|
||||
sharedRegexRouterLinkActive="active"
|
||||
sharedRegexRouterLinkActiveTest="^\/kunde\/\d*\/customer\/(\(create|create)"
|
||||
>
|
||||
<span class="side-menu-group-item-icon"></span>
|
||||
<span class="side-menu-group-item-label">Erfassen</span>
|
||||
</a>
|
||||
}
|
||||
@if (customerRewardRoute()) {
|
||||
<a
|
||||
class="side-menu-group-item"
|
||||
(click)="closeSideMenu(); focusSearchBox()"
|
||||
[routerLink]="customerRewardRoute()"
|
||||
(isActiveChange)="focusSearchBox()"
|
||||
sharedRegexRouterLinkActive="active"
|
||||
sharedRegexRouterLinkActiveTest="^\/\d*\/reward"
|
||||
>
|
||||
<span class="side-menu-group-item-icon"> </span>
|
||||
<span class="side-menu-group-item-label">Prämienshop</span>
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a
|
||||
*ifRole="'Store'"
|
||||
class="side-menu-group-item"
|
||||
(click)="closeSideMenu(); focusSearchBox()"
|
||||
[routerLink]="pickUpShelfOutRoutePath$ | async"
|
||||
sharedRegexRouterLinkActive="active"
|
||||
sharedRegexRouterLinkActiveTest="^\/kunde\/\d*\/pickup-shelf"
|
||||
(isActiveChange)="focusSearchBox()"
|
||||
>
|
||||
<span class="side-menu-group-item-icon">
|
||||
<shared-icon icon="unarchive"></shared-icon>
|
||||
</span>
|
||||
<span class="side-menu-group-item-label">Warenausgabe</span>
|
||||
</a>
|
||||
<a
|
||||
*ifRole="'Store'"
|
||||
class="side-menu-group-item"
|
||||
(click)="closeSideMenu(); focusSearchBox()"
|
||||
[routerLink]="['/', tabId(), 'return']"
|
||||
(isActiveChange)="focusSearchBox()"
|
||||
>
|
||||
<span class="side-menu-group-item-icon w-[2.375rem] h-12">
|
||||
<ng-icon name="isaNavigationReturn"></ng-icon>
|
||||
</span>
|
||||
<span class="side-menu-group-item-label">Retoure</span>
|
||||
</a>
|
||||
|
||||
<a
|
||||
*ifRole="'CallCenter'"
|
||||
class="side-menu-group-item"
|
||||
(click)="closeSideMenu(); resetBranch(); focusSearchBox()"
|
||||
[routerLink]="customerOrdersRoutePath$ | async"
|
||||
sharedRegexRouterLinkActive="active"
|
||||
sharedRegexRouterLinkActiveTest="^\/kunde\/\d*\/order"
|
||||
(isActiveChange)="focusSearchBox()"
|
||||
>
|
||||
<span class="side-menu-group-item-icon">
|
||||
<shared-icon icon="deployed-code"></shared-icon>
|
||||
</span>
|
||||
<span class="side-menu-group-item-label">Bestellungen</span>
|
||||
</a>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div class="side-menu-group" *ifRole="'Store'">
|
||||
<span class="side-menu-group-label">Filiale</span>
|
||||
<nav class="side-menu-group-nav">
|
||||
@if (taskCalenderNavigation$ | async; as taskCalenderNavigation) {
|
||||
<a
|
||||
class="side-menu-group-item"
|
||||
(click)="closeSideMenu(); focusSearchBox()"
|
||||
[routerLink]="taskCalenderNavigation.path"
|
||||
[queryParams]="taskCalenderNavigation.queryParams"
|
||||
routerLinkActive="active"
|
||||
(isActiveChange)="focusSearchBox()"
|
||||
>
|
||||
<span class="side-menu-group-item-icon">
|
||||
<shared-icon icon="event-available"></shared-icon>
|
||||
</span>
|
||||
<span class="side-menu-group-item-label">Kalender</span>
|
||||
</a>
|
||||
}
|
||||
@if (assortmentNavigation$ | async; as assortmentNavigation) {
|
||||
<a
|
||||
class="side-menu-group-item"
|
||||
(click)="closeSideMenu()"
|
||||
[routerLink]="assortmentNavigation.path"
|
||||
[queryParams]="assortmentNavigation.queryParams"
|
||||
routerLinkActive="active"
|
||||
>
|
||||
<span class="side-menu-group-item-icon">
|
||||
<shared-icon icon="shape-outline"></shared-icon>
|
||||
</span>
|
||||
<span class="side-menu-group-item-label">Sortiment</span>
|
||||
</a>
|
||||
}
|
||||
|
||||
<div class="side-menu-group-sub-item-wrapper">
|
||||
@if (pickUpShelfInRoutePath$ | async; as pickUpShelfInNavigation) {
|
||||
<a
|
||||
class="side-menu-group-item"
|
||||
(click)="closeSideMenu(); focusSearchBox()"
|
||||
[routerLink]="pickUpShelfInNavigation.path"
|
||||
[queryParams]="pickUpShelfInNavigation.queryParams"
|
||||
sharedRegexRouterLinkActive="active"
|
||||
sharedRegexRouterLinkActiveTest="^\/filiale\/(pickup-shelf|goods\/in)"
|
||||
(isActiveChange)="shelfActive($event); focusSearchBox()"
|
||||
>
|
||||
<span class="side-menu-group-item-icon">
|
||||
<shared-icon icon="isa-abholfach"></shared-icon>
|
||||
</span>
|
||||
<span class="side-menu-group-item-label">Abholfach</span>
|
||||
<button
|
||||
class="side-menu-group-arrow"
|
||||
[class.side-menu-item-rotate]="shelfExpanded"
|
||||
(click)="
|
||||
$event.stopPropagation();
|
||||
$event.preventDefault();
|
||||
shelfExpanded = !shelfExpanded
|
||||
"
|
||||
>
|
||||
<shared-icon icon="keyboard-arrow-down"></shared-icon>
|
||||
</button>
|
||||
</a>
|
||||
}
|
||||
|
||||
<div class="side-menu-group-sub-items" [class.hidden]="!shelfExpanded">
|
||||
@if (pickUpShelfInRoutePath$ | async; as pickUpShelfInListNavigation) {
|
||||
<a
|
||||
class="side-menu-group-item"
|
||||
(click)="closeSideMenu(); focusSearchBox()"
|
||||
[routerLink]="pickUpShelfInListNavigation.path"
|
||||
[queryParams]="pickUpShelfInListNavigation.queryParams"
|
||||
[class.has-child-view]="currentShelfView$ | async"
|
||||
sharedRegexRouterLinkActive="active"
|
||||
[sharedRegexRouterLinkActiveTest]="'^\/filiale\/pickup-shelf'"
|
||||
(isActiveChange)="shelfActive($event); focusSearchBox()"
|
||||
>
|
||||
<span class="side-menu-group-item-icon"></span>
|
||||
<span class="side-menu-group-item-label">Einbuchen</span>
|
||||
</a>
|
||||
}
|
||||
<a
|
||||
class="side-menu-group-item"
|
||||
(click)="closeSideMenu()"
|
||||
[routerLink]="['/filiale', 'goods', 'in', 'reservation']"
|
||||
[queryParams]="{ view: 'reservation' }"
|
||||
[class.active-child]="(currentShelfView$ | async) === 'reservation'"
|
||||
routerLinkActive="active"
|
||||
(isActiveChange)="shelfActive($event)"
|
||||
>
|
||||
<span class="side-menu-group-item-icon"></span>
|
||||
<span class="side-menu-group-item-label">Reservierung</span>
|
||||
</a>
|
||||
<a
|
||||
class="side-menu-group-item"
|
||||
(click)="closeSideMenu()"
|
||||
[routerLink]="['/filiale', 'goods', 'in', 'cleanup']"
|
||||
[queryParams]="{ view: 'cleanup' }"
|
||||
[class.active-child]="(currentShelfView$ | async) === 'cleanup'"
|
||||
routerLinkActive="active"
|
||||
(isActiveChange)="shelfActive($event)"
|
||||
>
|
||||
<span class="side-menu-group-item-icon"></span>
|
||||
<span class="side-menu-group-item-label">Ausräumen</span>
|
||||
</a>
|
||||
<a
|
||||
class="side-menu-group-item"
|
||||
(click)="closeSideMenu()"
|
||||
[routerLink]="['/filiale', 'goods', 'in', 'preview']"
|
||||
[queryParams]="{ view: 'remission' }"
|
||||
[class.active-child]="(currentShelfView$ | async) === 'remission'"
|
||||
routerLinkActive="active"
|
||||
(isActiveChange)="shelfActive($event)"
|
||||
>
|
||||
<span class="side-menu-group-item-icon"></span>
|
||||
<span class="side-menu-group-item-label">Remi-Vorschau</span>
|
||||
</a>
|
||||
<a
|
||||
class="side-menu-group-item"
|
||||
(click)="closeSideMenu()"
|
||||
[routerLink]="['/filiale', 'goods', 'in', 'list']"
|
||||
[queryParams]="{ view: 'wareneingangsliste' }"
|
||||
[class.active-child]="
|
||||
(currentShelfView$ | async) === 'wareneingangsliste'
|
||||
"
|
||||
routerLinkActive="active"
|
||||
(isActiveChange)="shelfActive($event)"
|
||||
>
|
||||
<span class="side-menu-group-item-icon"></span>
|
||||
<span class="side-menu-group-item-label">Fehlende</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (remissionNavigation$ | async; as remissionNavigation) {
|
||||
<a
|
||||
class="side-menu-group-item"
|
||||
(click)="closeSideMenu()"
|
||||
[routerLink]="remissionNavigation.path"
|
||||
[queryParams]="remissionNavigation.queryParams"
|
||||
routerLinkActive="active"
|
||||
>
|
||||
<span class="side-menu-group-item-icon">
|
||||
<shared-icon icon="assignment-return"></shared-icon>
|
||||
</span>
|
||||
<span class="side-menu-group-item-label">Remission</span>
|
||||
</a>
|
||||
}
|
||||
|
||||
@if (packageInspectionNavigation$ | async; as packageInspectionNavigation) {
|
||||
<a
|
||||
class="side-menu-group-item"
|
||||
(click)="closeSideMenu(); fetchAndOpenPackages()"
|
||||
[routerLink]="packageInspectionNavigation.path"
|
||||
[queryParams]="packageInspectionNavigation.queryParams"
|
||||
routerLinkActive="active"
|
||||
>
|
||||
<span class="side-menu-group-item-icon">
|
||||
<shared-icon icon="clipboard-check-outline"></shared-icon>
|
||||
</span>
|
||||
<span class="side-menu-group-item-label">Wareneingang</span>
|
||||
</a>
|
||||
}
|
||||
<div class="side-menu-group-sub-item-wrapper">
|
||||
<a
|
||||
class="side-menu-group-item"
|
||||
(click)="closeSideMenu(); focusSearchBox()"
|
||||
[routerLink]="[
|
||||
'/',
|
||||
tabId(),
|
||||
'remission',
|
||||
]"
|
||||
(isActiveChange)="focusSearchBox(); remissionExpanded.set($event)"
|
||||
routerLinkActive="active"
|
||||
#rlActive="routerLinkActive"
|
||||
>
|
||||
<span class="side-menu-group-item-icon w-[2.375rem] h-12">
|
||||
<ng-icon name="isaNavigationRemission2" size="1.5rem"></ng-icon>
|
||||
</span>
|
||||
<span class="side-menu-group-item-label">Remission</span>
|
||||
<button
|
||||
class="side-menu-group-arrow"
|
||||
[class.side-menu-item-rotate]="remissionExpanded()"
|
||||
(click)="
|
||||
$event.stopPropagation();
|
||||
$event.preventDefault();
|
||||
remissionExpanded.set(!remissionExpanded())
|
||||
"
|
||||
>
|
||||
<shared-icon icon="keyboard-arrow-down"></shared-icon>
|
||||
</button>
|
||||
</a>
|
||||
@if (remissionExpanded()) {
|
||||
<div class="side-menu-group-sub-items">
|
||||
<a
|
||||
class="side-menu-group-item"
|
||||
(click)="closeSideMenu(); focusSearchBox()"
|
||||
[routerLink]="['/', tabId(), 'remission']"
|
||||
(isActiveChange)="focusSearchBox()"
|
||||
sharedRegexRouterLinkActive="active"
|
||||
sharedRegexRouterLinkActiveTest="^\/\d*\/remission\/(mandatory|department)"
|
||||
>
|
||||
<span class="side-menu-group-item-icon"> </span>
|
||||
<span class="side-menu-group-item-label">Remission</span>
|
||||
</a>
|
||||
<a
|
||||
class="side-menu-group-item"
|
||||
(click)="closeSideMenu(); focusSearchBox()"
|
||||
[routerLink]="['/', tabId(), 'remission', 'return-receipt']"
|
||||
(isActiveChange)="focusSearchBox()"
|
||||
sharedRegexRouterLinkActive="active"
|
||||
sharedRegexRouterLinkActiveTest="^\/\d*\/remission\/return-receipt"
|
||||
>
|
||||
<span class="side-menu-group-item-icon"> </span>
|
||||
<span class="side-menu-group-item-label">Warenbegleitscheine</span>
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
@@ -1,435 +1,447 @@
|
||||
import {
|
||||
Component,
|
||||
ChangeDetectionStrategy,
|
||||
Inject,
|
||||
ChangeDetectorRef,
|
||||
inject,
|
||||
DOCUMENT,
|
||||
} from '@angular/core';
|
||||
import { AuthModule, AuthService } from '@core/auth';
|
||||
import { StockService } from '@generated/swagger/wws-api';
|
||||
import { first, map, retry, switchMap, take } from 'rxjs/operators';
|
||||
import { ShellService } from '../shell.service';
|
||||
import { ApplicationProcess, ApplicationService } from '@core/application';
|
||||
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
|
||||
import { EnvironmentService } from '@core/environment';
|
||||
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Config } from '@core/config';
|
||||
import { BreadcrumbService } from '@core/breadcrumb';
|
||||
import { IconComponent } from '@shared/components/icon';
|
||||
import { RegexRouterLinkActiveDirective } from '@shared/directives/router-link-active';
|
||||
import { WrongDestinationModalService } from '@modal/wrong-destination';
|
||||
import {
|
||||
CustomerCreateNavigation,
|
||||
CustomerOrdersNavigationService,
|
||||
CustomerSearchNavigation,
|
||||
PickupShelfInNavigationService,
|
||||
PickUpShelfOutNavigationService,
|
||||
ProductCatalogNavigationService,
|
||||
} from '@shared/services/navigation';
|
||||
|
||||
import { TabService } from '@isa/core/tabs';
|
||||
import { NgIconComponent, provideIcons } from '@ng-icons/core';
|
||||
import { isaNavigationRemission2, isaNavigationReturn } from '@isa/icons';
|
||||
|
||||
@Component({
|
||||
selector: 'shell-side-menu',
|
||||
templateUrl: 'side-menu.component.html',
|
||||
styleUrls: ['side-menu.component.css'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [
|
||||
CommonModule,
|
||||
NgIconComponent,
|
||||
IconComponent,
|
||||
RouterModule,
|
||||
AuthModule,
|
||||
RegexRouterLinkActiveDirective,
|
||||
],
|
||||
providers: [provideIcons({ isaNavigationReturn, isaNavigationRemission2 })],
|
||||
})
|
||||
export class ShellSideMenuComponent {
|
||||
processService = inject(TabService);
|
||||
|
||||
branchKey$ = this._stockService.StockCurrentBranch().pipe(
|
||||
retry(3),
|
||||
map((x) => x.result.key),
|
||||
);
|
||||
|
||||
section$ = this._app.getSection$();
|
||||
|
||||
processes$ = this.section$.pipe(
|
||||
switchMap((section) => this._app.getProcesses$(section)),
|
||||
);
|
||||
|
||||
processesCount$ = this.processes$.pipe(
|
||||
map((processes) => processes?.length ?? 0),
|
||||
);
|
||||
|
||||
activeProcess$ = this._app.activatedProcessId$.pipe(
|
||||
switchMap((processId) => this._app.getProcessById$(processId)),
|
||||
);
|
||||
|
||||
get isTablet() {
|
||||
return this._environment.matchTablet();
|
||||
}
|
||||
|
||||
customerBasePath$ = this.activeProcess$.pipe(
|
||||
map((process) => {
|
||||
if (
|
||||
!!process &&
|
||||
process.section === 'customer' &&
|
||||
process.type !== 'cart-checkout'
|
||||
) {
|
||||
// Übernehme aktiven Prozess
|
||||
return `/kunde/${process.id}`;
|
||||
} else {
|
||||
// Über Guards wird ein neuer Prozess erstellt
|
||||
return '/kunde';
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
customerSearchRoute$ = this.getLastActivatedCustomerProcessId$().pipe(
|
||||
map((processId) => {
|
||||
return this._customerSearchNavigation.defaultRoute({ processId });
|
||||
}),
|
||||
);
|
||||
|
||||
customerCreateRoute$ = this.getLastActivatedCustomerProcessId$().pipe(
|
||||
map((processId) => {
|
||||
return this._customerCreateNavigation.defaultRoute({ processId });
|
||||
}),
|
||||
);
|
||||
|
||||
pickUpShelfOutRoutePath$ = this.getLastActivatedCustomerProcessId$().pipe(
|
||||
map((processId) => {
|
||||
if (processId) {
|
||||
// Übernehme aktiven Prozess
|
||||
return this._pickUpShelfOutNavigation.defaultRoute({ processId }).path;
|
||||
} else {
|
||||
// Über Guards wird ein neuer Prozess erstellt
|
||||
return this._pickUpShelfOutNavigation.defaultRoute({}).path;
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
productRoutePath$ = this.getLastActivatedCustomerProcessId$().pipe(
|
||||
map((processId) => {
|
||||
if (processId) {
|
||||
// Übernehme aktiven Prozess
|
||||
return this._catalogNavigationService.getArticleSearchBasePath(
|
||||
processId,
|
||||
).path;
|
||||
} else {
|
||||
// Über Guards wird ein neuer Prozess erstellt
|
||||
return this._catalogNavigationService.getArticleSearchBasePath().path;
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
customerOrdersRoutePath$ = this.getLastActivatedCustomerProcessId$().pipe(
|
||||
map((processId) => {
|
||||
if (processId) {
|
||||
// Übernehme aktiven Prozess
|
||||
return this._customerOrdersNavigationService.getCustomerOrdersBasePath(
|
||||
processId,
|
||||
).path;
|
||||
} else {
|
||||
// Über Guards wird ein neuer Prozess erstellt
|
||||
return this._customerOrdersNavigationService.getCustomerOrdersBasePath()
|
||||
.path;
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
taskCalenderNavigation$ = this.getLastNavigationByProcessId(
|
||||
this._config.get('process.ids.taskCalendar'),
|
||||
{
|
||||
path: ['/filiale', 'task-calendar'],
|
||||
queryParams: {},
|
||||
},
|
||||
'/filiale/task-calendar',
|
||||
);
|
||||
|
||||
assortmentNavigation$ = this.getLastNavigationByProcessId(
|
||||
this._config.get('process.ids.assortment'),
|
||||
{
|
||||
path: ['/filiale', 'assortment'],
|
||||
queryParams: {},
|
||||
},
|
||||
);
|
||||
|
||||
pickUpShelfInRoutePath$ = this.getLastNavigationByProcessId(
|
||||
this._config.get('process.ids.pickupShelf'),
|
||||
this._pickUpShelfInNavigation.defaultRoute(),
|
||||
'/filiale/pickup-shelf',
|
||||
);
|
||||
|
||||
// #4478 - RD // Abholfach - Routing löst Suche aus
|
||||
// pickUpShelfInListRoutePath$ = this.getLastNavigationByProcessId(
|
||||
// this._config.get('process.ids.pickupShelf'),
|
||||
// this._pickUpShelfInNavigation.listRoute()
|
||||
// );
|
||||
|
||||
remissionNavigation$ = this.getLastNavigationByProcessId(
|
||||
this._config.get('process.ids.remission'),
|
||||
{
|
||||
path: ['/filiale', 'remission'],
|
||||
queryParams: {},
|
||||
},
|
||||
);
|
||||
|
||||
packageInspectionNavigation$ = this.getLastNavigationByProcessId(
|
||||
this._config.get('process.ids.packageInspection'),
|
||||
{
|
||||
path: ['/filiale', 'package-inspection'],
|
||||
queryParams: {},
|
||||
},
|
||||
);
|
||||
|
||||
get currentShelfView$() {
|
||||
return this._route.queryParams.pipe(map((params) => params.view));
|
||||
}
|
||||
|
||||
shelfExpanded = false;
|
||||
customerExpanded = false;
|
||||
|
||||
constructor(
|
||||
private _shellService: ShellService,
|
||||
private _authService: AuthService,
|
||||
private _stockService: StockService,
|
||||
private _app: ApplicationService,
|
||||
private _router: Router,
|
||||
private _route: ActivatedRoute,
|
||||
private readonly _wrongDestinationModalService: WrongDestinationModalService,
|
||||
private _environment: EnvironmentService,
|
||||
private _catalogNavigationService: ProductCatalogNavigationService,
|
||||
private _customerOrdersNavigationService: CustomerOrdersNavigationService,
|
||||
private _config: Config,
|
||||
private _breadcrumbService: BreadcrumbService,
|
||||
private _customerSearchNavigation: CustomerSearchNavigation,
|
||||
private _customerCreateNavigation: CustomerCreateNavigation,
|
||||
private _pickUpShelfOutNavigation: PickUpShelfOutNavigationService,
|
||||
private _pickUpShelfInNavigation: PickupShelfInNavigationService,
|
||||
private _cdr: ChangeDetectorRef,
|
||||
@Inject(DOCUMENT) private readonly _document: Document,
|
||||
) {}
|
||||
|
||||
customerActive(isActive: boolean) {
|
||||
if (isActive) {
|
||||
this.expandCustomer();
|
||||
}
|
||||
}
|
||||
|
||||
shelfActive(isActive: boolean) {
|
||||
if (isActive) {
|
||||
this.expandShelf();
|
||||
}
|
||||
}
|
||||
|
||||
expandCustomer() {
|
||||
this.customerExpanded = true;
|
||||
this._cdr.markForCheck();
|
||||
}
|
||||
|
||||
expandShelf() {
|
||||
this.shelfExpanded = true;
|
||||
this._cdr.markForCheck();
|
||||
}
|
||||
|
||||
getLastNavigationByProcessId(
|
||||
id: number,
|
||||
fallback?: { path: string[]; queryParams: unknown },
|
||||
pathContainsString?: string,
|
||||
) {
|
||||
return this._breadcrumbService.getBreadcrumbByKey$(id)?.pipe(
|
||||
map((breadcrumbs) => {
|
||||
const lastCrumb = breadcrumbs
|
||||
.filter((breadcrumb) => {
|
||||
/**
|
||||
* #4532 - Der optionale Filter wurde hinzugefügt Breadcrumbs mit fehlerhaften Pfad auszuschließen.
|
||||
* Dieser Filter kann entfernt werden, sobald die Breadcrumbs korrekt gesetzt werden. Jedoch konnte man bisher nicht feststellen,
|
||||
* woher die fehlerhaften Breadcrumbs kommen.
|
||||
*/
|
||||
if (!pathContainsString) {
|
||||
// Wenn kein Filter gesetzt ist, dann wird der letzte Breadcrumb zurückgegeben
|
||||
return true;
|
||||
}
|
||||
|
||||
const pathStr = Array.isArray(breadcrumb.path)
|
||||
? breadcrumb.path.join('/')
|
||||
: breadcrumb.path;
|
||||
return pathStr.includes(pathContainsString);
|
||||
})
|
||||
// eslint-disable-next-line no-prototype-builtins
|
||||
.filter((breadcrumb) => !breadcrumb?.params?.hasOwnProperty('view'))
|
||||
.filter((breadcrumb) => !breadcrumb?.tags?.includes('reservation'))
|
||||
.filter((breadcrumb) => !breadcrumb?.tags?.includes('cleanup'))
|
||||
.filter(
|
||||
(breadcrumb) => !breadcrumb?.tags?.includes('wareneingangsliste'),
|
||||
)
|
||||
.filter((breadcrumb) => !breadcrumb?.tags?.includes('preview'))
|
||||
.reduce((last, current) => {
|
||||
if (!last) return current;
|
||||
|
||||
if (last.changed > current.changed) {
|
||||
return last;
|
||||
} else {
|
||||
return current;
|
||||
}
|
||||
}, undefined);
|
||||
|
||||
if (!lastCrumb) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
// #4692 Return Fallback if Values contain undefined or null values, regardless if path is from type string or array
|
||||
if (typeof lastCrumb?.path === 'string') {
|
||||
if (
|
||||
lastCrumb?.path?.includes('undefined') ||
|
||||
lastCrumb?.path?.includes('null')
|
||||
) {
|
||||
return fallback;
|
||||
}
|
||||
} else {
|
||||
const valuesToCheck = [];
|
||||
|
||||
// eslint-disable-next-line no-unsafe-optional-chaining
|
||||
for (const value of lastCrumb?.path) {
|
||||
if (
|
||||
value?.outlets &&
|
||||
value?.outlets?.primary &&
|
||||
value?.outlets?.side
|
||||
) {
|
||||
valuesToCheck.push(
|
||||
...Object.values(value?.outlets?.primary),
|
||||
...Object.values(value?.outlets?.side),
|
||||
);
|
||||
} else {
|
||||
valuesToCheck.push(value);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.checkIfArrayContainsUndefinedOrNull(valuesToCheck)) {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
return { path: lastCrumb.path, queryParams: lastCrumb.params };
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
checkIfArrayContainsUndefinedOrNull(array: unknown[]) {
|
||||
return (
|
||||
array?.includes(undefined) ||
|
||||
array?.includes('undefined') ||
|
||||
array?.includes(null) ||
|
||||
array?.includes('null')
|
||||
);
|
||||
}
|
||||
|
||||
getLastActivatedCustomerProcessId$() {
|
||||
return this._app.getProcesses$('customer').pipe(
|
||||
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() {
|
||||
this._shellService.closeSideMenu();
|
||||
}
|
||||
|
||||
logout() {
|
||||
this._authService.logout();
|
||||
}
|
||||
|
||||
async resetBranch() {
|
||||
const process = await this.activeProcess$.pipe(first()).toPromise();
|
||||
if (process?.id) {
|
||||
this._app.patchProcessData(process.id, { selectedBranch: undefined });
|
||||
}
|
||||
}
|
||||
|
||||
focusSearchBox() {
|
||||
setTimeout(() => this._document.getElementById('searchbox')?.focus(), 0);
|
||||
}
|
||||
|
||||
async createProcess() {
|
||||
const process = await this.createCartProcess();
|
||||
this.navigateToCatalog(process);
|
||||
}
|
||||
|
||||
async createCartProcess() {
|
||||
const nextProcessName = await this.getNextProcessName();
|
||||
|
||||
const process: ApplicationProcess = {
|
||||
id: this.getNextProcessId(),
|
||||
type: 'cart',
|
||||
name: nextProcessName,
|
||||
section: 'customer',
|
||||
closeable: true,
|
||||
};
|
||||
|
||||
this._app.createProcess(process);
|
||||
|
||||
return process;
|
||||
}
|
||||
|
||||
async getNextProcessName() {
|
||||
let processes = await this._app
|
||||
.getProcesses$('customer')
|
||||
.pipe(first())
|
||||
.toPromise();
|
||||
|
||||
processes = processes.filter(
|
||||
(x) => x.type === 'cart' && x.name.startsWith('Vorgang '),
|
||||
);
|
||||
|
||||
const maxProcessNumber = processes.reduce((max, process) => {
|
||||
const number = parseInt(process.name.replace('Vorgang ', ''), 10);
|
||||
return number > max ? number : max;
|
||||
}, 0);
|
||||
|
||||
return `Vorgang ${maxProcessNumber + 1}`;
|
||||
}
|
||||
|
||||
getNextProcessId() {
|
||||
return Date.now();
|
||||
}
|
||||
|
||||
async navigateToCatalog(process: ApplicationProcess) {
|
||||
await this._catalogNavigationService
|
||||
.getArticleSearchBasePath(process.id)
|
||||
.navigate();
|
||||
}
|
||||
|
||||
navigateToDashboard() {
|
||||
this._router.navigate(['/kunde', 'dashboard']);
|
||||
}
|
||||
|
||||
async closeAllProcesses() {
|
||||
const processes = await this.processes$.pipe(take(1)).toPromise();
|
||||
|
||||
processes.forEach((process) => this._app.removeProcess(process.id));
|
||||
|
||||
this.navigateToDashboard();
|
||||
}
|
||||
|
||||
fetchAndOpenPackages = () =>
|
||||
this._wrongDestinationModalService.fetchAndOpen();
|
||||
}
|
||||
import {
|
||||
Component,
|
||||
ChangeDetectionStrategy,
|
||||
computed,
|
||||
ChangeDetectorRef,
|
||||
inject,
|
||||
DOCUMENT,
|
||||
signal,
|
||||
} from '@angular/core';
|
||||
import { AuthModule, AuthService } from '@core/auth';
|
||||
import { StockService } from '@generated/swagger/wws-api';
|
||||
import { first, map, retry, switchMap, take } from 'rxjs/operators';
|
||||
import { ShellService } from '../shell.service';
|
||||
import { ApplicationProcess, ApplicationService } from '@core/application';
|
||||
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
|
||||
import { EnvironmentService } from '@core/environment';
|
||||
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Config } from '@core/config';
|
||||
import { BreadcrumbService } from '@core/breadcrumb';
|
||||
import { IconComponent } from '@shared/components/icon';
|
||||
import { RegexRouterLinkActiveDirective } from '@shared/directives/router-link-active';
|
||||
import { WrongDestinationModalService } from '@modal/wrong-destination';
|
||||
import {
|
||||
CustomerCreateNavigation,
|
||||
CustomerOrdersNavigationService,
|
||||
CustomerSearchNavigation,
|
||||
PickupShelfInNavigationService,
|
||||
PickUpShelfOutNavigationService,
|
||||
ProductCatalogNavigationService,
|
||||
} from '@shared/services/navigation';
|
||||
import { toObservable, toSignal } from '@angular/core/rxjs-interop';
|
||||
|
||||
import { TabService } from '@isa/core/tabs';
|
||||
import { NgIconComponent, provideIcons } from '@ng-icons/core';
|
||||
import { isaNavigationRemission2, isaNavigationReturn } from '@isa/icons';
|
||||
import z from 'zod';
|
||||
|
||||
@Component({
|
||||
selector: 'shell-side-menu',
|
||||
templateUrl: 'side-menu.component.html',
|
||||
styleUrls: ['side-menu.component.css'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [
|
||||
CommonModule,
|
||||
NgIconComponent,
|
||||
IconComponent,
|
||||
RouterModule,
|
||||
AuthModule,
|
||||
RegexRouterLinkActiveDirective,
|
||||
],
|
||||
providers: [provideIcons({ isaNavigationReturn, isaNavigationRemission2 })],
|
||||
})
|
||||
export class ShellSideMenuComponent {
|
||||
#shellService = inject(ShellService);
|
||||
#authService = inject(AuthService);
|
||||
#stockService = inject(StockService);
|
||||
#app = inject(ApplicationService);
|
||||
#router = inject(Router);
|
||||
#route = inject(ActivatedRoute);
|
||||
#wrongDestinationModalService = inject(WrongDestinationModalService);
|
||||
#environment = inject(EnvironmentService);
|
||||
#catalogNavigationService = inject(ProductCatalogNavigationService);
|
||||
#customerOrdersNavigationService = inject(CustomerOrdersNavigationService);
|
||||
#config = inject(Config);
|
||||
#breadcrumbService = inject(BreadcrumbService);
|
||||
#customerSearchNavigation = inject(CustomerSearchNavigation);
|
||||
#customerCreateNavigation = inject(CustomerCreateNavigation);
|
||||
#pickUpShelfOutNavigation = inject(PickUpShelfOutNavigationService);
|
||||
#pickUpShelfInNavigation = inject(PickupShelfInNavigationService);
|
||||
#cdr = inject(ChangeDetectorRef);
|
||||
#document = inject(DOCUMENT);
|
||||
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(
|
||||
retry(3),
|
||||
map((x) => x.result.key),
|
||||
);
|
||||
|
||||
section$ = this.#app.getSection$();
|
||||
|
||||
processes$ = this.section$.pipe(
|
||||
switchMap((section) => this.#app.getProcesses$(section)),
|
||||
);
|
||||
|
||||
processesCount$ = this.processes$.pipe(
|
||||
map((processes) => processes?.length ?? 0),
|
||||
);
|
||||
|
||||
activeProcess$ = this.#app.activatedProcessId$.pipe(
|
||||
switchMap((processId) => this.#app.getProcessById$(processId)),
|
||||
);
|
||||
|
||||
get isTablet() {
|
||||
return this.#environment.matchTablet();
|
||||
}
|
||||
|
||||
nextId() {
|
||||
return Date.now();
|
||||
}
|
||||
|
||||
customerBasePath$ = this.activeProcess$.pipe(
|
||||
map((process) => {
|
||||
if (
|
||||
!!process &&
|
||||
process.section === 'customer' &&
|
||||
process.type !== 'cart-checkout'
|
||||
) {
|
||||
// Übernehme aktiven Prozess
|
||||
return `/kunde/${process.id}`;
|
||||
} else {
|
||||
// Über Guards wird ein neuer Prozess erstellt
|
||||
return '/kunde';
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
customerSearchRoute = toSignal(
|
||||
this.getLastActivatedCustomerProcessId$().pipe(
|
||||
map((processId) => {
|
||||
return this.#customerSearchNavigation.defaultRoute({ processId });
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
customerCreateRoute = toSignal(
|
||||
this.getLastActivatedCustomerProcessId$().pipe(
|
||||
map((processId) => {
|
||||
return this.#customerCreateNavigation.defaultRoute({ processId });
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
customerRewardRoute = computed(() => {
|
||||
const routeName = 'reward';
|
||||
const tabId = this.tabService.activatedTab()?.id;
|
||||
return this.#router.createUrlTree(['/', tabId || this.nextId(), routeName]);
|
||||
});
|
||||
|
||||
pickUpShelfOutRoutePath$ = this.getLastActivatedCustomerProcessId$().pipe(
|
||||
map((processId) => {
|
||||
if (processId) {
|
||||
// Übernehme aktiven Prozess
|
||||
return this.#pickUpShelfOutNavigation.defaultRoute({ processId }).path;
|
||||
} else {
|
||||
// Über Guards wird ein neuer Prozess erstellt
|
||||
return this.#pickUpShelfOutNavigation.defaultRoute({}).path;
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
productRoutePath$ = this.getLastActivatedCustomerProcessId$().pipe(
|
||||
map((processId) => {
|
||||
if (processId) {
|
||||
// Übernehme aktiven Prozess
|
||||
return this.#catalogNavigationService.getArticleSearchBasePath(
|
||||
processId,
|
||||
).path;
|
||||
} else {
|
||||
// Über Guards wird ein neuer Prozess erstellt
|
||||
return this.#catalogNavigationService.getArticleSearchBasePath().path;
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
customerOrdersRoutePath$ = this.getLastActivatedCustomerProcessId$().pipe(
|
||||
map((processId) => {
|
||||
if (processId) {
|
||||
// Übernehme aktiven Prozess
|
||||
return this.#customerOrdersNavigationService.getCustomerOrdersBasePath(
|
||||
processId,
|
||||
).path;
|
||||
} else {
|
||||
// Über Guards wird ein neuer Prozess erstellt
|
||||
return this.#customerOrdersNavigationService.getCustomerOrdersBasePath()
|
||||
.path;
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
taskCalenderNavigation$ = this.getLastNavigationByProcessId(
|
||||
this.#config.get('process.ids.taskCalendar'),
|
||||
{
|
||||
path: ['/filiale', 'task-calendar'],
|
||||
queryParams: {},
|
||||
},
|
||||
'/filiale/task-calendar',
|
||||
);
|
||||
|
||||
assortmentNavigation$ = this.getLastNavigationByProcessId(
|
||||
this.#config.get('process.ids.assortment'),
|
||||
{
|
||||
path: ['/filiale', 'assortment'],
|
||||
queryParams: {},
|
||||
},
|
||||
);
|
||||
|
||||
pickUpShelfInRoutePath$ = this.getLastNavigationByProcessId(
|
||||
this.#config.get('process.ids.pickupShelf'),
|
||||
this.#pickUpShelfInNavigation.defaultRoute(),
|
||||
'/filiale/pickup-shelf',
|
||||
);
|
||||
|
||||
// #4478 - RD // Abholfach - Routing löst Suche aus
|
||||
// pickUpShelfInListRoutePath$ = this.getLastNavigationByProcessId(
|
||||
// this._config.get('process.ids.pickupShelf'),
|
||||
// this._pickUpShelfInNavigation.listRoute()
|
||||
// );
|
||||
|
||||
remissionNavigation$ = this.getLastNavigationByProcessId(
|
||||
this.#config.get('process.ids.remission'),
|
||||
{
|
||||
path: ['/filiale', 'remission'],
|
||||
queryParams: {},
|
||||
},
|
||||
);
|
||||
|
||||
packageInspectionNavigation$ = this.getLastNavigationByProcessId(
|
||||
this.#config.get('process.ids.packageInspection'),
|
||||
{
|
||||
path: ['/filiale', 'package-inspection'],
|
||||
queryParams: {},
|
||||
},
|
||||
);
|
||||
|
||||
get currentShelfView$() {
|
||||
return this.#route.queryParams.pipe(map((params) => params.view));
|
||||
}
|
||||
|
||||
shelfExpanded = false;
|
||||
customerExpanded = signal(false);
|
||||
remissionExpanded = signal(false);
|
||||
|
||||
customerActive(isActive: boolean) {
|
||||
if (isActive) {
|
||||
this.customerExpanded.set(true);
|
||||
}
|
||||
}
|
||||
|
||||
toggleCustomerExpanded() {
|
||||
this.customerExpanded.set(!this.customerExpanded());
|
||||
}
|
||||
|
||||
shelfActive(isActive: boolean) {
|
||||
if (isActive) {
|
||||
this.expandShelf();
|
||||
}
|
||||
}
|
||||
|
||||
expandShelf() {
|
||||
this.shelfExpanded = true;
|
||||
this.#cdr.markForCheck();
|
||||
}
|
||||
|
||||
getLastNavigationByProcessId(
|
||||
id: number,
|
||||
fallback?: { path: string[]; queryParams: unknown },
|
||||
pathContainsString?: string,
|
||||
) {
|
||||
return this.#breadcrumbService.getBreadcrumbByKey$(id)?.pipe(
|
||||
map((breadcrumbs) => {
|
||||
const lastCrumb = breadcrumbs
|
||||
.filter((breadcrumb) => {
|
||||
/**
|
||||
* #4532 - Der optionale Filter wurde hinzugefügt Breadcrumbs mit fehlerhaften Pfad auszuschließen.
|
||||
* Dieser Filter kann entfernt werden, sobald die Breadcrumbs korrekt gesetzt werden. Jedoch konnte man bisher nicht feststellen,
|
||||
* woher die fehlerhaften Breadcrumbs kommen.
|
||||
*/
|
||||
if (!pathContainsString) {
|
||||
// Wenn kein Filter gesetzt ist, dann wird der letzte Breadcrumb zurückgegeben
|
||||
return true;
|
||||
}
|
||||
|
||||
const pathStr = Array.isArray(breadcrumb.path)
|
||||
? breadcrumb.path.join('/')
|
||||
: breadcrumb.path;
|
||||
return pathStr.includes(pathContainsString);
|
||||
})
|
||||
// eslint-disable-next-line no-prototype-builtins
|
||||
.filter((breadcrumb) => !breadcrumb?.params?.hasOwnProperty('view'))
|
||||
.filter((breadcrumb) => !breadcrumb?.tags?.includes('reservation'))
|
||||
.filter((breadcrumb) => !breadcrumb?.tags?.includes('cleanup'))
|
||||
.filter(
|
||||
(breadcrumb) => !breadcrumb?.tags?.includes('wareneingangsliste'),
|
||||
)
|
||||
.filter((breadcrumb) => !breadcrumb?.tags?.includes('preview'))
|
||||
.reduce((last, current) => {
|
||||
if (!last) return current;
|
||||
|
||||
if (last.changed > current.changed) {
|
||||
return last;
|
||||
} else {
|
||||
return current;
|
||||
}
|
||||
}, undefined);
|
||||
|
||||
if (!lastCrumb) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
// #4692 Return Fallback if Values contain undefined or null values, regardless if path is from type string or array
|
||||
if (typeof lastCrumb?.path === 'string') {
|
||||
if (
|
||||
lastCrumb?.path?.includes('undefined') ||
|
||||
lastCrumb?.path?.includes('null')
|
||||
) {
|
||||
return fallback;
|
||||
}
|
||||
} else {
|
||||
const valuesToCheck = [];
|
||||
|
||||
// eslint-disable-next-line no-unsafe-optional-chaining
|
||||
for (const value of lastCrumb?.path) {
|
||||
if (
|
||||
value?.outlets &&
|
||||
value?.outlets?.primary &&
|
||||
value?.outlets?.side
|
||||
) {
|
||||
valuesToCheck.push(
|
||||
...Object.values(value?.outlets?.primary),
|
||||
...Object.values(value?.outlets?.side),
|
||||
);
|
||||
} else {
|
||||
valuesToCheck.push(value);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.checkIfArrayContainsUndefinedOrNull(valuesToCheck)) {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
return { path: lastCrumb.path, queryParams: lastCrumb.params };
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
checkIfArrayContainsUndefinedOrNull(array: unknown[]) {
|
||||
return (
|
||||
array?.includes(undefined) ||
|
||||
array?.includes('undefined') ||
|
||||
array?.includes(null) ||
|
||||
array?.includes('null')
|
||||
);
|
||||
}
|
||||
|
||||
getLastActivatedCustomerProcessId$() {
|
||||
return this.tabId$;
|
||||
}
|
||||
|
||||
closeSideMenu() {
|
||||
this.#shellService.closeSideMenu();
|
||||
}
|
||||
|
||||
logout() {
|
||||
this.#authService.logout();
|
||||
}
|
||||
|
||||
async resetBranch() {
|
||||
const process = await this.activeProcess$.pipe(first()).toPromise();
|
||||
if (process?.id) {
|
||||
this.#app.patchProcessData(process.id, { selectedBranch: undefined });
|
||||
}
|
||||
}
|
||||
|
||||
focusSearchBox() {
|
||||
setTimeout(() => this.#document.getElementById('searchbox')?.focus(), 0);
|
||||
}
|
||||
|
||||
async createProcess() {
|
||||
const process = await this.createCartProcess();
|
||||
this.navigateToCatalog(process);
|
||||
}
|
||||
|
||||
async createCartProcess() {
|
||||
const nextProcessName = await this.getNextProcessName();
|
||||
|
||||
const process: ApplicationProcess = {
|
||||
id: this.getNextProcessId(),
|
||||
type: 'cart',
|
||||
name: nextProcessName,
|
||||
section: 'customer',
|
||||
closeable: true,
|
||||
};
|
||||
|
||||
this.#app.createProcess(process);
|
||||
|
||||
return process;
|
||||
}
|
||||
|
||||
async getNextProcessName() {
|
||||
let processes = await this.#app
|
||||
.getProcesses$('customer')
|
||||
.pipe(first())
|
||||
.toPromise();
|
||||
|
||||
processes = processes.filter(
|
||||
(x) => x.type === 'cart' && x.name.startsWith('Vorgang '),
|
||||
);
|
||||
|
||||
const maxProcessNumber = processes.reduce((max, process) => {
|
||||
const number = parseInt(process.name.replace('Vorgang ', ''), 10);
|
||||
return number > max ? number : max;
|
||||
}, 0);
|
||||
|
||||
return `Vorgang ${maxProcessNumber + 1}`;
|
||||
}
|
||||
|
||||
getNextProcessId() {
|
||||
return Date.now();
|
||||
}
|
||||
|
||||
async navigateToCatalog(process: ApplicationProcess) {
|
||||
await this.#catalogNavigationService
|
||||
.getArticleSearchBasePath(process.id)
|
||||
.navigate();
|
||||
}
|
||||
|
||||
navigateToDashboard() {
|
||||
this.#router.navigate(['/kunde', 'dashboard']);
|
||||
}
|
||||
|
||||
async closeAllProcesses() {
|
||||
const processes = await this.processes$.pipe(take(1)).toPromise();
|
||||
|
||||
processes.forEach((process) => this.#app.removeProcess(process.id));
|
||||
|
||||
this.navigateToDashboard();
|
||||
}
|
||||
|
||||
fetchAndOpenPackages = () =>
|
||||
this.#wrongDestinationModalService.fetchAndOpen();
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
@layer components {
|
||||
@import "../../../libs/ui/buttons/src/buttons.scss";
|
||||
@import "../../../libs/ui/bullet-list/src/bullet-list.scss";
|
||||
@import "../../../libs/ui/datepicker/src/datepicker.scss";
|
||||
@import "../../../libs/ui/dialog/src/dialog.scss";
|
||||
@import "../../../libs/ui/input-controls/src/input-controls.scss";
|
||||
@@ -17,6 +18,7 @@
|
||||
@import "../../../libs/ui/search-bar/src/search-bar.scss";
|
||||
@import "../../../libs/ui/skeleton-loader/src/skeleton-loader.scss";
|
||||
@import "../../../libs/ui/tooltip/src/tooltip.scss";
|
||||
@import "../../../libs/ui/label/src/label.scss";
|
||||
|
||||
.input-control {
|
||||
@apply rounded border border-solid border-[#AEB7C1] px-4 py-[1.125rem] outline-none;
|
||||
|
||||
@@ -16,20 +16,20 @@ import {
|
||||
forwardRef,
|
||||
Optional,
|
||||
inject,
|
||||
} from '@angular/core';
|
||||
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
|
||||
import { UiAutocompleteComponent } from '@ui/autocomplete';
|
||||
import { UiFormControlDirective } from '@ui/form-control';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { ScanAdapterService } from '@adapter/scan';
|
||||
import { injectCancelSearch } from '@shared/services/cancel-subject';
|
||||
import { containsElement } from '@utils/common';
|
||||
import { EnvironmentService } from '@core/environment';
|
||||
} from "@angular/core";
|
||||
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from "@angular/forms";
|
||||
import { UiAutocompleteComponent } from "@ui/autocomplete";
|
||||
import { UiFormControlDirective } from "@ui/form-control";
|
||||
import { Subscription } from "rxjs";
|
||||
import { ScanAdapterService } from "@adapter/scan";
|
||||
import { injectCancelSearch } from "@shared/services/cancel-subject";
|
||||
import { containsElement } from "@utils/common";
|
||||
import { EnvironmentService } from "@core/environment";
|
||||
|
||||
@Component({
|
||||
selector: 'ui-searchbox',
|
||||
templateUrl: 'searchbox.component.html',
|
||||
styleUrls: ['searchbox.component.scss'],
|
||||
selector: "ui-searchbox",
|
||||
templateUrl: "searchbox.component.html",
|
||||
styleUrls: ["searchbox.component.scss"],
|
||||
providers: [
|
||||
{
|
||||
provide: NG_VALUE_ACCESSOR,
|
||||
@@ -49,9 +49,9 @@ export class UiSearchboxNextComponent
|
||||
private readonly _cancelSearch = injectCancelSearch({ optional: true });
|
||||
|
||||
disabled: boolean;
|
||||
type = 'text';
|
||||
type = "text";
|
||||
|
||||
@ViewChild('input', { read: ElementRef, static: true })
|
||||
@ViewChild("input", { read: ElementRef, static: true })
|
||||
input: ElementRef;
|
||||
|
||||
@ContentChild(UiAutocompleteComponent)
|
||||
@@ -61,9 +61,9 @@ export class UiSearchboxNextComponent
|
||||
focusAfterViewInit: boolean = true;
|
||||
|
||||
@Input()
|
||||
placeholder: string = '';
|
||||
placeholder: string = "";
|
||||
|
||||
private _query = '';
|
||||
private _query = "";
|
||||
|
||||
@Input()
|
||||
get query() {
|
||||
@@ -94,7 +94,7 @@ export class UiSearchboxNextComponent
|
||||
scanner = false;
|
||||
|
||||
@Input()
|
||||
hint: string = '';
|
||||
hint: string = "";
|
||||
|
||||
@Output()
|
||||
hintCleared = new EventEmitter<void>();
|
||||
@@ -107,11 +107,11 @@ export class UiSearchboxNextComponent
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.setQuery('');
|
||||
this.setQuery("");
|
||||
this._cancelSearch();
|
||||
}
|
||||
|
||||
@HostBinding('class.autocomplete-opend')
|
||||
@HostBinding("class.autocomplete-opend")
|
||||
get autocompleteOpen() {
|
||||
return this.autocomplete?.opend;
|
||||
}
|
||||
@@ -212,14 +212,14 @@ export class UiSearchboxNextComponent
|
||||
}
|
||||
|
||||
clearHint() {
|
||||
this.hint = '';
|
||||
this.hint = "";
|
||||
this.focused.emit(true);
|
||||
this.hintCleared.emit();
|
||||
this.cdr.markForCheck();
|
||||
}
|
||||
|
||||
onKeyup(event: KeyboardEvent) {
|
||||
if (event.key === 'Enter') {
|
||||
if (event.key === "Enter") {
|
||||
if (this.autocomplete?.opend && this.autocomplete?.activeItem) {
|
||||
this.setQuery(this.autocomplete?.activeItem?.item);
|
||||
this.autocomplete?.close();
|
||||
@@ -227,7 +227,7 @@ export class UiSearchboxNextComponent
|
||||
this.search.emit(this.query);
|
||||
|
||||
event.preventDefault();
|
||||
} else if (event.key === 'ArrowUp' || event.key === 'ArrowDown') {
|
||||
} else if (event.key === "ArrowUp" || event.key === "ArrowDown") {
|
||||
this.handleArrowUpDownEvent(event);
|
||||
}
|
||||
}
|
||||
@@ -235,12 +235,14 @@ export class UiSearchboxNextComponent
|
||||
handleArrowUpDownEvent(event: KeyboardEvent) {
|
||||
this.autocomplete?.handleKeyboardEvent(event);
|
||||
if (this.autocomplete?.activeItem) {
|
||||
const query = this.autocompleteValueSelector(this.autocomplete.activeItem.item);
|
||||
const query = this.autocompleteValueSelector(
|
||||
this.autocomplete.activeItem.item,
|
||||
);
|
||||
this.setQuery(query, false, false);
|
||||
}
|
||||
}
|
||||
|
||||
@HostListener('window:click', ['$event'])
|
||||
@HostListener("window:click", ["$event"])
|
||||
focusLost(event: MouseEvent) {
|
||||
if (
|
||||
this.autocomplete?.opend &&
|
||||
@@ -254,9 +256,11 @@ export class UiSearchboxNextComponent
|
||||
this.search.emit(this.query);
|
||||
}
|
||||
|
||||
@HostListener('focusout', ['$event'])
|
||||
@HostListener("focusout", ["$event"])
|
||||
onBlur() {
|
||||
this.onTouched();
|
||||
if (typeof this.onTouched === "function") {
|
||||
this.onTouched();
|
||||
}
|
||||
this.focused.emit(false);
|
||||
this.cdr.markForCheck();
|
||||
}
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
}
|
||||
|
||||
.ui-tooltip-panel {
|
||||
@apply pointer-events-auto;
|
||||
|
||||
.triangle {
|
||||
width: 30px;
|
||||
polygon {
|
||||
|
||||
@@ -17,6 +17,7 @@ import { provideRouter } from '@angular/router';
|
||||
type ProductInfoInputs = {
|
||||
item: ProductInfoItem;
|
||||
orientation: ProductInfoOrientation;
|
||||
innerGridClass: string;
|
||||
};
|
||||
|
||||
const meta: Meta<ProductInfoInputs> = {
|
||||
@@ -50,8 +51,10 @@ const meta: Meta<ProductInfoInputs> = {
|
||||
value: 19.99,
|
||||
},
|
||||
},
|
||||
tag: 'Prio 2',
|
||||
},
|
||||
orientation: 'horizontal',
|
||||
innerGridClass: 'grid-cols-[minmax(20rem,1fr),auto]',
|
||||
},
|
||||
argTypes: {
|
||||
item: {
|
||||
@@ -68,6 +71,16 @@ const meta: Meta<ProductInfoInputs> = {
|
||||
},
|
||||
},
|
||||
},
|
||||
innerGridClass: {
|
||||
control: 'text',
|
||||
description:
|
||||
'Custom CSS classes for the inner grid layout. (Applies on vertical layout only)',
|
||||
table: {
|
||||
defaultValue: {
|
||||
summary: 'grid-cols-[minmax(20rem,1fr),auto]',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
@@ -95,6 +108,7 @@ export const Default: Story = {
|
||||
value: 29.99,
|
||||
},
|
||||
},
|
||||
tag: 'Prio 2',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import { type Meta, type StoryObj, argsToTemplate } from '@storybook/angular';
|
||||
import { ProductShelfMetaInfoComponent } from '@isa/remission/shared/product';
|
||||
|
||||
const meta: Meta<ProductShelfMetaInfoComponent> = {
|
||||
component: ProductShelfMetaInfoComponent,
|
||||
title: 'remission/shared/product/ProductShelfMetaInfoComponent',
|
||||
args: {
|
||||
department: 'Reise',
|
||||
shelfLabel: 'Europa',
|
||||
productGroupKey: '311',
|
||||
productGroupValue: 'Romane TB',
|
||||
assortment: 'Basissortiment|BPrämienartikel|n',
|
||||
returnReason: 'Beschädigt',
|
||||
},
|
||||
argTypes: {
|
||||
department: {
|
||||
control: { type: 'text' },
|
||||
description: 'The department of the product.',
|
||||
defaultValue: undefined,
|
||||
},
|
||||
shelfLabel: {
|
||||
control: { type: 'text' },
|
||||
description: 'The shelf label of the product.',
|
||||
defaultValue: undefined,
|
||||
},
|
||||
productGroupKey: {
|
||||
control: { type: 'text' },
|
||||
description: 'The key of the product group.',
|
||||
defaultValue: undefined,
|
||||
},
|
||||
productGroupValue: {
|
||||
control: { type: 'text' },
|
||||
description: 'The value of the product group.',
|
||||
defaultValue: undefined,
|
||||
},
|
||||
assortment: {
|
||||
control: { type: 'text' },
|
||||
description: 'The assortment of the product.',
|
||||
defaultValue: undefined,
|
||||
},
|
||||
returnReason: {
|
||||
control: { type: 'text' },
|
||||
description: 'The reason for the return of the product.',
|
||||
defaultValue: undefined,
|
||||
},
|
||||
},
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `<remi-product-shelf-meta-info ${argsToTemplate(args)}></remi-product-shelf-meta-info>`,
|
||||
}),
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<ProductShelfMetaInfoComponent>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
department: 'Reise',
|
||||
shelfLabel: 'Europa',
|
||||
productGroupKey: '311',
|
||||
productGroupValue: 'Romane TB',
|
||||
assortment: 'Basissortiment|BPrämienartikel|n',
|
||||
returnReason: 'Beschädigt',
|
||||
},
|
||||
};
|
||||
@@ -1,74 +1,64 @@
|
||||
import {
|
||||
type Meta,
|
||||
type StoryObj,
|
||||
argsToTemplate,
|
||||
moduleMetadata,
|
||||
} from '@storybook/angular';
|
||||
import { ProductStockInfoComponent } from '@isa/remission/shared/product';
|
||||
import { provideProductImageUrl } from '@isa/shared/product-image';
|
||||
import { provideProductRouterLinkBuilder } from '@isa/shared/product-router-link';
|
||||
|
||||
const meta: Meta<ProductStockInfoComponent> = {
|
||||
component: ProductStockInfoComponent,
|
||||
title: 'remission/shared/product/ProductStockInfoComponent',
|
||||
decorators: [
|
||||
moduleMetadata({
|
||||
providers: [
|
||||
provideProductImageUrl('https://produktbilder-test.paragon-data.net'),
|
||||
provideProductRouterLinkBuilder((ean: string) => ean),
|
||||
],
|
||||
}),
|
||||
],
|
||||
args: {
|
||||
stock: 92,
|
||||
removedFromStock: 0,
|
||||
predefinedReturnQuantity: 4,
|
||||
remainingQuantityInStock: 0,
|
||||
zob: 0,
|
||||
},
|
||||
argTypes: {
|
||||
stock: {
|
||||
control: { type: 'number' },
|
||||
description: 'The current stock of the product.',
|
||||
defaultValue: 0,
|
||||
},
|
||||
removedFromStock: {
|
||||
control: { type: 'number' },
|
||||
description: 'The amount of stock that has been removed.',
|
||||
defaultValue: 0,
|
||||
},
|
||||
predefinedReturnQuantity: {
|
||||
control: { type: 'number' },
|
||||
description: 'The predefined return quantity for the product.',
|
||||
defaultValue: 0,
|
||||
},
|
||||
remainingQuantityInStock: {
|
||||
control: { type: 'number' },
|
||||
description: 'The remaining quantity in stock after returns.',
|
||||
defaultValue: 0,
|
||||
},
|
||||
zob: {
|
||||
control: { type: 'number' },
|
||||
description: 'Min Stock Category Management Information.',
|
||||
defaultValue: 0,
|
||||
},
|
||||
},
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `<remi-product-stock-info ${argsToTemplate(args)}></remi-product-stock-info>`,
|
||||
}),
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<ProductStockInfoComponent>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
stock: 92,
|
||||
removedFromStock: 0,
|
||||
predefinedReturnQuantity: 4,
|
||||
remainingQuantityInStock: 0,
|
||||
zob: 0,
|
||||
},
|
||||
};
|
||||
import {
|
||||
type Meta,
|
||||
type StoryObj,
|
||||
argsToTemplate,
|
||||
moduleMetadata,
|
||||
} from '@storybook/angular';
|
||||
import { ProductStockInfoComponent } from '@isa/remission/shared/product';
|
||||
import { provideProductImageUrl } from '@isa/shared/product-image';
|
||||
import { provideProductRouterLinkBuilder } from '@isa/shared/product-router-link';
|
||||
|
||||
const meta: Meta<ProductStockInfoComponent> = {
|
||||
component: ProductStockInfoComponent,
|
||||
title: 'remission/shared/product/ProductStockInfoComponent',
|
||||
decorators: [
|
||||
moduleMetadata({
|
||||
providers: [
|
||||
provideProductImageUrl('https://produktbilder-test.paragon-data.net'),
|
||||
provideProductRouterLinkBuilder((ean: string) => ean),
|
||||
],
|
||||
}),
|
||||
],
|
||||
args: {
|
||||
availableStock: 92,
|
||||
stockToRemit: 91,
|
||||
targetStock: 1,
|
||||
zob: 0,
|
||||
},
|
||||
argTypes: {
|
||||
availableStock: {
|
||||
control: { type: 'number' },
|
||||
description: 'Total available stock for the product.',
|
||||
},
|
||||
stockToRemit: {
|
||||
control: { type: 'number' },
|
||||
description: 'Stock quantity to remit.',
|
||||
},
|
||||
targetStock: {
|
||||
control: { type: 'number' },
|
||||
description: 'Target stock level after remittance.',
|
||||
},
|
||||
zob: {
|
||||
control: { type: 'number' },
|
||||
description: 'Min Stock Category Management Information.',
|
||||
defaultValue: 0,
|
||||
},
|
||||
},
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `<remi-product-stock-info ${argsToTemplate(args)}></remi-product-stock-info>`,
|
||||
}),
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<ProductStockInfoComponent>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
availableStock: 92,
|
||||
stockToRemit: 91,
|
||||
targetStock: 1,
|
||||
zob: 0,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
import { argsToTemplate, type Meta, type StoryObj, moduleMetadata } from '@storybook/angular';
|
||||
import { EmptyStateComponent } from '@isa/ui/empty-state';
|
||||
import {
|
||||
argsToTemplate,
|
||||
type Meta,
|
||||
type StoryObj,
|
||||
moduleMetadata,
|
||||
} from '@storybook/angular';
|
||||
import { EmptyStateAppearance, EmptyStateComponent } from '@isa/ui/empty-state';
|
||||
import { ButtonComponent } from '@isa/ui/buttons';
|
||||
|
||||
type EmptyStateComponentInputs = {
|
||||
title: string;
|
||||
description: string;
|
||||
appearance: EmptyStateAppearance;
|
||||
};
|
||||
|
||||
const meta: Meta<EmptyStateComponentInputs> = {
|
||||
@@ -22,6 +28,10 @@ const meta: Meta<EmptyStateComponentInputs> = {
|
||||
description: {
|
||||
control: 'text',
|
||||
},
|
||||
appearance: {
|
||||
control: 'select',
|
||||
options: Object.values(EmptyStateAppearance),
|
||||
},
|
||||
},
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
@@ -40,5 +50,6 @@ export const Default: Story = {
|
||||
args: {
|
||||
title: 'Keine Suchergebnisse',
|
||||
description: 'Suchen Sie nach einer Rechnungsnummer oder Kundennamen.',
|
||||
appearance: EmptyStateAppearance.NoResults,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
@@ -31,7 +31,7 @@ const meta: Meta<ClientRowComponent> = {
|
||||
</ui-item-row-data-value>
|
||||
</ui-item-row-data-row>
|
||||
<ui-item-row-data-row>
|
||||
<ui-item-row-data-label>Rechnugsnr.</ui-item-row-data-label>
|
||||
<ui-item-row-data-label>Beleg-Nr.</ui-item-row-data-label>
|
||||
<ui-item-row-data-value>
|
||||
<span class="isa-text-body-2-bold">
|
||||
1234567890
|
||||
|
||||
39
apps/isa-app/stories/ui/label/ui-label.stories.ts
Normal file
39
apps/isa-app/stories/ui/label/ui-label.stories.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { argsToTemplate, type Meta, type StoryObj } from '@storybook/angular';
|
||||
import { Labeltype, LabelPriority, LabelComponent } from '@isa/ui/label';
|
||||
|
||||
type UiLabelInputs = {
|
||||
type: Labeltype;
|
||||
priority: LabelPriority;
|
||||
};
|
||||
|
||||
const meta: Meta<UiLabelInputs> = {
|
||||
component: LabelComponent,
|
||||
title: 'ui/label/Label',
|
||||
argTypes: {
|
||||
type: {
|
||||
control: { type: 'select' },
|
||||
options: Object.values(Labeltype),
|
||||
description: 'Determines the label type',
|
||||
},
|
||||
priority: {
|
||||
control: { type: 'select' },
|
||||
options: Object.values(LabelPriority),
|
||||
description: 'Determines the label priority',
|
||||
},
|
||||
},
|
||||
args: {
|
||||
type: 'tag',
|
||||
priority: 'high',
|
||||
},
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `<ui-label ${argsToTemplate(args)}>Prio 1</ui-label>`,
|
||||
}),
|
||||
};
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<LabelComponent>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {},
|
||||
};
|
||||
@@ -9,10 +9,10 @@ trigger:
|
||||
variables:
|
||||
# Major Version einstellen
|
||||
- name: 'Major'
|
||||
value: '3'
|
||||
value: '4'
|
||||
# Minor Version einstellen
|
||||
- name: 'Minor'
|
||||
value: '4'
|
||||
value: '0'
|
||||
- name: 'Patch'
|
||||
value: "$[counter(format('{0}.{1}', variables['Major'], variables['Minor']),0)]"
|
||||
- name: 'BuildUniqueID'
|
||||
@@ -89,7 +89,7 @@ jobs:
|
||||
condition: and(ne(variables['Build.SourceBranch'], 'refs/heads/integration'), ne(variables['Build.SourceBranch'], 'refs/heads/master'), not(startsWith(variables['Build.SourceBranch'], 'refs/heads/hotfix/')), not(startsWith(variables['Build.SourceBranch'], 'refs/heads/release/')))
|
||||
variables:
|
||||
- name: DockerTagSourceBranch
|
||||
value: $[replace(variables['Build.SourceBranch'], '/', '_')]
|
||||
value: $[replace(variables['Build.SourceBranch'], '/', '-')]
|
||||
- name: 'DockerTag'
|
||||
value: |
|
||||
$(Build.BuildNumber)-$(Build.SourceVersion)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,128 +1,136 @@
|
||||
# Tech Stack Documentation
|
||||
|
||||
## Core Technologies
|
||||
|
||||
### Frontend Framework
|
||||
|
||||
- **[Angular](https://angular.dev/overview)** (Latest Version)
|
||||
- Modern web framework for building scalable single-page applications
|
||||
- Leverages TypeScript for enhanced type safety and developer experience
|
||||
|
||||
### State Management
|
||||
|
||||
- **[NgRx](https://ngrx.io/docs)**
|
||||
- Redux-inspired state management for Angular applications
|
||||
- Provides predictable state container and powerful dev tools
|
||||
- Used for managing complex application state and side effects
|
||||
|
||||
### Styling
|
||||
|
||||
- **[Tailwind CSS](https://tailwindcss.com/)**
|
||||
- Utility-first CSS framework
|
||||
- Enables rapid UI development with pre-built classes
|
||||
- Highly customizable through configuration
|
||||
|
||||
## Development Tools
|
||||
|
||||
### Language
|
||||
|
||||
- **[TypeScript](https://www.typescriptlang.org/docs/handbook/intro.html)**
|
||||
- Strongly typed programming language
|
||||
- Provides enhanced IDE support and compile-time error checking
|
||||
- Used throughout the entire application
|
||||
|
||||
### Runtime
|
||||
|
||||
- **[Node.js](https://nodejs.org/docs/latest-v22.x/api/index.html)**
|
||||
- JavaScript runtime environment
|
||||
- Used for development server and build tools
|
||||
- Required for running npm scripts and development tools
|
||||
|
||||
### Testing Framework
|
||||
|
||||
- **[Jest](https://jestjs.io/docs/getting-started)**
|
||||
|
||||
- Primary testing framework
|
||||
- Used for unit and integration tests
|
||||
- Provides snapshot testing capabilities
|
||||
|
||||
- **[Spectator](https://ngneat.github.io/spectator/)**
|
||||
- Angular testing utility library
|
||||
- Simplifies component testing
|
||||
- Reduces boilerplate in test files
|
||||
|
||||
### UI Development
|
||||
|
||||
- **[Storybook](https://storybook.js.org/docs/get-started/frameworks/angular)**
|
||||
- UI component development environment
|
||||
- Enables isolated component development
|
||||
- Serves as living documentation for components
|
||||
|
||||
### Utilities
|
||||
|
||||
- **[date-fns](https://date-fns.org/docs/Getting-Started)**
|
||||
- Modern JavaScript date utility library
|
||||
- Used for consistent date formatting and manipulation
|
||||
- Tree-shakeable to optimize bundle size
|
||||
- **[Lodash](https://lodash.com/)**
|
||||
- Utility library for common JavaScript tasks
|
||||
- **[UUID](https://www.npmjs.com/package/uuid)**
|
||||
- Generates unique identifiers
|
||||
- **[Zod](https://github.com/colinhacks/zod)**
|
||||
- TypeScript-first schema validation library
|
||||
|
||||
## Additional Technical Areas
|
||||
|
||||
### Authentication & Authorization
|
||||
|
||||
- **[angular-oauth2-oidc](https://github.com/manfredsteyer/angular-oauth2-oidc)**
|
||||
- Simplifies implementing OAuth2 and OIDC authentication in Angular.
|
||||
- **[angular-oauth2-oidc-jwks](https://github.com/manfredsteyer/angular-oauth2-oidc)**
|
||||
- Adds JWKS support for secure token management.
|
||||
|
||||
### Real-Time Communication
|
||||
|
||||
- **[@microsoft/signalr](https://www.npmjs.com/package/@microsoft/signalr)**
|
||||
- Provides real-time communication between client and server components.
|
||||
|
||||
### Barcode Scanning
|
||||
|
||||
- **[Scandit Web Data Capture Barcode](https://www.scandit.com/documentation/web/)**
|
||||
- Robust barcode scanning capabilities integrated into the application.
|
||||
- **[Scandit Web Data Capture Core](https://www.scandit.com/documentation/web/)**
|
||||
- Core library supporting the barcode scanning features.
|
||||
|
||||
### Tooling
|
||||
|
||||
- **[Nx](https://nx.dev/)**
|
||||
- Powerful monorepo tool for Angular and other frontend applications.
|
||||
- **[Husky](https://typicode.github.io/husky/#/)**
|
||||
- Manages Git hooks for consistent developer workflows.
|
||||
- **[ESLint](https://eslint.org/) & [Prettier](https://prettier.io/)**
|
||||
- Linting and formatting tools to maintain consistent code quality.
|
||||
- **[Storybook](https://storybook.js.org/)**
|
||||
- Isolated component development and living documentation environment.
|
||||
|
||||
## Development Environment Setup
|
||||
|
||||
1. **Required Software**
|
||||
|
||||
- Node.js (Latest LTS version)
|
||||
- npm (comes with Node.js)
|
||||
- Git
|
||||
|
||||
2. **IDE Recommendations**
|
||||
|
||||
- Visual Studio Code with following extensions:
|
||||
- Angular Language Service
|
||||
- ESLint
|
||||
- Prettier
|
||||
- Tailwind CSS IntelliSense
|
||||
|
||||
3. **Getting Started**
|
||||
```bash
|
||||
npm install # Install dependencies
|
||||
npm run start # Start development server
|
||||
npm run test # Run tests
|
||||
npm run storybook # Start Storybook
|
||||
```
|
||||
# Tech Stack Documentation
|
||||
|
||||
## Core Technologies
|
||||
|
||||
### Frontend Framework
|
||||
|
||||
- **[Angular](https://angular.dev/overview)** (Latest Version)
|
||||
- Modern web framework for building scalable single-page applications
|
||||
- Leverages TypeScript for enhanced type safety and developer experience
|
||||
|
||||
### State Management
|
||||
|
||||
- **[NgRx](https://ngrx.io/docs)**
|
||||
- Redux-inspired state management for Angular applications
|
||||
- Provides predictable state container and powerful dev tools
|
||||
- Used for managing complex application state and side effects
|
||||
|
||||
### Styling
|
||||
|
||||
- **[Tailwind CSS](https://tailwindcss.com/)**
|
||||
- Utility-first CSS framework
|
||||
- Enables rapid UI development with pre-built classes
|
||||
- Highly customizable through configuration
|
||||
|
||||
## Development Tools
|
||||
|
||||
### Language
|
||||
|
||||
- **[TypeScript](https://www.typescriptlang.org/docs/handbook/intro.html)**
|
||||
- Strongly typed programming language
|
||||
- Provides enhanced IDE support and compile-time error checking
|
||||
- Used throughout the entire application
|
||||
|
||||
### Runtime
|
||||
|
||||
- **[Node.js](https://nodejs.org/docs/latest-v22.x/api/index.html)**
|
||||
- JavaScript runtime environment
|
||||
- Used for development server and build tools
|
||||
- Required for running npm scripts and development tools
|
||||
|
||||
### Testing Framework
|
||||
|
||||
- **[Jest](https://jestjs.io/docs/getting-started)**
|
||||
|
||||
- Primary testing framework
|
||||
- Used for unit and integration tests
|
||||
- Provides snapshot testing capabilities
|
||||
|
||||
- **[Spectator](https://ngneat.github.io/spectator/)**
|
||||
- Angular testing utility library
|
||||
- Simplifies component testing
|
||||
- Reduces boilerplate in test files
|
||||
|
||||
### UI Development
|
||||
|
||||
- **[Storybook](https://storybook.js.org/docs/get-started/frameworks/angular)**
|
||||
- UI component development environment
|
||||
- Enables isolated component development
|
||||
- Serves as living documentation for components
|
||||
|
||||
### Utilities
|
||||
|
||||
- **[date-fns](https://date-fns.org/docs/Getting-Started)**
|
||||
- Modern JavaScript date utility library
|
||||
- Used for consistent date formatting and manipulation
|
||||
- Tree-shakeable to optimize bundle size
|
||||
- **[Lodash](https://lodash.com/)**
|
||||
- Utility library for common JavaScript tasks
|
||||
- **[UUID](https://www.npmjs.com/package/uuid)**
|
||||
- Generates unique identifiers
|
||||
- **[Zod](https://github.com/colinhacks/zod)**
|
||||
- TypeScript-first schema validation library
|
||||
|
||||
## Additional Technical Areas
|
||||
|
||||
### Authentication & Authorization
|
||||
|
||||
- **[angular-oauth2-oidc](https://github.com/manfredsteyer/angular-oauth2-oidc)**
|
||||
- Simplifies implementing OAuth2 and OIDC authentication in Angular.
|
||||
- **[angular-oauth2-oidc-jwks](https://github.com/manfredsteyer/angular-oauth2-oidc)**
|
||||
- Adds JWKS support for secure token management.
|
||||
|
||||
### Real-Time Communication
|
||||
|
||||
- **[@microsoft/signalr](https://www.npmjs.com/package/@microsoft/signalr)**
|
||||
- Provides real-time communication between client and server components.
|
||||
|
||||
### Barcode Scanning
|
||||
|
||||
- **[Scandit Web Data Capture Barcode](https://www.scandit.com/documentation/web/)**
|
||||
- Robust barcode scanning capabilities integrated into the application.
|
||||
- **[Scandit Web Data Capture Core](https://www.scandit.com/documentation/web/)**
|
||||
- Core library supporting the barcode scanning features.
|
||||
|
||||
### Tooling
|
||||
|
||||
- **[Nx](https://nx.dev/)**
|
||||
- Powerful monorepo tool for Angular and other frontend applications.
|
||||
- **[Husky](https://typicode.github.io/husky/#/)**
|
||||
- Manages Git hooks for consistent developer workflows.
|
||||
- **[ESLint](https://eslint.org/) & [Prettier](https://prettier.io/)**
|
||||
- Linting and formatting tools to maintain consistent code quality.
|
||||
- **[Storybook](https://storybook.js.org/)**
|
||||
- Isolated component development and living documentation environment.
|
||||
|
||||
## Domain Libraries
|
||||
|
||||
### Customer Relationship Management (CRM)
|
||||
|
||||
- **`@isa/crm/data-access`**
|
||||
- Handles data access logic for customer-related features.
|
||||
- Contains services for fetching and managing customer data.
|
||||
|
||||
## Development Environment Setup
|
||||
|
||||
1. **Required Software**
|
||||
|
||||
- Node.js (Latest LTS version)
|
||||
- npm (comes with Node.js)
|
||||
- Git
|
||||
|
||||
2. **IDE Recommendations**
|
||||
|
||||
- Visual Studio Code with following extensions:
|
||||
- Angular Language Service
|
||||
- ESLint
|
||||
- Prettier
|
||||
- Tailwind CSS IntelliSense
|
||||
|
||||
3. **Getting Started**
|
||||
```bash
|
||||
npm install # Install dependencies
|
||||
npm run start # Start development server
|
||||
npm run test # Run tests
|
||||
npm run storybook # Start Storybook
|
||||
```
|
||||
|
||||
@@ -31,8 +31,9 @@ const PARAMETER_CODEC = new ParameterCodec();
|
||||
export class BaseService {
|
||||
constructor(
|
||||
protected config: CatConfiguration,
|
||||
protected http: HttpClient,
|
||||
) {}
|
||||
protected http: HttpClient
|
||||
) {
|
||||
}
|
||||
|
||||
private _rootUrl: string = '';
|
||||
|
||||
@@ -56,7 +57,7 @@ export class BaseService {
|
||||
*/
|
||||
protected newParams(): HttpParams {
|
||||
return new HttpParams({
|
||||
encoder: PARAMETER_CODEC,
|
||||
encoder: PARAMETER_CODEC
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
* Auocomplete-Ergebnis
|
||||
*/
|
||||
export interface AutocompleteDTO {
|
||||
|
||||
/**
|
||||
* Anzeige / Bezeichner
|
||||
*/
|
||||
|
||||
@@ -5,6 +5,7 @@ import { CatalogType } from './catalog-type';
|
||||
* Suchabfrage
|
||||
*/
|
||||
export interface AutocompleteTokenDTO {
|
||||
|
||||
/**
|
||||
* Katalogbereich
|
||||
*/
|
||||
@@ -13,7 +14,7 @@ export interface AutocompleteTokenDTO {
|
||||
/**
|
||||
* Filter
|
||||
*/
|
||||
filter?: { [key: string]: string };
|
||||
filter?: {[key: string]: string};
|
||||
|
||||
/**
|
||||
* Eingabe
|
||||
|
||||
@@ -7,6 +7,7 @@ import { AvailabilityType } from './availability-type';
|
||||
* Verfügbarkeit
|
||||
*/
|
||||
export interface AvailabilityDTO {
|
||||
|
||||
/**
|
||||
* Voraussichtliches Lieferdatum
|
||||
*/
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
/* tslint:disable */
|
||||
export type AvailabilityType = 0 | 1 | 2 | 32 | 256 | 512 | 1024 | 2048 | 4096 | 8192 | 16384;
|
||||
export type AvailabilityType = 0 | 1 | 2 | 32 | 256 | 512 | 1024 | 2048 | 4096 | 8192 | 16384;
|
||||
@@ -1,2 +1,2 @@
|
||||
/* tslint:disable */
|
||||
export type Avoirdupois = 0 | 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128 | 256 | 512 | 1024 | 2048 | 4096;
|
||||
export type Avoirdupois = 0 | 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128 | 256 | 512 | 1024 | 2048 | 4096;
|
||||
@@ -3,4 +3,4 @@
|
||||
/**
|
||||
* Katalogbereich
|
||||
*/
|
||||
export type CatalogType = 0 | 1 | 2 | 4;
|
||||
export type CatalogType = 0 | 1 | 2 | 4;
|
||||
@@ -1,2 +1,2 @@
|
||||
/* 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 */
|
||||
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 */
|
||||
export type DialogSettings = 0 | 1 | 2 | 4;
|
||||
export type DialogSettings = 0 | 1 | 2 | 4;
|
||||
@@ -2,7 +2,7 @@
|
||||
import { TouchedBase } from './touched-base';
|
||||
import { CRUDA } from './cruda';
|
||||
import { EntityStatus } from './entity-status';
|
||||
export interface EntityDTO extends TouchedBase {
|
||||
export interface EntityDTO extends TouchedBase{
|
||||
changed?: string;
|
||||
created?: string;
|
||||
cruda?: CRUDA;
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
/* tslint:disable */
|
||||
export type EntityStatus = 0 | 1 | 2 | 4 | 8;
|
||||
export type EntityStatus = 0 | 1 | 2 | 4 | 8;
|
||||
@@ -4,6 +4,7 @@
|
||||
* Bild
|
||||
*/
|
||||
export interface ImageDTO {
|
||||
|
||||
/**
|
||||
* Copyright
|
||||
*/
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
/* tslint:disable */
|
||||
export type InputType = 0 | 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128 | 256 | 512 | 1024 | 2048 | 3072 | 4096 | 8192 | 12288;
|
||||
export type InputType = 0 | 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128 | 256 | 512 | 1024 | 2048 | 3072 | 4096 | 8192 | 12288;
|
||||
@@ -11,7 +11,8 @@ import { StockInfoDTO } from './stock-info-dto';
|
||||
import { Successor } from './successor';
|
||||
import { TextDTO } from './text-dto';
|
||||
import { ItemType } from './item-type';
|
||||
export interface ItemDTO extends EntityDTO {
|
||||
export interface ItemDTO extends EntityDTO{
|
||||
|
||||
/**
|
||||
* Verfügbarkeit laut Katalog
|
||||
*/
|
||||
@@ -30,7 +31,7 @@ export interface ItemDTO extends EntityDTO {
|
||||
/**
|
||||
* Weitere Artikel-IDs
|
||||
*/
|
||||
ids?: { [key: string]: number };
|
||||
ids?: {[key: string]: number};
|
||||
|
||||
/**
|
||||
* Primary image / Id des Hauptbilds
|
||||
@@ -45,7 +46,7 @@ export interface ItemDTO extends EntityDTO {
|
||||
/**
|
||||
* Markierungen (Lesezeichen) wie (BOD, Prämie)
|
||||
*/
|
||||
labels?: { [key: string]: string };
|
||||
labels?: {[key: string]: string};
|
||||
|
||||
/**
|
||||
* Produkt-Stammdaten
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
/**
|
||||
* Artikel-/Produkttyp
|
||||
*/
|
||||
export type ItemType = 0 | 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128 | 256 | 512 | 1024 | 2048 | 4096 | 8192 | 16384 | 32768 | 65536;
|
||||
export type ItemType = 0 | 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128 | 256 | 512 | 1024 | 2048 | 4096 | 8192 | 16384 | 32768 | 65536;
|
||||
@@ -1,5 +1,6 @@
|
||||
/* tslint:disable */
|
||||
export interface LesepunkteRequest {
|
||||
|
||||
/**
|
||||
* Artikel ID
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* tslint:disable */
|
||||
import { ResponseArgsOfIEnumerableOfAutocompleteDTO } from './response-args-of-ienumerable-of-autocomplete-dto';
|
||||
export interface ListResponseArgsOfAutocompleteDTO extends ResponseArgsOfIEnumerableOfAutocompleteDTO {
|
||||
export interface ListResponseArgsOfAutocompleteDTO extends ResponseArgsOfIEnumerableOfAutocompleteDTO{
|
||||
completed?: boolean;
|
||||
hits?: number;
|
||||
skip?: number;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* tslint:disable */
|
||||
import { ResponseArgsOfIEnumerableOfItemDTO } from './response-args-of-ienumerable-of-item-dto';
|
||||
export interface ListResponseArgsOfItemDTO extends ResponseArgsOfIEnumerableOfItemDTO {
|
||||
export interface ListResponseArgsOfItemDTO extends ResponseArgsOfIEnumerableOfItemDTO{
|
||||
completed?: boolean;
|
||||
hits?: number;
|
||||
skip?: number;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { TouchedBase } from './touched-base';
|
||||
import { PriceValueDTO } from './price-value-dto';
|
||||
import { VATValueDTO } from './vatvalue-dto';
|
||||
export interface PriceDTO extends TouchedBase {
|
||||
export interface PriceDTO extends TouchedBase{
|
||||
value?: PriceValueDTO;
|
||||
vat?: VATValueDTO;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* tslint:disable */
|
||||
import { TouchedBase } from './touched-base';
|
||||
export interface PriceValueDTO extends TouchedBase {
|
||||
export interface PriceValueDTO extends TouchedBase{
|
||||
currency?: string;
|
||||
currencySymbol?: string;
|
||||
value?: number;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* tslint:disable */
|
||||
export interface ProblemDetails {
|
||||
detail?: string;
|
||||
extensions: { [key: string]: any };
|
||||
extensions: {[key: string]: any};
|
||||
instance?: string;
|
||||
status?: number;
|
||||
title?: string;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { TouchedBase } from './touched-base';
|
||||
import { SizeOfString } from './size-of-string';
|
||||
import { WeightOfAvoirdupois } from './weight-of-avoirdupois';
|
||||
export interface ProductDTO extends TouchedBase {
|
||||
export interface ProductDTO extends TouchedBase{
|
||||
additionalName?: string;
|
||||
catalogProductNumber?: string;
|
||||
contributors?: string;
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
/* tslint:disable */
|
||||
import { QueryTokenDTO2 } from './query-token-dto2';
|
||||
import { CatalogType } from './catalog-type';
|
||||
export interface QueryTokenDTO extends QueryTokenDTO2 {
|
||||
export interface QueryTokenDTO extends QueryTokenDTO2{
|
||||
|
||||
/**
|
||||
* Katalogbereich
|
||||
*/
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
/* tslint:disable */
|
||||
import { OrderByDTO } from './order-by-dto';
|
||||
export interface QueryTokenDTO2 {
|
||||
filter?: { [key: string]: string };
|
||||
filter?: {[key: string]: string};
|
||||
friendlyName?: string;
|
||||
fuzzy?: number;
|
||||
hitsOnly?: boolean;
|
||||
ids?: Array<number>;
|
||||
input?: { [key: string]: string };
|
||||
options?: { [key: string]: string };
|
||||
input?: {[key: string]: string};
|
||||
options?: {[key: string]: string};
|
||||
orderBy?: Array<OrderByDTO>;
|
||||
skip?: number;
|
||||
take?: number;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/* tslint:disable */
|
||||
import { ResponseArgs } from './response-args';
|
||||
export interface ResponseArgsOfIDictionaryOfLongAndNullableInteger extends ResponseArgs {
|
||||
result?: { [key: string]: number };
|
||||
export interface ResponseArgsOfIDictionaryOfLongAndNullableInteger extends ResponseArgs{
|
||||
result?: {[key: string]: number};
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* tslint:disable */
|
||||
import { ResponseArgs } from './response-args';
|
||||
import { AutocompleteDTO } from './autocomplete-dto';
|
||||
export interface ResponseArgsOfIEnumerableOfAutocompleteDTO extends ResponseArgs {
|
||||
export interface ResponseArgsOfIEnumerableOfAutocompleteDTO extends ResponseArgs{
|
||||
result?: Array<AutocompleteDTO>;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* tslint:disable */
|
||||
import { ResponseArgs } from './response-args';
|
||||
import { InputGroupDTO } from './input-group-dto';
|
||||
export interface ResponseArgsOfIEnumerableOfInputGroupDTO extends ResponseArgs {
|
||||
export interface ResponseArgsOfIEnumerableOfInputGroupDTO extends ResponseArgs{
|
||||
result?: Array<InputGroupDTO>;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* tslint:disable */
|
||||
import { ResponseArgs } from './response-args';
|
||||
import { ItemDTO } from './item-dto';
|
||||
export interface ResponseArgsOfIEnumerableOfItemDTO extends ResponseArgs {
|
||||
export interface ResponseArgsOfIEnumerableOfItemDTO extends ResponseArgs{
|
||||
result?: Array<ItemDTO>;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* tslint:disable */
|
||||
import { ResponseArgs } from './response-args';
|
||||
import { OrderByDTO } from './order-by-dto';
|
||||
export interface ResponseArgsOfIEnumerableOfOrderByDTO extends ResponseArgs {
|
||||
export interface ResponseArgsOfIEnumerableOfOrderByDTO extends ResponseArgs{
|
||||
result?: Array<OrderByDTO>;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* tslint:disable */
|
||||
import { ResponseArgs } from './response-args';
|
||||
import { QueryTokenDTO } from './query-token-dto';
|
||||
export interface ResponseArgsOfIEnumerableOfQueryTokenDTO extends ResponseArgs {
|
||||
export interface ResponseArgsOfIEnumerableOfQueryTokenDTO extends ResponseArgs{
|
||||
result?: Array<QueryTokenDTO>;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* tslint:disable */
|
||||
import { ResponseArgs } from './response-args';
|
||||
import { ItemDTO } from './item-dto';
|
||||
export interface ResponseArgsOfItemDTO extends ResponseArgs {
|
||||
export interface ResponseArgsOfItemDTO extends ResponseArgs{
|
||||
result?: ItemDTO;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* tslint:disable */
|
||||
import { ResponseArgs } from './response-args';
|
||||
import { UISettingsDTO } from './uisettings-dto';
|
||||
export interface ResponseArgsOfUISettingsDTO extends ResponseArgs {
|
||||
export interface ResponseArgsOfUISettingsDTO extends ResponseArgs{
|
||||
result?: UISettingsDTO;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { DialogOfString } from './dialog-of-string';
|
||||
export interface ResponseArgs {
|
||||
dialog?: DialogOfString;
|
||||
error: boolean;
|
||||
invalidProperties?: { [key: string]: string };
|
||||
invalidProperties?: {[key: string]: string};
|
||||
message?: string;
|
||||
requestId?: number;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/* tslint:disable */
|
||||
export interface ReviewDTO {
|
||||
|
||||
/**
|
||||
* Autor
|
||||
*/
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
* Regalinfo
|
||||
*/
|
||||
export interface ShelfInfoDTO {
|
||||
|
||||
/**
|
||||
* Sortiment
|
||||
*/
|
||||
|
||||
@@ -3,4 +3,5 @@
|
||||
/**
|
||||
* Shop
|
||||
*/
|
||||
export interface ShopDTO {}
|
||||
export interface ShopDTO {
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
* Eigenchaften
|
||||
*/
|
||||
export interface SpecDTO {
|
||||
|
||||
/**
|
||||
* PK
|
||||
*/
|
||||
|
||||
@@ -5,6 +5,7 @@ import { StockStatus } from './stock-status';
|
||||
* Bestandsinformation
|
||||
*/
|
||||
export interface StockInfoDTO {
|
||||
|
||||
/**
|
||||
* Filiale PK
|
||||
*/
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
/**
|
||||
* Dispositionsstatus
|
||||
*/
|
||||
export type StockStatus = 0 | 1 | 2 | 3 | 4;
|
||||
export type StockStatus = 0 | 1 | 2 | 3 | 4;
|
||||
@@ -1,6 +1,7 @@
|
||||
/* tslint:disable */
|
||||
import { ProductDTO } from './product-dto';
|
||||
export interface Successor extends ProductDTO {
|
||||
export interface Successor extends ProductDTO{
|
||||
|
||||
/**
|
||||
* PK
|
||||
*/
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/* tslint:disable */
|
||||
export interface TextDTO {
|
||||
|
||||
/**
|
||||
* PK
|
||||
*/
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
/* tslint:disable */
|
||||
export interface TouchedBase {}
|
||||
export interface TouchedBase {
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/* tslint:disable */
|
||||
export interface TranslationDTO {
|
||||
target?: string;
|
||||
values?: { [key: string]: string };
|
||||
values?: {[key: string]: string};
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
/* tslint:disable */
|
||||
import { QuerySettingsDTO } from './query-settings-dto';
|
||||
import { TranslationDTO } from './translation-dto';
|
||||
export interface UISettingsDTO extends QuerySettingsDTO {
|
||||
export interface UISettingsDTO extends QuerySettingsDTO{
|
||||
|
||||
/**
|
||||
* Url Template für Detail-Bild
|
||||
*/
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
/* tslint:disable */
|
||||
export type VATType = 0 | 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128;
|
||||
export type VATType = 0 | 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128;
|
||||
@@ -1,7 +1,7 @@
|
||||
/* tslint:disable */
|
||||
import { TouchedBase } from './touched-base';
|
||||
import { VATType } from './vattype';
|
||||
export interface VATValueDTO extends TouchedBase {
|
||||
export interface VATValueDTO extends TouchedBase{
|
||||
inPercent?: number;
|
||||
label?: string;
|
||||
value?: number;
|
||||
|
||||
@@ -16,7 +16,10 @@ class PromotionService extends __BaseService {
|
||||
static readonly PromotionLesepunktePath = '/promotion/lesepunkte';
|
||||
static readonly PromotionLesepunkte2Path = '/stock/{stockId}/promotion/lesepunkte';
|
||||
|
||||
constructor(config: __Configuration, http: HttpClient) {
|
||||
constructor(
|
||||
config: __Configuration,
|
||||
http: HttpClient
|
||||
) {
|
||||
super(config, http);
|
||||
}
|
||||
|
||||
@@ -24,24 +27,26 @@ class PromotionService extends __BaseService {
|
||||
* Lesepunkte
|
||||
* @param items Ids und Mengen
|
||||
*/
|
||||
PromotionLesepunkteResponse(
|
||||
items: Array<LesepunkteRequest>,
|
||||
): __Observable<__StrictHttpResponse<ResponseArgsOfIDictionaryOfLongAndNullableInteger>> {
|
||||
PromotionLesepunkteResponse(items: Array<LesepunkteRequest>): __Observable<__StrictHttpResponse<ResponseArgsOfIDictionaryOfLongAndNullableInteger>> {
|
||||
let __params = this.newParams();
|
||||
let __headers = new HttpHeaders();
|
||||
let __body: any = null;
|
||||
__body = items;
|
||||
let req = new HttpRequest<any>('POST', this.rootUrl + `/promotion/lesepunkte`, __body, {
|
||||
headers: __headers,
|
||||
params: __params,
|
||||
responseType: 'json',
|
||||
});
|
||||
let req = new HttpRequest<any>(
|
||||
'POST',
|
||||
this.rootUrl + `/promotion/lesepunkte`,
|
||||
__body,
|
||||
{
|
||||
headers: __headers,
|
||||
params: __params,
|
||||
responseType: 'json'
|
||||
});
|
||||
|
||||
return this.http.request<any>(req).pipe(
|
||||
__filter((_r) => _r instanceof HttpResponse),
|
||||
__filter(_r => _r instanceof HttpResponse),
|
||||
__map((_r) => {
|
||||
return _r as __StrictHttpResponse<ResponseArgsOfIDictionaryOfLongAndNullableInteger>;
|
||||
}),
|
||||
})
|
||||
);
|
||||
}
|
||||
/**
|
||||
@@ -49,7 +54,9 @@ class PromotionService extends __BaseService {
|
||||
* @param items Ids und Mengen
|
||||
*/
|
||||
PromotionLesepunkte(items: Array<LesepunkteRequest>): __Observable<ResponseArgsOfIDictionaryOfLongAndNullableInteger> {
|
||||
return this.PromotionLesepunkteResponse(items).pipe(__map((_r) => _r.body as ResponseArgsOfIDictionaryOfLongAndNullableInteger));
|
||||
return this.PromotionLesepunkteResponse(items).pipe(
|
||||
__map(_r => _r.body as ResponseArgsOfIDictionaryOfLongAndNullableInteger)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -60,9 +67,7 @@ class PromotionService extends __BaseService {
|
||||
*
|
||||
* - `items`: Ids und Mengen
|
||||
*/
|
||||
PromotionLesepunkte2Response(
|
||||
params: PromotionService.PromotionLesepunkte2Params,
|
||||
): __Observable<__StrictHttpResponse<ResponseArgsOfIDictionaryOfLongAndNullableInteger>> {
|
||||
PromotionLesepunkte2Response(params: PromotionService.PromotionLesepunkte2Params): __Observable<__StrictHttpResponse<ResponseArgsOfIDictionaryOfLongAndNullableInteger>> {
|
||||
let __params = this.newParams();
|
||||
let __headers = new HttpHeaders();
|
||||
let __body: any = null;
|
||||
@@ -75,15 +80,14 @@ class PromotionService extends __BaseService {
|
||||
{
|
||||
headers: __headers,
|
||||
params: __params,
|
||||
responseType: 'json',
|
||||
},
|
||||
);
|
||||
responseType: 'json'
|
||||
});
|
||||
|
||||
return this.http.request<any>(req).pipe(
|
||||
__filter((_r) => _r instanceof HttpResponse),
|
||||
__filter(_r => _r instanceof HttpResponse),
|
||||
__map((_r) => {
|
||||
return _r as __StrictHttpResponse<ResponseArgsOfIDictionaryOfLongAndNullableInteger>;
|
||||
}),
|
||||
})
|
||||
);
|
||||
}
|
||||
/**
|
||||
@@ -94,18 +98,20 @@ class PromotionService extends __BaseService {
|
||||
*
|
||||
* - `items`: Ids und Mengen
|
||||
*/
|
||||
PromotionLesepunkte2(
|
||||
params: PromotionService.PromotionLesepunkte2Params,
|
||||
): __Observable<ResponseArgsOfIDictionaryOfLongAndNullableInteger> {
|
||||
return this.PromotionLesepunkte2Response(params).pipe(__map((_r) => _r.body as ResponseArgsOfIDictionaryOfLongAndNullableInteger));
|
||||
PromotionLesepunkte2(params: PromotionService.PromotionLesepunkte2Params): __Observable<ResponseArgsOfIDictionaryOfLongAndNullableInteger> {
|
||||
return this.PromotionLesepunkte2Response(params).pipe(
|
||||
__map(_r => _r.body as ResponseArgsOfIDictionaryOfLongAndNullableInteger)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
module PromotionService {
|
||||
|
||||
/**
|
||||
* Parameters for PromotionLesepunkte2
|
||||
*/
|
||||
export interface PromotionLesepunkte2Params {
|
||||
|
||||
/**
|
||||
* Lager PK (optional)
|
||||
*/
|
||||
@@ -118,4 +124,4 @@ module PromotionService {
|
||||
}
|
||||
}
|
||||
|
||||
export { PromotionService };
|
||||
export { PromotionService }
|
||||
|
||||
@@ -37,12 +37,16 @@ class SearchService extends __BaseService {
|
||||
static readonly SearchDetailByEANPath = '/s/ean/{ean}';
|
||||
static readonly SearchDetailByEAN2Path = '/stock/{stockId}/ean/{ean}';
|
||||
static readonly SearchSettingsPath = '/s/settings';
|
||||
static readonly SearchLoyaltySettingsPath = '/s/loyalty/settings';
|
||||
static readonly SearchSearchFilterPath = '/s/filter';
|
||||
static readonly SearchSearchSortPath = '/s/sort';
|
||||
static readonly SearchHistoryPath = '/s/history';
|
||||
static readonly SearchGetRecommendationsPath = '/s/recommendations/{digId}';
|
||||
|
||||
constructor(config: __Configuration, http: HttpClient) {
|
||||
constructor(
|
||||
config: __Configuration,
|
||||
http: HttpClient
|
||||
) {
|
||||
super(config, http);
|
||||
}
|
||||
|
||||
@@ -55,17 +59,21 @@ class SearchService extends __BaseService {
|
||||
let __headers = new HttpHeaders();
|
||||
let __body: any = null;
|
||||
__body = queryToken;
|
||||
let req = new HttpRequest<any>('POST', this.rootUrl + `/s/top`, __body, {
|
||||
headers: __headers,
|
||||
params: __params,
|
||||
responseType: 'json',
|
||||
});
|
||||
let req = new HttpRequest<any>(
|
||||
'POST',
|
||||
this.rootUrl + `/s/top`,
|
||||
__body,
|
||||
{
|
||||
headers: __headers,
|
||||
params: __params,
|
||||
responseType: 'json'
|
||||
});
|
||||
|
||||
return this.http.request<any>(req).pipe(
|
||||
__filter((_r) => _r instanceof HttpResponse),
|
||||
__filter(_r => _r instanceof HttpResponse),
|
||||
__map((_r) => {
|
||||
return _r as __StrictHttpResponse<ListResponseArgsOfItemDTO>;
|
||||
}),
|
||||
})
|
||||
);
|
||||
}
|
||||
/**
|
||||
@@ -73,7 +81,9 @@ class SearchService extends __BaseService {
|
||||
* @param queryToken Suchkriterien
|
||||
*/
|
||||
SearchTop(queryToken: QueryTokenDTO): __Observable<ListResponseArgsOfItemDTO> {
|
||||
return this.SearchTopResponse(queryToken).pipe(__map((_r) => _r.body as ListResponseArgsOfItemDTO));
|
||||
return this.SearchTopResponse(queryToken).pipe(
|
||||
__map(_r => _r.body as ListResponseArgsOfItemDTO)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -90,17 +100,21 @@ class SearchService extends __BaseService {
|
||||
let __body: any = null;
|
||||
|
||||
__body = params.queryToken;
|
||||
let req = new HttpRequest<any>('POST', this.rootUrl + `/stock/${encodeURIComponent(String(params.stockId))}/s/top`, __body, {
|
||||
headers: __headers,
|
||||
params: __params,
|
||||
responseType: 'json',
|
||||
});
|
||||
let req = new HttpRequest<any>(
|
||||
'POST',
|
||||
this.rootUrl + `/stock/${encodeURIComponent(String(params.stockId))}/s/top`,
|
||||
__body,
|
||||
{
|
||||
headers: __headers,
|
||||
params: __params,
|
||||
responseType: 'json'
|
||||
});
|
||||
|
||||
return this.http.request<any>(req).pipe(
|
||||
__filter((_r) => _r instanceof HttpResponse),
|
||||
__filter(_r => _r instanceof HttpResponse),
|
||||
__map((_r) => {
|
||||
return _r as __StrictHttpResponse<ListResponseArgsOfItemDTO>;
|
||||
}),
|
||||
})
|
||||
);
|
||||
}
|
||||
/**
|
||||
@@ -112,7 +126,9 @@ class SearchService extends __BaseService {
|
||||
* - `queryToken`: Suchkriterien
|
||||
*/
|
||||
SearchTop2(params: SearchService.SearchTop2Params): __Observable<ListResponseArgsOfItemDTO> {
|
||||
return this.SearchTop2Response(params).pipe(__map((_r) => _r.body as ListResponseArgsOfItemDTO));
|
||||
return this.SearchTop2Response(params).pipe(
|
||||
__map(_r => _r.body as ListResponseArgsOfItemDTO)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -124,17 +140,21 @@ class SearchService extends __BaseService {
|
||||
let __headers = new HttpHeaders();
|
||||
let __body: any = null;
|
||||
__body = queryToken;
|
||||
let req = new HttpRequest<any>('POST', this.rootUrl + `/s`, __body, {
|
||||
headers: __headers,
|
||||
params: __params,
|
||||
responseType: 'json',
|
||||
});
|
||||
let req = new HttpRequest<any>(
|
||||
'POST',
|
||||
this.rootUrl + `/s`,
|
||||
__body,
|
||||
{
|
||||
headers: __headers,
|
||||
params: __params,
|
||||
responseType: 'json'
|
||||
});
|
||||
|
||||
return this.http.request<any>(req).pipe(
|
||||
__filter((_r) => _r instanceof HttpResponse),
|
||||
__filter(_r => _r instanceof HttpResponse),
|
||||
__map((_r) => {
|
||||
return _r as __StrictHttpResponse<ListResponseArgsOfItemDTO>;
|
||||
}),
|
||||
})
|
||||
);
|
||||
}
|
||||
/**
|
||||
@@ -142,7 +162,9 @@ class SearchService extends __BaseService {
|
||||
* @param queryToken Suchkriterien
|
||||
*/
|
||||
SearchSearch(queryToken: QueryTokenDTO): __Observable<ListResponseArgsOfItemDTO> {
|
||||
return this.SearchSearchResponse(queryToken).pipe(__map((_r) => _r.body as ListResponseArgsOfItemDTO));
|
||||
return this.SearchSearchResponse(queryToken).pipe(
|
||||
__map(_r => _r.body as ListResponseArgsOfItemDTO)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -159,17 +181,21 @@ class SearchService extends __BaseService {
|
||||
let __body: any = null;
|
||||
|
||||
__body = params.queryToken;
|
||||
let req = new HttpRequest<any>('POST', this.rootUrl + `/stock/${encodeURIComponent(String(params.stockId))}/s`, __body, {
|
||||
headers: __headers,
|
||||
params: __params,
|
||||
responseType: 'json',
|
||||
});
|
||||
let req = new HttpRequest<any>(
|
||||
'POST',
|
||||
this.rootUrl + `/stock/${encodeURIComponent(String(params.stockId))}/s`,
|
||||
__body,
|
||||
{
|
||||
headers: __headers,
|
||||
params: __params,
|
||||
responseType: 'json'
|
||||
});
|
||||
|
||||
return this.http.request<any>(req).pipe(
|
||||
__filter((_r) => _r instanceof HttpResponse),
|
||||
__filter(_r => _r instanceof HttpResponse),
|
||||
__map((_r) => {
|
||||
return _r as __StrictHttpResponse<ListResponseArgsOfItemDTO>;
|
||||
}),
|
||||
})
|
||||
);
|
||||
}
|
||||
/**
|
||||
@@ -181,7 +207,9 @@ class SearchService extends __BaseService {
|
||||
* - `queryToken`: Suchkriterien
|
||||
*/
|
||||
SearchSearch2(params: SearchService.SearchSearch2Params): __Observable<ListResponseArgsOfItemDTO> {
|
||||
return this.SearchSearch2Response(params).pipe(__map((_r) => _r.body as ListResponseArgsOfItemDTO));
|
||||
return this.SearchSearch2Response(params).pipe(
|
||||
__map(_r => _r.body as ListResponseArgsOfItemDTO)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -193,17 +221,21 @@ class SearchService extends __BaseService {
|
||||
let __headers = new HttpHeaders();
|
||||
let __body: any = null;
|
||||
__body = queryToken;
|
||||
let req = new HttpRequest<any>('POST', this.rootUrl + `/s/complete`, __body, {
|
||||
headers: __headers,
|
||||
params: __params,
|
||||
responseType: 'json',
|
||||
});
|
||||
let req = new HttpRequest<any>(
|
||||
'POST',
|
||||
this.rootUrl + `/s/complete`,
|
||||
__body,
|
||||
{
|
||||
headers: __headers,
|
||||
params: __params,
|
||||
responseType: 'json'
|
||||
});
|
||||
|
||||
return this.http.request<any>(req).pipe(
|
||||
__filter((_r) => _r instanceof HttpResponse),
|
||||
__filter(_r => _r instanceof HttpResponse),
|
||||
__map((_r) => {
|
||||
return _r as __StrictHttpResponse<ListResponseArgsOfAutocompleteDTO>;
|
||||
}),
|
||||
})
|
||||
);
|
||||
}
|
||||
/**
|
||||
@@ -211,7 +243,9 @@ class SearchService extends __BaseService {
|
||||
* @param queryToken Suchbegriff
|
||||
*/
|
||||
SearchAutocomplete(queryToken: AutocompleteTokenDTO): __Observable<ListResponseArgsOfAutocompleteDTO> {
|
||||
return this.SearchAutocompleteResponse(queryToken).pipe(__map((_r) => _r.body as ListResponseArgsOfAutocompleteDTO));
|
||||
return this.SearchAutocompleteResponse(queryToken).pipe(
|
||||
__map(_r => _r.body as ListResponseArgsOfAutocompleteDTO)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -222,25 +256,27 @@ class SearchService extends __BaseService {
|
||||
*
|
||||
* - `queryToken`: Suchbegriff
|
||||
*/
|
||||
SearchAutocomplete2Response(
|
||||
params: SearchService.SearchAutocomplete2Params,
|
||||
): __Observable<__StrictHttpResponse<ListResponseArgsOfAutocompleteDTO>> {
|
||||
SearchAutocomplete2Response(params: SearchService.SearchAutocomplete2Params): __Observable<__StrictHttpResponse<ListResponseArgsOfAutocompleteDTO>> {
|
||||
let __params = this.newParams();
|
||||
let __headers = new HttpHeaders();
|
||||
let __body: any = null;
|
||||
|
||||
__body = params.queryToken;
|
||||
let req = new HttpRequest<any>('POST', this.rootUrl + `/stock/${encodeURIComponent(String(params.stockId))}/s/complete`, __body, {
|
||||
headers: __headers,
|
||||
params: __params,
|
||||
responseType: 'json',
|
||||
});
|
||||
let req = new HttpRequest<any>(
|
||||
'POST',
|
||||
this.rootUrl + `/stock/${encodeURIComponent(String(params.stockId))}/s/complete`,
|
||||
__body,
|
||||
{
|
||||
headers: __headers,
|
||||
params: __params,
|
||||
responseType: 'json'
|
||||
});
|
||||
|
||||
return this.http.request<any>(req).pipe(
|
||||
__filter((_r) => _r instanceof HttpResponse),
|
||||
__filter(_r => _r instanceof HttpResponse),
|
||||
__map((_r) => {
|
||||
return _r as __StrictHttpResponse<ListResponseArgsOfAutocompleteDTO>;
|
||||
}),
|
||||
})
|
||||
);
|
||||
}
|
||||
/**
|
||||
@@ -252,7 +288,9 @@ class SearchService extends __BaseService {
|
||||
* - `queryToken`: Suchbegriff
|
||||
*/
|
||||
SearchAutocomplete2(params: SearchService.SearchAutocomplete2Params): __Observable<ListResponseArgsOfAutocompleteDTO> {
|
||||
return this.SearchAutocomplete2Response(params).pipe(__map((_r) => _r.body as ListResponseArgsOfAutocompleteDTO));
|
||||
return this.SearchAutocomplete2Response(params).pipe(
|
||||
__map(_r => _r.body as ListResponseArgsOfAutocompleteDTO)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -264,17 +302,21 @@ class SearchService extends __BaseService {
|
||||
let __headers = new HttpHeaders();
|
||||
let __body: any = null;
|
||||
__body = ids;
|
||||
let req = new HttpRequest<any>('POST', this.rootUrl + `/s/byid`, __body, {
|
||||
headers: __headers,
|
||||
params: __params,
|
||||
responseType: 'json',
|
||||
});
|
||||
let req = new HttpRequest<any>(
|
||||
'POST',
|
||||
this.rootUrl + `/s/byid`,
|
||||
__body,
|
||||
{
|
||||
headers: __headers,
|
||||
params: __params,
|
||||
responseType: 'json'
|
||||
});
|
||||
|
||||
return this.http.request<any>(req).pipe(
|
||||
__filter((_r) => _r instanceof HttpResponse),
|
||||
__filter(_r => _r instanceof HttpResponse),
|
||||
__map((_r) => {
|
||||
return _r as __StrictHttpResponse<ListResponseArgsOfItemDTO>;
|
||||
}),
|
||||
})
|
||||
);
|
||||
}
|
||||
/**
|
||||
@@ -282,7 +324,9 @@ class SearchService extends __BaseService {
|
||||
* @param ids PKs
|
||||
*/
|
||||
SearchById(ids: Array<number>): __Observable<ListResponseArgsOfItemDTO> {
|
||||
return this.SearchByIdResponse(ids).pipe(__map((_r) => _r.body as ListResponseArgsOfItemDTO));
|
||||
return this.SearchByIdResponse(ids).pipe(
|
||||
__map(_r => _r.body as ListResponseArgsOfItemDTO)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -299,17 +343,21 @@ class SearchService extends __BaseService {
|
||||
let __body: any = null;
|
||||
|
||||
__body = params.ids;
|
||||
let req = new HttpRequest<any>('POST', this.rootUrl + `/stock/${encodeURIComponent(String(params.stockId))}/s/byid`, __body, {
|
||||
headers: __headers,
|
||||
params: __params,
|
||||
responseType: 'json',
|
||||
});
|
||||
let req = new HttpRequest<any>(
|
||||
'POST',
|
||||
this.rootUrl + `/stock/${encodeURIComponent(String(params.stockId))}/s/byid`,
|
||||
__body,
|
||||
{
|
||||
headers: __headers,
|
||||
params: __params,
|
||||
responseType: 'json'
|
||||
});
|
||||
|
||||
return this.http.request<any>(req).pipe(
|
||||
__filter((_r) => _r instanceof HttpResponse),
|
||||
__filter(_r => _r instanceof HttpResponse),
|
||||
__map((_r) => {
|
||||
return _r as __StrictHttpResponse<ListResponseArgsOfItemDTO>;
|
||||
}),
|
||||
})
|
||||
);
|
||||
}
|
||||
/**
|
||||
@@ -321,7 +369,9 @@ class SearchService extends __BaseService {
|
||||
* - `ids`: PKs
|
||||
*/
|
||||
SearchById2(params: SearchService.SearchById2Params): __Observable<ListResponseArgsOfItemDTO> {
|
||||
return this.SearchById2Response(params).pipe(__map((_r) => _r.body as ListResponseArgsOfItemDTO));
|
||||
return this.SearchById2Response(params).pipe(
|
||||
__map(_r => _r.body as ListResponseArgsOfItemDTO)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -333,17 +383,21 @@ class SearchService extends __BaseService {
|
||||
let __headers = new HttpHeaders();
|
||||
let __body: any = null;
|
||||
__body = eans;
|
||||
let req = new HttpRequest<any>('POST', this.rootUrl + `/s/byean`, __body, {
|
||||
headers: __headers,
|
||||
params: __params,
|
||||
responseType: 'json',
|
||||
});
|
||||
let req = new HttpRequest<any>(
|
||||
'POST',
|
||||
this.rootUrl + `/s/byean`,
|
||||
__body,
|
||||
{
|
||||
headers: __headers,
|
||||
params: __params,
|
||||
responseType: 'json'
|
||||
});
|
||||
|
||||
return this.http.request<any>(req).pipe(
|
||||
__filter((_r) => _r instanceof HttpResponse),
|
||||
__filter(_r => _r instanceof HttpResponse),
|
||||
__map((_r) => {
|
||||
return _r as __StrictHttpResponse<ListResponseArgsOfItemDTO>;
|
||||
}),
|
||||
})
|
||||
);
|
||||
}
|
||||
/**
|
||||
@@ -351,7 +405,9 @@ class SearchService extends __BaseService {
|
||||
* @param eans EANs
|
||||
*/
|
||||
SearchByEAN(eans: Array<string>): __Observable<ListResponseArgsOfItemDTO> {
|
||||
return this.SearchByEANResponse(eans).pipe(__map((_r) => _r.body as ListResponseArgsOfItemDTO));
|
||||
return this.SearchByEANResponse(eans).pipe(
|
||||
__map(_r => _r.body as ListResponseArgsOfItemDTO)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -368,17 +424,21 @@ class SearchService extends __BaseService {
|
||||
let __body: any = null;
|
||||
|
||||
__body = params.eans;
|
||||
let req = new HttpRequest<any>('POST', this.rootUrl + `/stock/${encodeURIComponent(String(params.stockId))}/s/byean`, __body, {
|
||||
headers: __headers,
|
||||
params: __params,
|
||||
responseType: 'json',
|
||||
});
|
||||
let req = new HttpRequest<any>(
|
||||
'POST',
|
||||
this.rootUrl + `/stock/${encodeURIComponent(String(params.stockId))}/s/byean`,
|
||||
__body,
|
||||
{
|
||||
headers: __headers,
|
||||
params: __params,
|
||||
responseType: 'json'
|
||||
});
|
||||
|
||||
return this.http.request<any>(req).pipe(
|
||||
__filter((_r) => _r instanceof HttpResponse),
|
||||
__filter(_r => _r instanceof HttpResponse),
|
||||
__map((_r) => {
|
||||
return _r as __StrictHttpResponse<ListResponseArgsOfItemDTO>;
|
||||
}),
|
||||
})
|
||||
);
|
||||
}
|
||||
/**
|
||||
@@ -390,7 +450,9 @@ class SearchService extends __BaseService {
|
||||
* - `eans`: EANs
|
||||
*/
|
||||
SearchByEAN2(params: SearchService.SearchByEAN2Params): __Observable<ListResponseArgsOfItemDTO> {
|
||||
return this.SearchByEAN2Response(params).pipe(__map((_r) => _r.body as ListResponseArgsOfItemDTO));
|
||||
return this.SearchByEAN2Response(params).pipe(
|
||||
__map(_r => _r.body as ListResponseArgsOfItemDTO)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -407,17 +469,21 @@ class SearchService extends __BaseService {
|
||||
let __body: any = null;
|
||||
__body = params.eans;
|
||||
|
||||
let req = new HttpRequest<any>('POST', this.rootUrl + `/branch/${encodeURIComponent(String(params.branchNumber))}/s/byean`, __body, {
|
||||
headers: __headers,
|
||||
params: __params,
|
||||
responseType: 'json',
|
||||
});
|
||||
let req = new HttpRequest<any>(
|
||||
'POST',
|
||||
this.rootUrl + `/branch/${encodeURIComponent(String(params.branchNumber))}/s/byean`,
|
||||
__body,
|
||||
{
|
||||
headers: __headers,
|
||||
params: __params,
|
||||
responseType: 'json'
|
||||
});
|
||||
|
||||
return this.http.request<any>(req).pipe(
|
||||
__filter((_r) => _r instanceof HttpResponse),
|
||||
__filter(_r => _r instanceof HttpResponse),
|
||||
__map((_r) => {
|
||||
return _r as __StrictHttpResponse<ListResponseArgsOfItemDTO>;
|
||||
}),
|
||||
})
|
||||
);
|
||||
}
|
||||
/**
|
||||
@@ -429,7 +495,9 @@ class SearchService extends __BaseService {
|
||||
* - `branchNumber`: Filiale-Nr (optional)
|
||||
*/
|
||||
SearchByEAN3(params: SearchService.SearchByEAN3Params): __Observable<ListResponseArgsOfItemDTO> {
|
||||
return this.SearchByEAN3Response(params).pipe(__map((_r) => _r.body as ListResponseArgsOfItemDTO));
|
||||
return this.SearchByEAN3Response(params).pipe(
|
||||
__map(_r => _r.body as ListResponseArgsOfItemDTO)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -446,17 +514,21 @@ class SearchService extends __BaseService {
|
||||
let __body: any = null;
|
||||
|
||||
if (params.doNotTrack != null) __params = __params.set('doNotTrack', params.doNotTrack.toString());
|
||||
let req = new HttpRequest<any>('GET', this.rootUrl + `/s/${encodeURIComponent(String(params.id))}`, __body, {
|
||||
headers: __headers,
|
||||
params: __params,
|
||||
responseType: 'json',
|
||||
});
|
||||
let req = new HttpRequest<any>(
|
||||
'GET',
|
||||
this.rootUrl + `/s/${encodeURIComponent(String(params.id))}`,
|
||||
__body,
|
||||
{
|
||||
headers: __headers,
|
||||
params: __params,
|
||||
responseType: 'json'
|
||||
});
|
||||
|
||||
return this.http.request<any>(req).pipe(
|
||||
__filter((_r) => _r instanceof HttpResponse),
|
||||
__filter(_r => _r instanceof HttpResponse),
|
||||
__map((_r) => {
|
||||
return _r as __StrictHttpResponse<ResponseArgsOfItemDTO>;
|
||||
}),
|
||||
})
|
||||
);
|
||||
}
|
||||
/**
|
||||
@@ -468,7 +540,9 @@ class SearchService extends __BaseService {
|
||||
* - `doNotTrack`:
|
||||
*/
|
||||
SearchDetail(params: SearchService.SearchDetailParams): __Observable<ResponseArgsOfItemDTO> {
|
||||
return this.SearchDetailResponse(params).pipe(__map((_r) => _r.body as ResponseArgsOfItemDTO));
|
||||
return this.SearchDetailResponse(params).pipe(
|
||||
__map(_r => _r.body as ResponseArgsOfItemDTO)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -486,6 +560,7 @@ class SearchService extends __BaseService {
|
||||
let __headers = new HttpHeaders();
|
||||
let __body: any = null;
|
||||
|
||||
|
||||
if (params.doNotTrack != null) __params = __params.set('doNotTrack', params.doNotTrack.toString());
|
||||
let req = new HttpRequest<any>(
|
||||
'GET',
|
||||
@@ -494,15 +569,14 @@ class SearchService extends __BaseService {
|
||||
{
|
||||
headers: __headers,
|
||||
params: __params,
|
||||
responseType: 'json',
|
||||
},
|
||||
);
|
||||
responseType: 'json'
|
||||
});
|
||||
|
||||
return this.http.request<any>(req).pipe(
|
||||
__filter((_r) => _r instanceof HttpResponse),
|
||||
__filter(_r => _r instanceof HttpResponse),
|
||||
__map((_r) => {
|
||||
return _r as __StrictHttpResponse<ResponseArgsOfItemDTO>;
|
||||
}),
|
||||
})
|
||||
);
|
||||
}
|
||||
/**
|
||||
@@ -516,7 +590,9 @@ class SearchService extends __BaseService {
|
||||
* - `doNotTrack`:
|
||||
*/
|
||||
SearchDetail2(params: SearchService.SearchDetail2Params): __Observable<ResponseArgsOfItemDTO> {
|
||||
return this.SearchDetail2Response(params).pipe(__map((_r) => _r.body as ResponseArgsOfItemDTO));
|
||||
return this.SearchDetail2Response(params).pipe(
|
||||
__map(_r => _r.body as ResponseArgsOfItemDTO)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -528,17 +604,21 @@ class SearchService extends __BaseService {
|
||||
let __headers = new HttpHeaders();
|
||||
let __body: any = null;
|
||||
|
||||
let req = new HttpRequest<any>('GET', this.rootUrl + `/s/ean/${encodeURIComponent(String(ean))}`, __body, {
|
||||
headers: __headers,
|
||||
params: __params,
|
||||
responseType: 'json',
|
||||
});
|
||||
let req = new HttpRequest<any>(
|
||||
'GET',
|
||||
this.rootUrl + `/s/ean/${encodeURIComponent(String(ean))}`,
|
||||
__body,
|
||||
{
|
||||
headers: __headers,
|
||||
params: __params,
|
||||
responseType: 'json'
|
||||
});
|
||||
|
||||
return this.http.request<any>(req).pipe(
|
||||
__filter((_r) => _r instanceof HttpResponse),
|
||||
__filter(_r => _r instanceof HttpResponse),
|
||||
__map((_r) => {
|
||||
return _r as __StrictHttpResponse<ResponseArgsOfItemDTO>;
|
||||
}),
|
||||
})
|
||||
);
|
||||
}
|
||||
/**
|
||||
@@ -546,7 +626,9 @@ class SearchService extends __BaseService {
|
||||
* @param ean EAN
|
||||
*/
|
||||
SearchDetailByEAN(ean: string): __Observable<ResponseArgsOfItemDTO> {
|
||||
return this.SearchDetailByEANResponse(ean).pipe(__map((_r) => _r.body as ResponseArgsOfItemDTO));
|
||||
return this.SearchDetailByEANResponse(ean).pipe(
|
||||
__map(_r => _r.body as ResponseArgsOfItemDTO)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -562,6 +644,7 @@ class SearchService extends __BaseService {
|
||||
let __headers = new HttpHeaders();
|
||||
let __body: any = null;
|
||||
|
||||
|
||||
let req = new HttpRequest<any>(
|
||||
'GET',
|
||||
this.rootUrl + `/stock/${encodeURIComponent(String(params.stockId))}/ean/${encodeURIComponent(String(params.ean))}`,
|
||||
@@ -569,15 +652,14 @@ class SearchService extends __BaseService {
|
||||
{
|
||||
headers: __headers,
|
||||
params: __params,
|
||||
responseType: 'json',
|
||||
},
|
||||
);
|
||||
responseType: 'json'
|
||||
});
|
||||
|
||||
return this.http.request<any>(req).pipe(
|
||||
__filter((_r) => _r instanceof HttpResponse),
|
||||
__filter(_r => _r instanceof HttpResponse),
|
||||
__map((_r) => {
|
||||
return _r as __StrictHttpResponse<ResponseArgsOfItemDTO>;
|
||||
}),
|
||||
})
|
||||
);
|
||||
}
|
||||
/**
|
||||
@@ -589,7 +671,9 @@ class SearchService extends __BaseService {
|
||||
* - `ean`: EAN
|
||||
*/
|
||||
SearchDetailByEAN2(params: SearchService.SearchDetailByEAN2Params): __Observable<ResponseArgsOfItemDTO> {
|
||||
return this.SearchDetailByEAN2Response(params).pipe(__map((_r) => _r.body as ResponseArgsOfItemDTO));
|
||||
return this.SearchDetailByEAN2Response(params).pipe(
|
||||
__map(_r => _r.body as ResponseArgsOfItemDTO)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -599,24 +683,63 @@ class SearchService extends __BaseService {
|
||||
let __params = this.newParams();
|
||||
let __headers = new HttpHeaders();
|
||||
let __body: any = null;
|
||||
let req = new HttpRequest<any>('GET', this.rootUrl + `/s/settings`, __body, {
|
||||
headers: __headers,
|
||||
params: __params,
|
||||
responseType: 'json',
|
||||
});
|
||||
let req = new HttpRequest<any>(
|
||||
'GET',
|
||||
this.rootUrl + `/s/settings`,
|
||||
__body,
|
||||
{
|
||||
headers: __headers,
|
||||
params: __params,
|
||||
responseType: 'json'
|
||||
});
|
||||
|
||||
return this.http.request<any>(req).pipe(
|
||||
__filter((_r) => _r instanceof HttpResponse),
|
||||
__filter(_r => _r instanceof HttpResponse),
|
||||
__map((_r) => {
|
||||
return _r as __StrictHttpResponse<ResponseArgsOfUISettingsDTO>;
|
||||
}),
|
||||
})
|
||||
);
|
||||
}
|
||||
/**
|
||||
* Settings
|
||||
*/
|
||||
SearchSettings(): __Observable<ResponseArgsOfUISettingsDTO> {
|
||||
return this.SearchSettingsResponse().pipe(__map((_r) => _r.body as ResponseArgsOfUISettingsDTO));
|
||||
return this.SearchSettingsResponse().pipe(
|
||||
__map(_r => _r.body as ResponseArgsOfUISettingsDTO)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Settings
|
||||
*/
|
||||
SearchLoyaltySettingsResponse(): __Observable<__StrictHttpResponse<ResponseArgsOfUISettingsDTO>> {
|
||||
let __params = this.newParams();
|
||||
let __headers = new HttpHeaders();
|
||||
let __body: any = null;
|
||||
let req = new HttpRequest<any>(
|
||||
'GET',
|
||||
this.rootUrl + `/s/loyalty/settings`,
|
||||
__body,
|
||||
{
|
||||
headers: __headers,
|
||||
params: __params,
|
||||
responseType: 'json'
|
||||
});
|
||||
|
||||
return this.http.request<any>(req).pipe(
|
||||
__filter(_r => _r instanceof HttpResponse),
|
||||
__map((_r) => {
|
||||
return _r as __StrictHttpResponse<ResponseArgsOfUISettingsDTO>;
|
||||
})
|
||||
);
|
||||
}
|
||||
/**
|
||||
* Settings
|
||||
*/
|
||||
SearchLoyaltySettings(): __Observable<ResponseArgsOfUISettingsDTO> {
|
||||
return this.SearchLoyaltySettingsResponse().pipe(
|
||||
__map(_r => _r.body as ResponseArgsOfUISettingsDTO)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -626,24 +749,30 @@ class SearchService extends __BaseService {
|
||||
let __params = this.newParams();
|
||||
let __headers = new HttpHeaders();
|
||||
let __body: any = null;
|
||||
let req = new HttpRequest<any>('GET', this.rootUrl + `/s/filter`, __body, {
|
||||
headers: __headers,
|
||||
params: __params,
|
||||
responseType: 'json',
|
||||
});
|
||||
let req = new HttpRequest<any>(
|
||||
'GET',
|
||||
this.rootUrl + `/s/filter`,
|
||||
__body,
|
||||
{
|
||||
headers: __headers,
|
||||
params: __params,
|
||||
responseType: 'json'
|
||||
});
|
||||
|
||||
return this.http.request<any>(req).pipe(
|
||||
__filter((_r) => _r instanceof HttpResponse),
|
||||
__filter(_r => _r instanceof HttpResponse),
|
||||
__map((_r) => {
|
||||
return _r as __StrictHttpResponse<ResponseArgsOfIEnumerableOfInputGroupDTO>;
|
||||
}),
|
||||
})
|
||||
);
|
||||
}
|
||||
/**
|
||||
* Filter
|
||||
*/
|
||||
SearchSearchFilter(): __Observable<ResponseArgsOfIEnumerableOfInputGroupDTO> {
|
||||
return this.SearchSearchFilterResponse().pipe(__map((_r) => _r.body as ResponseArgsOfIEnumerableOfInputGroupDTO));
|
||||
return this.SearchSearchFilterResponse().pipe(
|
||||
__map(_r => _r.body as ResponseArgsOfIEnumerableOfInputGroupDTO)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -653,24 +782,30 @@ class SearchService extends __BaseService {
|
||||
let __params = this.newParams();
|
||||
let __headers = new HttpHeaders();
|
||||
let __body: any = null;
|
||||
let req = new HttpRequest<any>('GET', this.rootUrl + `/s/sort`, __body, {
|
||||
headers: __headers,
|
||||
params: __params,
|
||||
responseType: 'json',
|
||||
});
|
||||
let req = new HttpRequest<any>(
|
||||
'GET',
|
||||
this.rootUrl + `/s/sort`,
|
||||
__body,
|
||||
{
|
||||
headers: __headers,
|
||||
params: __params,
|
||||
responseType: 'json'
|
||||
});
|
||||
|
||||
return this.http.request<any>(req).pipe(
|
||||
__filter((_r) => _r instanceof HttpResponse),
|
||||
__filter(_r => _r instanceof HttpResponse),
|
||||
__map((_r) => {
|
||||
return _r as __StrictHttpResponse<ResponseArgsOfIEnumerableOfOrderByDTO>;
|
||||
}),
|
||||
})
|
||||
);
|
||||
}
|
||||
/**
|
||||
* Filter
|
||||
*/
|
||||
SearchSearchSort(): __Observable<ResponseArgsOfIEnumerableOfOrderByDTO> {
|
||||
return this.SearchSearchSortResponse().pipe(__map((_r) => _r.body as ResponseArgsOfIEnumerableOfOrderByDTO));
|
||||
return this.SearchSearchSortResponse().pipe(
|
||||
__map(_r => _r.body as ResponseArgsOfIEnumerableOfOrderByDTO)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -682,17 +817,21 @@ class SearchService extends __BaseService {
|
||||
let __headers = new HttpHeaders();
|
||||
let __body: any = null;
|
||||
if (take != null) __params = __params.set('take', take.toString());
|
||||
let req = new HttpRequest<any>('GET', this.rootUrl + `/s/history`, __body, {
|
||||
headers: __headers,
|
||||
params: __params,
|
||||
responseType: 'json',
|
||||
});
|
||||
let req = new HttpRequest<any>(
|
||||
'GET',
|
||||
this.rootUrl + `/s/history`,
|
||||
__body,
|
||||
{
|
||||
headers: __headers,
|
||||
params: __params,
|
||||
responseType: 'json'
|
||||
});
|
||||
|
||||
return this.http.request<any>(req).pipe(
|
||||
__filter((_r) => _r instanceof HttpResponse),
|
||||
__filter(_r => _r instanceof HttpResponse),
|
||||
__map((_r) => {
|
||||
return _r as __StrictHttpResponse<ResponseArgsOfIEnumerableOfQueryTokenDTO>;
|
||||
}),
|
||||
})
|
||||
);
|
||||
}
|
||||
/**
|
||||
@@ -700,7 +839,9 @@ class SearchService extends __BaseService {
|
||||
* @param take Take
|
||||
*/
|
||||
SearchHistory(take?: null | number): __Observable<ResponseArgsOfIEnumerableOfQueryTokenDTO> {
|
||||
return this.SearchHistoryResponse(take).pipe(__map((_r) => _r.body as ResponseArgsOfIEnumerableOfQueryTokenDTO));
|
||||
return this.SearchHistoryResponse(take).pipe(
|
||||
__map(_r => _r.body as ResponseArgsOfIEnumerableOfQueryTokenDTO)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -713,25 +854,27 @@ class SearchService extends __BaseService {
|
||||
*
|
||||
* @return ResponseArgs of Recomendations
|
||||
*/
|
||||
SearchGetRecommendationsResponse(
|
||||
params: SearchService.SearchGetRecommendationsParams,
|
||||
): __Observable<__StrictHttpResponse<ResponseArgsOfIEnumerableOfItemDTO>> {
|
||||
SearchGetRecommendationsResponse(params: SearchService.SearchGetRecommendationsParams): __Observable<__StrictHttpResponse<ResponseArgsOfIEnumerableOfItemDTO>> {
|
||||
let __params = this.newParams();
|
||||
let __headers = new HttpHeaders();
|
||||
let __body: any = null;
|
||||
|
||||
if (params.sessionId != null) __params = __params.set('sessionId', params.sessionId.toString());
|
||||
let req = new HttpRequest<any>('GET', this.rootUrl + `/s/recommendations/${encodeURIComponent(String(params.digId))}`, __body, {
|
||||
headers: __headers,
|
||||
params: __params,
|
||||
responseType: 'json',
|
||||
});
|
||||
let req = new HttpRequest<any>(
|
||||
'GET',
|
||||
this.rootUrl + `/s/recommendations/${encodeURIComponent(String(params.digId))}`,
|
||||
__body,
|
||||
{
|
||||
headers: __headers,
|
||||
params: __params,
|
||||
responseType: 'json'
|
||||
});
|
||||
|
||||
return this.http.request<any>(req).pipe(
|
||||
__filter((_r) => _r instanceof HttpResponse),
|
||||
__filter(_r => _r instanceof HttpResponse),
|
||||
__map((_r) => {
|
||||
return _r as __StrictHttpResponse<ResponseArgsOfIEnumerableOfItemDTO>;
|
||||
}),
|
||||
})
|
||||
);
|
||||
}
|
||||
/**
|
||||
@@ -745,15 +888,19 @@ class SearchService extends __BaseService {
|
||||
* @return ResponseArgs of Recomendations
|
||||
*/
|
||||
SearchGetRecommendations(params: SearchService.SearchGetRecommendationsParams): __Observable<ResponseArgsOfIEnumerableOfItemDTO> {
|
||||
return this.SearchGetRecommendationsResponse(params).pipe(__map((_r) => _r.body as ResponseArgsOfIEnumerableOfItemDTO));
|
||||
return this.SearchGetRecommendationsResponse(params).pipe(
|
||||
__map(_r => _r.body as ResponseArgsOfIEnumerableOfItemDTO)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
module SearchService {
|
||||
|
||||
/**
|
||||
* Parameters for SearchTop2
|
||||
*/
|
||||
export interface SearchTop2Params {
|
||||
|
||||
/**
|
||||
* Lager PK (optional)
|
||||
*/
|
||||
@@ -769,6 +916,7 @@ module SearchService {
|
||||
* Parameters for SearchSearch2
|
||||
*/
|
||||
export interface SearchSearch2Params {
|
||||
|
||||
/**
|
||||
* Lager PK (optional)
|
||||
*/
|
||||
@@ -784,6 +932,7 @@ module SearchService {
|
||||
* Parameters for SearchAutocomplete2
|
||||
*/
|
||||
export interface SearchAutocomplete2Params {
|
||||
|
||||
/**
|
||||
* Lager PK (optional)
|
||||
*/
|
||||
@@ -799,6 +948,7 @@ module SearchService {
|
||||
* Parameters for SearchById2
|
||||
*/
|
||||
export interface SearchById2Params {
|
||||
|
||||
/**
|
||||
* Lager PK (optional)
|
||||
*/
|
||||
@@ -814,6 +964,7 @@ module SearchService {
|
||||
* Parameters for SearchByEAN2
|
||||
*/
|
||||
export interface SearchByEAN2Params {
|
||||
|
||||
/**
|
||||
* Lager PK (optional)
|
||||
*/
|
||||
@@ -829,6 +980,7 @@ module SearchService {
|
||||
* Parameters for SearchByEAN3
|
||||
*/
|
||||
export interface SearchByEAN3Params {
|
||||
|
||||
/**
|
||||
* EANs
|
||||
*/
|
||||
@@ -844,6 +996,7 @@ module SearchService {
|
||||
* Parameters for SearchDetail
|
||||
*/
|
||||
export interface SearchDetailParams {
|
||||
|
||||
/**
|
||||
* PK
|
||||
*/
|
||||
@@ -855,6 +1008,7 @@ module SearchService {
|
||||
* Parameters for SearchDetail2
|
||||
*/
|
||||
export interface SearchDetail2Params {
|
||||
|
||||
/**
|
||||
* Lager PK (optional)
|
||||
*/
|
||||
@@ -871,6 +1025,7 @@ module SearchService {
|
||||
* Parameters for SearchDetailByEAN2
|
||||
*/
|
||||
export interface SearchDetailByEAN2Params {
|
||||
|
||||
/**
|
||||
* Lager PK (optional)
|
||||
*/
|
||||
@@ -895,4 +1050,4 @@ module SearchService {
|
||||
}
|
||||
}
|
||||
|
||||
export { SearchService };
|
||||
export { SearchService }
|
||||
|
||||
@@ -6,4 +6,4 @@ import { HttpResponse } from '@angular/common/http';
|
||||
*/
|
||||
export type StrictHttpResponse<T> = HttpResponse<T> & {
|
||||
readonly body: T;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -31,8 +31,9 @@ const PARAMETER_CODEC = new ParameterCodec();
|
||||
export class BaseService {
|
||||
constructor(
|
||||
protected config: CheckoutConfiguration,
|
||||
protected http: HttpClient,
|
||||
) {}
|
||||
protected http: HttpClient
|
||||
) {
|
||||
}
|
||||
|
||||
private _rootUrl: string = '';
|
||||
|
||||
@@ -56,7 +57,7 @@ export class BaseService {
|
||||
*/
|
||||
protected newParams(): HttpParams {
|
||||
return new HttpParams({
|
||||
encoder: PARAMETER_CODEC,
|
||||
encoder: PARAMETER_CODEC
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import { Injectable } from '@angular/core';
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class CheckoutConfiguration {
|
||||
rootUrl: string = 'https://isa-test.paragon-data.net';
|
||||
rootUrl: string = 'https://isa-test.paragon-data.net/checkout/v6';
|
||||
}
|
||||
|
||||
export interface CheckoutConfigurationInterface {
|
||||
|
||||
@@ -131,8 +131,9 @@ export { PaymentType } from './models/payment-type';
|
||||
export { CountryTargetDTO } from './models/country-target-dto';
|
||||
export { BranchTargetDTO } from './models/branch-target-dto';
|
||||
export { EntityDTOBaseOfShopDTOAndIShop } from './models/entity-dtobase-of-shop-dtoand-ishop';
|
||||
export { CampaignDTO } from './models/campaign-dto';
|
||||
export { LoyaltyDTO } from './models/loyalty-dto';
|
||||
export { PromotionDTO } from './models/promotion-dto';
|
||||
export { EntityDTOBaseOfShoppingCartItemDTOAndIShoppingCartItem } from './models/entity-dtobase-of-shopping-cart-item-dtoand-ishopping-cart-item';
|
||||
export { EntityDTOBaseOfCheckoutItemDTOAndICheckoutItem } from './models/entity-dtobase-of-checkout-item-dtoand-icheckout-item';
|
||||
export { DisplayItemDTO } from './models/display-item-dto';
|
||||
export { EntityDTOBaseOfCheckoutDeliveryDTOAndICheckoutDelivery } from './models/entity-dtobase-of-checkout-delivery-dtoand-icheckout-delivery';
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
/* tslint:disable */
|
||||
import { AvailabilityDTO } from './availability-dto';
|
||||
import { CampaignDTO } from './campaign-dto';
|
||||
import { EntityDTOContainerOfDestinationDTO } from './entity-dtocontainer-of-destination-dto';
|
||||
import { ItemType } from './item-type';
|
||||
import { LoyaltyDTO } from './loyalty-dto';
|
||||
import { ProductDTO } from './product-dto';
|
||||
import { PromotionDTO } from './promotion-dto';
|
||||
import { Price } from './price';
|
||||
export interface AddToShoppingCartDTO {
|
||||
availability?: AvailabilityDTO;
|
||||
campaign?: CampaignDTO;
|
||||
destination?: EntityDTOContainerOfDestinationDTO;
|
||||
itemType?: ItemType;
|
||||
loyalty?: LoyaltyDTO;
|
||||
product?: ProductDTO;
|
||||
promotion?: PromotionDTO;
|
||||
quantity: number;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user