Compare commits

...

62 Commits

Author SHA1 Message Date
Lorenz Hilpert
ddd1b81d0d Merge branch 'feature/responsive-customer-orders' into feature/responsive-customer-orders-breakpoints 2023-05-24 15:45:20 +02:00
Nino
c79a1cdad1 #2605 Focus Searchbox on Navigation Click - Finish first Version of Article Search Responsive Design - Reset customer Orders back to pre Responsive 2023-05-23 17:56:38 +02:00
Nino
edf978f5cf #3387 Hover Styling on Article Search Result List 2023-05-23 16:52:12 +02:00
Nino
4bfe35a8b9 Bugfix OrderBy Tablet Navigation 2023-05-23 16:07:46 +02:00
Nino
4b7c26b009 Update new Filter and Branch Selector Changes from ISA-Integration 2023-05-22 17:26:15 +02:00
Nino
2a442dde85 Filter Design angepasst 2023-05-17 16:33:11 +02:00
Nino
45bb39f466 Merge branch 'feature/responsive-customer-orders' of https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend into feature/responsive-customer-orders 2023-05-17 16:26:18 +02:00
Nino
b2731432ed #4009 Alle Filter Entfernen eingebaut in Artikelsuche Filter Overlay 2023-05-17 16:25:46 +02:00
Nino Righi
8c424bab43 Merge branch 'feature/responsive-customer-orders' into feature/responsive-customer-orders-breakpoints 2023-05-16 16:33:54 +02:00
Nino Righi
571c748a66 Show Outlet Bugfix 2023-05-16 16:26:14 +02:00
Nino Righi
c104f36255 Updated Breakpoint, set MaxWidth to 1920px 2023-05-16 14:31:28 +02:00
Lorenz Hilpert
672245c467 Breakpoints RD 2023-05-16 10:54:47 +02:00
Nino Righi
0ae92e34c6 Breakpoints Handling Improved 2023-05-15 18:44:03 +02:00
Nino Righi
ce72ce48b2 New Breakpoints Update 2023-05-12 17:48:27 +02:00
Nino Righi
bd5ec27425 Merge branch 'develop' into feature/responsive-customer-orders 2023-05-11 16:54:54 +02:00
Lorenz Hilpert
4dc98f7980 Shell Styling 2023-05-11 16:42:37 +02:00
Lorenz Hilpert
c89ee18db3 Update env service added matcher for screen sizes 2023-05-11 16:10:05 +02:00
Lorenz Hilpert
c4aa7999a6 Shell auf max breite 1440px und zentriert 2023-05-11 15:41:09 +02:00
Nino Righi
27a83242c5 Filter Adjustment and Bugfix 2023-05-09 18:01:56 +02:00
Nino Righi
7bf23c4bcc Improved Routing and QueryParams handling 2023-05-09 17:34:30 +02:00
Nino Righi
0bc80e12aa #4000 Reset Selected Branch on Shell Navigation click, Product Search and Customer Orders 2023-05-09 14:40:44 +02:00
Nino Righi
b586e3da9e Responsive Design - Tablet Styling Bugfixes and Adjustments - Filter Layout - Container Heights - Routing Bugfixes 2023-05-08 17:09:15 +02:00
Nino Righi
a433f2cfe4 Responsive Design Article Result List Add Item To Cart via Select Bullets 2023-05-05 16:50:21 +02:00
Nino Righi
ff368d68b7 Responsive Design Order by Ipad fix 2023-05-04 15:09:05 +02:00
Nino Righi
40ebd72263 Article Search Results Searchbox Responsive Design and Filter Box Shadow Fix 2023-05-03 17:00:11 +02:00
Nino Righi
11eca1e25a Added new Icons, Fixed Searchbox Autocomplete 2023-05-03 16:20:12 +02:00
Nino Righi
b8d0153232 Filter Responsive Design 2023-05-02 18:08:18 +02:00
Nino Righi
0a195e78e5 OrderBy Responsive Design 2023-05-02 15:37:48 +02:00
Nino Righi
97c26f5fa1 Finished Article Details Styling and Started Filter Responsive Design 2023-04-28 18:24:57 +02:00
Nino Righi
8dac64d5b8 Merge branch 'develop' into feature/responsive-customer-orders 2023-04-28 10:21:46 +02:00
Nino Righi
2c295b0797 Article Details Page Responsive Design Layout and Styling Refactor 2023-04-27 18:31:28 +02:00
Lorenz Hilpert
696015b6a4 Styling for Searchbox 2023-04-27 18:08:18 +02:00
Lorenz Hilpert
f2f70e1d83 Searchbox Update 2023-04-27 17:14:49 +02:00
Lorenz Hilpert
4d1dbaa2f3 Moved searchbox and filter to shared 2023-04-27 14:26:50 +02:00
Nino Righi
4344f4617c Merge branch 'develop' into feature/responsive-customer-orders 2023-04-27 10:56:36 +02:00
Nino Righi
c451d2b329 Update Styling Catalog Main and Results fullscreen 2023-04-26 18:00:14 +02:00
Nino Righi
96d1a4b826 Page Catalog Layout Changes 2023-04-26 15:55:07 +02:00
Lorenz Hilpert
595bb27d99 Shell Max Content Width 2023-04-26 10:42:15 +02:00
Nino Righi
958a388fb5 Updated Navigation Service Implementation, Unit Test Fixes 2023-04-25 17:09:13 +02:00
Nino Righi
a908767f08 Merge branch 'develop' into feature/responsive-customer-orders 2023-04-24 17:41:36 +02:00
Nino Righi
3c0406031a Process, Breadcrumb, Shell Article Search Navigation Update 2023-04-24 16:47:31 +02:00
Lorenz Hilpert
7b72532c9e Added Config - Missing Icons 2023-04-24 16:31:25 +02:00
Lorenz Hilpert
6b756fe893 Merge branch 'release/2.3' into develop 2023-04-24 15:35:34 +02:00
Lorenz Hilpert
54c7f51766 #3008 - Fix Process Tab Close 2023-04-24 15:34:29 +02:00
Lorenz Hilpert
94b787655e #3008 Navigation durch Bereich wechseln CTA 2023-04-24 15:29:58 +02:00
Lorenz Hilpert
cf1c4d37b9 Update Package Lock 2023-04-24 14:45:53 +02:00
Lorenz Hilpert
ada16bac6c Merge branch 'develop' into feature/rd-navigation-shell 2023-04-24 14:35:31 +02:00
Lorenz Hilpert
eefb6062c7 removed fdescribed 2023-04-21 18:49:57 +02:00
Lorenz Hilpert
470a451168 Unit Tests for ShellSideMenuComponent 2023-04-21 18:49:37 +02:00
Lorenz Hilpert
e3b018c5f7 remove fdescribe 2023-04-21 17:45:12 +02:00
Lorenz Hilpert
bd695e21d4 RD Navigation Unit Tests - ProcessBar 2023-04-21 17:44:49 +02:00
Nino Righi
79bb9b8c11 Routing and Breadcrumb Update Article Search, Created Navigation Service 2023-04-21 17:12:55 +02:00
Nino Righi
341b202bc4 Kundenbestellungen Zwischencommit 2023-04-20 18:10:00 +02:00
Nino Righi
f284dc1db5 Article Search and Customer Order Filter Navigation Update Based on previous Routes, Updated Routing on Tablet 2023-04-18 17:36:08 +02:00
Lorenz Hilpert
aaf156cee3 Merge branch 'develop' into feature/rd-navigation-shell 2023-04-18 14:11:34 +02:00
Lorenz Hilpert
e8020ffde6 RD Shell - Navigation 2023-04-18 14:09:36 +02:00
Nino Righi
2144ec838c Customer Order Splitscreen Navigation Update 2023-04-06 16:12:29 +02:00
Nino Righi
bff10cb2ff Customer Order Changes 2023-04-05 11:04:38 +02:00
Nino Righi
d303b1444b Zoom Update and Article Result List Styling Update 2023-04-03 17:54:08 +02:00
Nino Righi
150e7965ee Zwischencommit 2023-03-29 12:57:05 +02:00
Nino Righi
3fcf3d9396 Responsive Design Splitscreen Article Search Results Update 2023-03-28 15:08:31 +02:00
Nino Righi
52278b8baf Initial Responsive Design Implementation based on Article Search 2023-03-24 17:19:59 +01:00
397 changed files with 9609 additions and 4335 deletions

View File

@@ -3,6 +3,5 @@
"johnpapa.angular2",
"esbenp.prettier-vscode",
"angular.ng-template",
"eg2.vscode-npm-script"
]
}

View File

@@ -375,37 +375,6 @@
}
}
},
"@shell/breadcrumb": {
"projectType": "library",
"root": "apps/shell/breadcrumb",
"sourceRoot": "apps/shell/breadcrumb/src",
"prefix": "shell",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:ng-packagr",
"options": {
"tsConfig": "apps/shell/breadcrumb/tsconfig.lib.json",
"project": "apps/shell/breadcrumb/ng-package.json"
},
"configurations": {
"production": {
"tsConfig": "apps/shell/breadcrumb/tsconfig.lib.prod.json"
}
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"tsConfig": "apps/shell/breadcrumb/tsconfig.spec.json",
"karmaConfig": "karma.conf.js",
"polyfills": [
"zone.js",
"zone.js/testing"
]
}
}
}
},
"@domain/defs": {
"projectType": "library",
"root": "apps/domain/defs",
@@ -840,37 +809,6 @@
}
}
},
"@shell/header": {
"projectType": "library",
"root": "apps/shell/header",
"sourceRoot": "apps/shell/header/src",
"prefix": "shell",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:ng-packagr",
"options": {
"tsConfig": "apps/shell/header/tsconfig.lib.json",
"project": "apps/shell/header/ng-package.json"
},
"configurations": {
"production": {
"tsConfig": "apps/shell/header/tsconfig.lib.prod.json"
}
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"tsConfig": "apps/shell/header/tsconfig.spec.json",
"karmaConfig": "karma.conf.js",
"polyfills": [
"zone.js",
"zone.js/testing"
]
}
}
}
},
"@modal/reorder": {
"projectType": "library",
"root": "apps/modal/reorder",
@@ -1058,74 +996,6 @@
}
}
},
"@shell/footer": {
"projectType": "library",
"root": "apps/shell/footer",
"sourceRoot": "apps/shell/footer/src",
"prefix": "lib",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:ng-packagr",
"options": {
"project": "apps/shell/footer/ng-package.json"
},
"configurations": {
"production": {
"tsConfig": "apps/shell/footer/tsconfig.lib.prod.json"
},
"development": {
"tsConfig": "apps/shell/footer/tsconfig.lib.json"
}
},
"defaultConfiguration": "production"
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"tsConfig": "apps/shell/footer/tsconfig.spec.json",
"karmaConfig": "karma.conf.js",
"polyfills": [
"zone.js",
"zone.js/testing"
]
}
}
}
},
"@shell/process": {
"projectType": "library",
"root": "apps/shell/process",
"sourceRoot": "apps/shell/process/src",
"prefix": "lib",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:ng-packagr",
"options": {
"project": "apps/shell/process/ng-package.json"
},
"configurations": {
"production": {
"tsConfig": "apps/shell/process/tsconfig.lib.prod.json"
},
"development": {
"tsConfig": "apps/shell/process/tsconfig.lib.json"
}
},
"defaultConfiguration": "production"
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"tsConfig": "apps/shell/process/tsconfig.spec.json",
"karmaConfig": "karma.conf.js",
"polyfills": [
"zone.js",
"zone.js/testing"
]
}
}
}
},
"@domain/isa": {
"projectType": "library",
"root": "apps/domain/isa",
@@ -1160,40 +1030,6 @@
}
}
},
"@shell/filter-overlay": {
"projectType": "library",
"root": "apps/shell/filter-overlay",
"sourceRoot": "apps/shell/filter-overlay/src",
"prefix": "lib",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:ng-packagr",
"options": {
"project": "apps/shell/filter-overlay/ng-package.json"
},
"configurations": {
"production": {
"tsConfig": "apps/shell/filter-overlay/tsconfig.lib.prod.json"
},
"development": {
"tsConfig": "apps/shell/filter-overlay/tsconfig.lib.json"
}
},
"defaultConfiguration": "production"
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"tsConfig": "apps/shell/filter-overlay/tsconfig.spec.json",
"karmaConfig": "karma.conf.js",
"polyfills": [
"zone.js",
"zone.js/testing"
]
}
}
}
},
"@store/search-component-store": {
"projectType": "library",
"root": "apps/store/search-component-store",
@@ -1634,6 +1470,42 @@
}
}
}
},
"shell": {
"projectType": "library",
"root": "apps/shell",
"sourceRoot": "apps/shell/src",
"prefix": "shell",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:ng-packagr",
"options": {
"project": "apps/shell/ng-package.json"
},
"configurations": {
"production": {
"tsConfig": "apps/shell/tsconfig.lib.prod.json"
},
"development": {
"tsConfig": "apps/shell/tsconfig.lib.json"
}
},
"defaultConfiguration": "production"
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"tsConfig": "apps/shell/tsconfig.spec.json",
"polyfills": [
"zone.js",
"zone.js/testing"
]
}
}
}
}
},
"cli": {
"analytics": false
}
}

View File

@@ -1,5 +1,4 @@
import { Injectable } from '@angular/core';
import { DomainAvailabilityService } from '@domain/availability';
import { Store } from '@ngrx/store';
import { BranchDTO } from '@swagger/checkout';
import { isBoolean, isNumber } from '@utils/common';
@@ -16,25 +15,30 @@ import {
selectActivatedProcess,
patchProcess,
patchProcessData,
selectTitle,
setTitle,
} from './store';
@Injectable()
export class ApplicationService {
/** @deprecated */
private activatedProcessIdSubject = new BehaviorSubject<number>(undefined);
/** @deprecated */
get activatedProcessId() {
return this.activatedProcessIdSubject.value;
}
/** @deprecated */
get activatedProcessId$() {
return this.activatedProcessIdSubject.asObservable();
}
title$ = this.store.select(selectTitle);
constructor(private store: Store) {}
setTitle(title: string) {
this.store.dispatch(setTitle({ title }));
}
getProcesses$(section?: 'customer' | 'branch') {
const processes$ = this.store.select(selectProcesses);
return processes$.pipe(map((processes) => processes.filter((process) => (section ? process.section === section : true))));

View File

@@ -3,6 +3,8 @@ import { ApplicationProcess } from '..';
const prefix = '[CORE-APPLICATION]';
export const setTitle = createAction(`${prefix} Set Title`, props<{ title: string }>());
export const setSection = createAction(`${prefix} Set Section`, props<{ section: 'customer' | 'branch' }>());
export const addProcess = createAction(`${prefix} Add Process`, props<{ process: ApplicationProcess }>());

View File

@@ -1,9 +1,18 @@
import { Action, createReducer, on } from '@ngrx/store';
import { setSection, addProcess, removeProcess, setActivatedProcess, patchProcess, patchProcessData } from './application.actions';
import {
setSection,
addProcess,
removeProcess,
setActivatedProcess,
patchProcess,
patchProcessData,
setTitle,
} from './application.actions';
import { ApplicationState, INITIAL_APPLICATION_STATE } from './application.state';
const _applicationReducer = createReducer(
INITIAL_APPLICATION_STATE,
on(setTitle, (state, { title }) => ({ ...state, title })),
on(setSection, (state, { section }) => ({ ...state, section })),
on(addProcess, (state, { process }) => ({ ...state, processes: [...state.processes, { data: {}, ...process }] })),
on(removeProcess, (state, { processId }) => {

View File

@@ -2,6 +2,8 @@ import { createFeatureSelector, createSelector } from '@ngrx/store';
import { ApplicationState } from './application.state';
export const selectApplicationState = createFeatureSelector<ApplicationState>('core-application');
export const selectTitle = createSelector(selectApplicationState, (s) => s.title);
export const selectSection = createSelector(selectApplicationState, (s) => s.section);
export const selectProcesses = createSelector(selectApplicationState, (s) => s.processes);

View File

@@ -1,11 +1,13 @@
import { ApplicationProcess } from '../defs';
export interface ApplicationState {
title: string;
processes: ApplicationProcess[];
section: 'customer' | 'branch';
}
export const INITIAL_APPLICATION_STATE: ApplicationState = {
title: '',
processes: [],
section: 'customer',
};

View File

@@ -135,9 +135,9 @@ export class BreadcrumbService {
crumbs.forEach((crumb) => this.removeBreadcrumb(crumb.id));
}
getLatestBreadcrumbForSection(section: 'customer' | 'branch') {
getLatestBreadcrumbForSection(section: 'customer' | 'branch', predicate: (crumb: Breadcrumb) => boolean = (_) => true) {
return this.store
.select(selectors.selectBreadcrumbsBySection, { section })
.pipe(map((crumbs) => crumbs.sort((a, b) => b.changed - a.changed).find((f) => true)));
.pipe(map((crumbs) => crumbs.sort((a, b) => b.changed - a.changed).find((f) => predicate(f))));
}
}

View File

@@ -22,7 +22,7 @@ export interface Breadcrumb {
/**
* Url
*/
path: string;
path: string | any[];
/**
* Query Parameter

View File

@@ -1,17 +1,76 @@
import { Injectable } from '@angular/core';
import { Platform } from '@angular/cdk/platform';
import { NativeContainerService } from 'native-container';
import { BreakpointObserver } from '@angular/cdk/layout';
const MATCH_TABLET = '(max-width: 1024px)';
const MATCH_DESKTOP_SMALL = '(min-width: 1025px) and (max-width: 1439px)';
const MATCH_DESKTOP = '(min-width: 1280px)';
const MATCH_DESKTOP_LARGE = '(min-width: 1440px)';
const MATCH_DESKTOP_X_LARGE = '(min-width: 1920px)';
const MATCH_DESKTOP_XX_LARGE = '(min-width: 2736px)';
@Injectable({
providedIn: 'root',
})
export class EnvironmentService {
constructor(private _platform: Platform, private _nativeContainer: NativeContainerService) {}
constructor(
private _platform: Platform,
private _nativeContainer: NativeContainerService,
private _breakpointObserver: BreakpointObserver
) {}
matchTablet(): boolean {
return this._breakpointObserver.isMatched(MATCH_TABLET);
}
matchTablet$ = this._breakpointObserver.observe(MATCH_TABLET);
matchDesktopSmall(): boolean {
return this._breakpointObserver.isMatched(MATCH_DESKTOP_SMALL);
}
matchDesktopSmall$ = this._breakpointObserver.observe(MATCH_DESKTOP_SMALL);
matchDesktop(): boolean {
return this._breakpointObserver.isMatched(MATCH_DESKTOP);
}
matchDesktop$ = this._breakpointObserver.observe(MATCH_DESKTOP);
matchDesktopLarge(): boolean {
return this._breakpointObserver.isMatched(MATCH_DESKTOP_LARGE);
}
matchDesktopLarge$ = this._breakpointObserver.observe(MATCH_DESKTOP_LARGE);
matchDesktopXLarge(): boolean {
return this._breakpointObserver.isMatched(MATCH_DESKTOP_X_LARGE);
}
matchDesktopXLarge$ = this._breakpointObserver.observe(MATCH_DESKTOP_X_LARGE);
matchDesktopXXLarge(): boolean {
return this._breakpointObserver.isMatched(MATCH_DESKTOP_XX_LARGE);
}
matchDesktopXXLarge$ = this._breakpointObserver.observe(MATCH_DESKTOP_XX_LARGE);
/**
* @deprecated Use `matchDesktopSmall` or 'matchDesktop' instead.
*/
isDesktop(): boolean {
return !this.isTablet();
}
/**
* @deprecated Use `matchTablet` instead.
*/
isTablet(): boolean {
return this.isNative() || this.isSafari();
}
@@ -21,6 +80,6 @@ export class EnvironmentService {
}
isSafari(): boolean {
return (this._platform.ANDROID || this._platform.IOS) && this._platform.SAFARI;
return this._platform.IOS && this._platform.SAFARI;
}
}

View File

@@ -1,6 +1,5 @@
import { isDevMode, NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { DebugComponent } from './debug/debug.component';
import {
CanActivateCartGuard,
CanActivateCartWithProcessIdGuard,
@@ -17,9 +16,9 @@ import {
} 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 { ShellComponent, ShellModule } from './shell';
import { TokenLoginComponent, TokenLoginModule } from './token-login';
const routes: Routes = [
@@ -40,7 +39,7 @@ const routes: Routes = [
children: [
{
path: 'kunde',
component: ShellComponent,
component: MainComponent,
children: [
{
path: 'dashboard',
@@ -106,7 +105,7 @@ const routes: Routes = [
},
{
path: 'filiale',
component: ShellComponent,
component: MainComponent,
children: [
{
path: 'task-calendar',
@@ -152,7 +151,7 @@ if (isDevMode()) {
}
@NgModule({
imports: [RouterModule.forRoot(routes), ShellModule, TokenLoginModule],
imports: [RouterModule.forRoot(routes), TokenLoginModule],
exports: [RouterModule],
})
export class AppRoutingModule {}

View File

@@ -1,7 +1,3 @@
:host {
@apply block box-border;
}
button {
@apply fixed bottom-4 right-2 bg-blue-500 text-white font-bold py-2 px-4 rounded z-tooltip;
@apply block;
}

View File

@@ -32,9 +32,11 @@ 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 { UiIconModule } from '@ui/icon';
import { UiIconModule, UI_ICON_CFG } from '@ui/icon';
import { PreviewComponent } from './preview';
import { NativeContainerService } from 'native-container';
import { ShellModule } from '@shared/shell';
import { MainComponent } from './main.component';
registerLocaleData(localeDe, localeDeExtra);
registerLocaleData(localeDe, 'de', localeDeExtra);
@@ -74,11 +76,12 @@ export function _notificationsHubOptionsFactory(config: Config, auth: AuthServic
}
@NgModule({
declarations: [AppComponent],
declarations: [AppComponent, MainComponent],
imports: [
BrowserModule,
BrowserAnimationsModule,
HttpClientModule,
ShellModule.forRoot(),
AppRoutingModule,
AppSwaggerModule,
AppDomainModule,
@@ -103,31 +106,7 @@ export function _notificationsHubOptionsFactory(config: Config, auth: AuthServic
ScanAdapterModule.forRoot(),
ScanditScanAdapterModule.forRoot(),
PlatformModule,
UiIconModule.forRoot({
aliases: [
{ alias: 'd-account', name: 'account' },
{ alias: 'd-no-account', name: 'package-variant-closed' },
{ name: 'isa-audio', alias: 'AU' },
{ name: 'isa-audio', alias: 'CAS' },
{ name: 'isa-audio', alias: 'DL' },
{ name: 'isa-audio', alias: 'KAS' },
{ name: 'isa-hard-cover', alias: 'BUCH' },
{ name: 'isa-hard-cover', alias: 'GEB' },
{ name: 'isa-hard-cover', alias: 'HC' },
{ name: 'isa-hard-cover', alias: 'KT' },
{ name: 'isa-ebook', alias: 'EB' },
{ name: 'isa-non-book', alias: 'GLO' },
{ name: 'isa-non-book', alias: 'HDL' },
{ name: 'isa-non-book', alias: 'NB' },
{ name: 'isa-non-book', alias: 'SPL' },
{ name: 'isa-calendar', alias: 'KA' },
{ name: 'isa-scroll', alias: 'MA' },
{ name: 'isa-software', alias: 'SW' },
{ name: 'isa-soft-cover', alias: 'TB' },
{ name: 'isa-video', alias: 'VI' },
{ name: 'isa-news-paper', alias: 'ZS' },
],
}),
UiIconModule.forRoot(),
],
providers: [
{
@@ -156,6 +135,11 @@ export function _notificationsHubOptionsFactory(config: Config, auth: AuthServic
useClass: IsaErrorHandler,
},
{ provide: LOCALE_ID, useValue: 'de-DE' },
{
provide: UI_ICON_CFG,
useFactory: (config: Config) => config.get('@ui/icon'),
deps: [Config],
},
],
bootstrap: [AppComponent],
})

View File

@@ -2,6 +2,7 @@ import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '@angular/router';
import { ApplicationProcess, ApplicationService } from '@core/application';
import { DomainCheckoutService } from '@domain/checkout';
import { ProductCatalogNavigationService } from '@shared/services';
import { first } from 'rxjs/operators';
@Injectable({ providedIn: 'root' })
@@ -9,6 +10,7 @@ export class CanActivateProductGuard implements CanActivate {
constructor(
private readonly _applicationService: ApplicationService,
private readonly _checkoutService: DomainCheckoutService,
private readonly _navigationService: ProductCatalogNavigationService,
private readonly _router: Router
) {}
@@ -38,7 +40,7 @@ export class CanActivateProductGuard implements CanActivate {
}
if (!lastActivatedProcessId) {
await this.fromCartProcess(processes, route);
await this.fromCartProcess(processes);
return false;
} else {
await this._router.navigate(this.getUrlFromSnapshot(route, ['/kunde', String(lastActivatedProcessId)]));
@@ -48,7 +50,7 @@ export class CanActivateProductGuard implements CanActivate {
}
// Bei offener Artikelsuche/Kundensuche und Klick auf Footer Artikelsuche
async fromCartProcess(processes: ApplicationProcess[], route: ActivatedRouteSnapshot) {
async fromCartProcess(processes: ApplicationProcess[]) {
const newProcessId = Date.now();
await this._applicationService.createProcess({
id: newProcessId,
@@ -57,7 +59,7 @@ export class CanActivateProductGuard implements CanActivate {
name: `Vorgang ${this.processNumber(processes.filter((process) => process.type === 'cart'))}`,
});
await this._router.navigate(this.getUrlFromSnapshot(route, ['/kunde', String(newProcessId)]));
await this._navigationService.navigateToProductSearch({ processId: newProcessId });
}
// Bei offener Warenausgabe und Klick auf Footer Artikelsuche

View File

@@ -0,0 +1,3 @@
<shell-root>
<router-outlet></router-outlet>
</shell-root>

View File

@@ -0,0 +1,10 @@
import { Component, ChangeDetectionStrategy } from '@angular/core';
@Component({
selector: 'app-main',
templateUrl: 'main.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MainComponent {
constructor() {}
}

View File

@@ -1,4 +0,0 @@
// start:ng42.barrel
export * from './shell.component';
export * from './shell.module';
// end:ng42.barrel

View File

@@ -1,88 +0,0 @@
<div class="shell-header-wrapper">
<shell-header [section]="section$ | async" (sectionChange)="setSection($event)">
<a [routerLink]="['/kunde/dashboard']" routerLinkActive="active" class="dashboard-btn">
<ui-icon icon="dashboard" size="26px"></ui-icon>
</a>
<button class="notifications-btn" [disabled]="(notificationCount$ | async) === 0" (click)="openNotifications()">
<ui-icon icon="notification" size="26px"></ui-icon>
<span class="notification-counter" *ngIf="notificationCount$ | async; let count">{{ count }}</span>
</button>
<button (click)="logout()" class="logout-btn">
<span *ngIf="currentBranch$ | async; let currentBranch">{{ currentBranch.key | uppercase }}</span>
<ui-icon icon="logout" size="26px"></ui-icon>
</button>
</shell-header>
</div>
<div class="shell-process-wrapper">
<shell-process
[label]="addProcessLabel$ | async"
[canAddProcess]="canAddProcess$ | async"
(addProcess)="addProcess(); processTabs?.last?.triggerAnimation()"
>
<shell-process-tab
#processTabs
(activateProcess)="activateProcess($event)"
(closeProcess)="closeProcess($event)"
(processAction)="processAction($event)"
*ngFor="let process of processes$ | async; trackBy: trackByIdFn"
[isActive]="(activatedProcessId$ | async) === process.id"
[process]="process"
></shell-process-tab>
</shell-process>
</div>
<div class="main-wrapper">
<main>
<router-outlet></router-outlet>
</main>
</div>
<div class="shell-footer-wrapper">
<shell-footer *ngIf="section$ | async; let section">
<ng-container *ngIf="section === 'customer'">
<a [routerLink]="[customerBasePath$ | async, 'product']" routerLinkActive="active">
<ui-icon icon="catalog" size="30px"></ui-icon>
Artikelsuche
</a>
<a [routerLink]="[customerBasePath$ | async, 'customer']" routerLinkActive="active">
<ui-icon icon="customer" size="24px"></ui-icon>
Kundensuche
</a>
<a *ifRole="'Store'" [routerLink]="[customerBasePath$ | async, 'goods', 'out']" routerLinkActive="active">
<ui-icon icon="box_out" size="24px"></ui-icon>
Warenausgabe
</a>
<a *ifRole="'CallCenter'" [routerLink]="[customerBasePath$ | async, 'order']" routerLinkActive="active">
<ui-svg-icon icon="package-variant-closed" [size]="28"></ui-svg-icon>
Kundenbestellungen
</a>
</ng-container>
<ng-container *ngIf="section === 'branch'">
<a [routerLink]="['/filiale/assortment']" routerLinkActive="active">
<ui-svg-icon icon="shape-outline" [size]="24"></ui-svg-icon>
Sortiment
</a>
<a [routerLink]="['/filiale/task-calendar']" routerLinkActive="active">
<ui-icon icon="calendar_check" size="24px"></ui-icon>
Tätigkeitskalender
</a>
<a [routerLink]="['/filiale/goods/in']" routerLinkActive="active">
<ui-icon icon="box_return" size="24px"></ui-icon>
Abholfach
</a>
<a [routerLink]="[remissionUrl$ | async]" [queryParams]="remissionQueryParams$ | async" routerLinkActive="active">
<ui-icon icon="documents_refresh" size="24px"></ui-icon>
Remission
</a>
<a [routerLink]="['/filiale/package-inspection']" routerLinkActive="active" (click)="fetchAndOpenPackages()">
<ui-svg-icon icon="clipboard-check-outline" [size]="24"></ui-svg-icon>
Wareneingang
</a>
</ng-container>
</shell-footer>
</div>
<button *ngIf="isDevelopment" class="block absolute bottom-0 right-0 z-tooltip p-4 opacity-5" (click)="debugOpen = !debugOpen">
<ui-svg-icon icon="bug-outline"></ui-svg-icon>
</button>
<app-debug *ngIf="debugOpen" class="absolute inset-x-0 top-0 max-h-[calc(100vh-80px)]"></app-debug>

View File

@@ -1,60 +0,0 @@
:host {
@apply block relative min-h-screen;
}
.main-wrapper {
@apply fixed right-0 left-0 overflow-auto;
top: 8.375rem;
bottom: 5rem;
main {
@apply w-full max-w-content mx-auto px-4 self-stretch;
}
}
.shell-header-wrapper {
@apply fixed top-0 left-0 right-0 bg-white;
shell-header {
@apply w-full max-w-content mx-auto;
}
button.notifications-btn {
@apply relative;
.notification-counter {
@apply absolute flex items-center justify-center top-2 right-px-3 text-sm rounded-full w-6 h-6 font-semibold;
background-color: var(--shell-notification-counter-background);
color: var(--shell-notification-counter-text);
z-index: 10;
}
}
}
.shell-process-wrapper {
@apply fixed left-0 right-0 bg-white;
top: 5.125rem;
shell-process {
@apply w-full max-w-content mx-auto;
height: 52px;
}
}
shell-process {
height: 52px;
grid-area: process;
}
.shell-footer-wrapper {
@apply fixed bottom-0 left-0 right-0 bg-white z-fixed shadow-card;
shell-footer {
@apply w-full max-w-content mx-auto;
.active {
@apply font-bold;
color: var(--shell-footer-link-active);
}
}
}

View File

@@ -1,445 +0,0 @@
// unit test ShellComponent with Spectator
import { Component } from '@angular/core';
import { Router } from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing';
import { ApplicationProcess, ApplicationService } from '@core/application';
import { AuthModule, AuthService } from '@core/auth';
import { Config } from '@core/config';
import { BreadcrumbService } from '@core/breadcrumb';
import { DomainAvailabilityService } from '@domain/availability';
import { DomainDashboardService } from '@domain/isa';
import { NotificationsHub } from '@hub/notifications';
import { ModalNotificationsComponent } from '@modal/notifications';
import { Spectator, createComponentFactory, SpyObject, createSpyObject } from '@ngneat/spectator';
import { DashboardComponent } from '@page/dashboard';
import { ShellFooterComponent } from '@shell/footer';
import { ShellHeaderComponent } from '@shell/header';
import { ShellProcessComponent, ShellProcessTabComponent } from '@shell/process';
import { IconRegistry, UiIconComponent, UiIconModule } from '@ui/icon';
import { UiModalService } from '@ui/modal';
import { EnvelopeDTO, MessageBoardItemDTO } from 'apps/hub/notifications/src/lib/defs';
import { MockComponent } from 'ng-mocks';
import { of } from 'rxjs';
import { first } from 'rxjs/operators';
import { ShellComponent } from './shell.component';
import { WrongDestinationModalService } from 'apps/page/package-inspection/src/lib/components/wrong-destination-modal/wrong-destination-modal.service';
// DummyComponent Class
@Component({
selector: 'dummy-component',
template: '<div></div>',
})
class DummyComponent {
constructor() {}
}
describe('ShellComponent', () => {
let spectator: Spectator<ShellComponent>;
let applicationServiceMock: SpyObject<ApplicationService>;
let modalServiceMock: SpyObject<UiModalService>;
let notificationsHubMock: SpyObject<NotificationsHub>;
let router: Router;
let breadcrumbServiceMock: SpyObject<BreadcrumbService>;
let authServiceMock: SpyObject<AuthService>;
const createComponent = createComponentFactory({
component: ShellComponent,
imports: [
UiIconModule,
RouterTestingModule.withRoutes([
{ path: 'kunde', component: DummyComponent },
{ path: 'kunde/dashboard', component: DashboardComponent },
]),
AuthModule,
],
declarations: [
MockComponent(ShellHeaderComponent),
MockComponent(ShellFooterComponent),
MockComponent(ShellProcessComponent),
MockComponent(ShellProcessTabComponent),
],
mocks: [
BreadcrumbService,
DomainAvailabilityService,
AuthService,
DomainDashboardService,
Config,
WrongDestinationModalService,
IconRegistry,
],
});
beforeEach(() => {
applicationServiceMock = createSpyObject(ApplicationService);
applicationServiceMock.getSection$.and.returnValue(of('customer'));
applicationServiceMock.getProcesses$.and.returnValue(of([]));
applicationServiceMock.getProcessById$.and.returnValue(of({ id: 4000 }));
applicationServiceMock.getActivatedProcessId$.and.returnValue(of(undefined));
applicationServiceMock.getLastActivatedProcessWithSectionAndType$.and.returnValue(of({}));
applicationServiceMock.getLastActivatedProcessWithSection$.and.returnValue(of({}));
notificationsHubMock = createSpyObject(NotificationsHub);
notificationsHubMock.notifications$ = of({});
modalServiceMock = createSpyObject(UiModalService);
authServiceMock = createSpyObject(AuthService);
spectator = createComponent({
providers: [
{ provide: ApplicationService, useValue: applicationServiceMock },
{ provide: NotificationsHub, useValue: notificationsHubMock },
{ provide: UiModalService, useValue: modalServiceMock },
{ provide: AuthService, useValue: authServiceMock },
],
});
breadcrumbServiceMock = spectator.inject(BreadcrumbService);
router = spectator.inject(Router);
});
it('should create', () => {
expect(spectator.component).toBeTruthy();
});
describe('shell-header', () => {
it('should call setSection() on sectionChange event with the section argument', () => {
spyOn(spectator.component, 'setSection');
spectator.triggerEventHandler('shell-header', 'sectionChange', 'branch');
expect(spectator.component.setSection).toHaveBeenCalledWith('branch');
});
it('should render the header buttons', () => {
// Test verhält sich anders, wenn die größe des Browserfensters kleiner ist als 640px, da
// die Buttons dann unsichtbar werden und ins Drei-Punkt Menü verschoben werden.
if (document.body.clientWidth > 639) {
expect(spectator.query('shell-header .notifications-btn')).toBeVisible();
expect(spectator.query('shell-header .dashboard-btn')).toBeVisible();
expect(spectator.query('shell-header .logout-btn')).toBeVisible();
} else {
expect(spectator.query('shell-header .notifications-btn')).not.toBeVisible();
expect(spectator.query('shell-header .dashboard-btn')).not.toBeVisible();
expect(spectator.query('shell-header .logout-btn')).not.toBeVisible();
}
});
it('should have a anchor tag which navigates to /kunde/dashboard', () => {
const anchor = spectator.query('shell-header a');
expect(anchor).toHaveAttribute('href', '/kunde/dashboard');
});
});
describe('shell-process', () => {
it('should call addProcess() on addProcess event', () => {
spyOn(spectator.component, 'addProcess');
spectator.triggerEventHandler('shell-process', 'addProcess', undefined);
expect(spectator.component.addProcess).toHaveBeenCalled();
});
describe('shell-process-tab', () => {
it('should render for each process', () => {
const processes = [{}, {}, {}];
applicationServiceMock.getSection$.and.returnValue(of('customer'));
applicationServiceMock.getProcesses$.and.returnValue(of(processes));
spectator.detectComponentChanges();
expect(spectator.queryAll('shell-process-tab')).toHaveLength(processes.length);
});
it('should call activateProcess() on activateProcess event', () => {
const processes = [{ id: 1 }];
applicationServiceMock.getProcesses$.and.returnValue(of(processes));
spectator.detectComponentChanges();
spyOn(spectator.component, 'activateProcess');
spectator.triggerEventHandler('shell-process-tab', 'activateProcess', processes[0].id);
expect(spectator.component.activateProcess).toHaveBeenCalledWith(1);
});
it('should call closeProcess() on closeProcess event', () => {
const processes = [{ id: 1 }];
applicationServiceMock.getProcesses$.and.returnValue(of(processes));
spectator.detectComponentChanges();
spyOn(spectator.component, 'closeProcess');
spectator.triggerEventHandler('shell-process-tab', 'closeProcess', processes[0].id);
expect(spectator.component.closeProcess).toHaveBeenCalledWith(1);
});
});
});
describe('shell-footer', () => {
it('should render when section is set', () => {
applicationServiceMock.getSection$.and.returnValue(of('customer'));
spectator.detectComponentChanges();
expect(spectator.query('shell-footer')).toBeVisible();
});
it('should not render when section is undefined', () => {
applicationServiceMock.getSection$.and.returnValue(of(undefined));
spectator.detectComponentChanges();
expect(spectator.query('shell-footer')).not.toBeVisible();
});
xit('should display the menu items for section customer', () => {
applicationServiceMock.getSection$.and.returnValue(of('customer'));
spectator.component.customerBasePath$ = of('/kunde/1');
spectator.detectComponentChanges();
authServiceMock.hasRole.and.returnValue(true);
const anchors = spectator.queryAll('shell-footer a');
expect(anchors[0]).toHaveText('Artikelsuche');
expect(anchors[0]).toHaveAttribute('href', '/kunde/1/product');
expect(anchors[1]).toHaveText('Kundensuche');
expect(anchors[1]).toHaveAttribute('href', '/kunde/1/customer');
expect(anchors[2]).toHaveText('Warenausgabe');
expect(anchors[2]).toHaveAttribute('href', '/kunde/1/goods/out');
});
it('should display the menu items for section branch', () => {
applicationServiceMock.getSection$.and.returnValue(of('branch'));
spectator.detectComponentChanges();
const anchors = spectator.queryAll('shell-footer a');
expect(anchors[0]).toHaveText('Sortiment');
expect(anchors[0]).toHaveAttribute('href', '/filiale/assortment');
expect(anchors[1]).toHaveText('Tätigkeitskalender');
expect(anchors[1]).toHaveAttribute('href', '/filiale/task-calendar');
expect(anchors[2]).toHaveText('Abholfach');
expect(anchors[2]).toHaveAttribute('href', '/filiale/goods/in');
expect(anchors[3]).toHaveText('Remission');
expect(anchors[3]).toHaveAttribute('href', '/filiale/remission');
// expect(anchors[4]).toHaveText('Wareneingang');
// expect(anchors[4]).toHaveAttribute('href', '/filiale/package-inspection');
});
});
describe('activatedProcessId$', () => {
it('should call _appService.getActivatedProcessId$() and return its value', async () => {
applicationServiceMock.getActivatedProcessId$.and.returnValue(of(1));
const processId = await spectator.component.activatedProcessId$.pipe(first()).toPromise();
expect(processId).toBe(1);
expect(applicationServiceMock.getActivatedProcessId$).toHaveBeenCalled();
});
});
describe('section$', () => {
it('should call _appService.getSection$() and return its value', async () => {
applicationServiceMock.getSection$.and.returnValue(of('branch'));
const section = await spectator.component.section$.pipe(first()).toPromise();
expect(section).toBe('branch');
expect(applicationServiceMock.getSection$).toHaveBeenCalled();
});
});
describe('processes$', () => {
it('should call _appService.processes$() and return its value', async () => {
applicationServiceMock.getProcesses$.and.returnValue(of([{}, {}]));
const processes = await spectator.component.processes$.pipe(first()).toPromise();
expect(processes).toHaveLength(2);
expect(applicationServiceMock.getProcesses$).toHaveBeenCalledWith('customer');
});
});
describe('remissionProcess$', () => {
it('should call _appService.getProcessById$() with Remission Id and return its value', async () => {
applicationServiceMock.getProcessById$.and.returnValue(of({ id: 4000 }));
await spectator.component.remissionProcess$.pipe(first()).toPromise();
expect(applicationServiceMock.getProcessById$).toHaveBeenCalled();
});
});
describe('remissionUrl$', () => {
it('should return the correct url if process.data.active is available', async () => {
const process = {
id: 4000,
data: {
active: 9999,
},
};
applicationServiceMock.getProcessById$.and.returnValue(of(process));
const url = await spectator.component.remissionUrl$.pipe(first()).toPromise();
expect(url).toBe('/filiale/remission/9999/list');
});
it('should return the correct url if process.data.active is not available', async () => {
const process = {
id: 4000,
data: {},
};
applicationServiceMock.getProcessById$.and.returnValue(of(process));
const url = await spectator.component.remissionUrl$.pipe(first()).toPromise();
expect(url).toBe('/filiale/remission');
});
});
describe('remissionQueryParams$', () => {
it('should return the correct queryParams if process.data.active and process.data.queryParams are available', async () => {
const process = {
id: 4000,
data: {
active: 9999,
queryParams: { filter: 'test' },
},
};
applicationServiceMock.getProcessById$.and.returnValue(of(process));
const queryParams = await spectator.component.remissionQueryParams$.pipe(first()).toPromise();
expect(queryParams).toEqual(process.data.queryParams);
});
it('should return the correct queryParams if process.data.active and process.data.queryParams are not available', async () => {
const process = {
id: 4000,
data: {},
};
applicationServiceMock.getProcessById$.and.returnValue(of(process));
const queryParams = await spectator.component.remissionQueryParams$.pipe(first()).toPromise();
expect(queryParams).toEqual({});
});
});
describe('setSection()', () => {
it('should call _appService.setSection() with the argument section', async () => {
await spectator.component.setSection('customer');
expect(applicationServiceMock.setSection).toHaveBeenCalledWith('customer');
});
it('should call activateProcess if getLastActivatedProcessWithSection returns a value', async () => {
applicationServiceMock.getLastActivatedProcessWithSection$.and.returnValue(of({ id: 1 }));
spyOn(spectator.component, 'activateProcess');
await spectator.component.setSection('customer');
expect(spectator.component.activateProcess).toHaveBeenCalledWith(1);
});
});
describe('logout()', () => {
it('should call _authService.logout()', () => {
spectator.component.logout();
expect(authServiceMock.logout).toHaveBeenCalled();
});
});
describe('addProcess()', () => {
it('should call navigate to /kunde/{timestamp}/product', () => {
spyOn(router, 'navigate');
spyOn(Date, 'now').and.returnValue(123);
spectator.component.addProcess();
expect(router.navigate).toHaveBeenCalledWith(['/kunde', 123, 'product']);
});
});
describe('closeProcess()', () => {
it('should call _appService.removeProcess() with the processId argument', () => {
const processes = [{}, {}, {}];
applicationServiceMock.getSection$.and.returnValue(of('customer'));
applicationServiceMock.getProcesses$.and.returnValue(of(processes));
spectator.component.closeProcess(1);
expect(applicationServiceMock.removeProcess).toHaveBeenCalledWith(1);
});
it('should navigate to kunde/dashboard if no process is available', async () => {
spyOn(router, 'navigate');
applicationServiceMock.getSection$.and.returnValue(of('customer'));
applicationServiceMock.getProcesses$.and.returnValue(of([]));
spectator.detectComponentChanges();
await spectator.component.closeProcess(1);
expect(router.navigate).toHaveBeenCalledWith(['/kunde', 'dashboard']);
});
it('should not navigate to kunde/dashboard if processes are available', async () => {
spyOn(router, 'navigate');
const processes = [
{ id: 1, name: 'test', section: 'customer' },
{ id: 2, name: 'test', section: 'customer' },
];
applicationServiceMock.getLastActivatedProcessWithSection$.and.returnValue(of({}));
applicationServiceMock.getSection$.and.returnValue(of('customer'));
applicationServiceMock.getProcesses$.and.returnValue(of(processes));
await spectator.component.closeProcess(1);
expect(router.navigate).not.toHaveBeenCalledWith(['/kunde', 'dashboard']);
});
it('should activate the next process when it was not the last process', async () => {
spyOn(spectator.component, 'activateProcess');
applicationServiceMock.getLastActivatedProcessWithSection$.and.returnValue(
of({
id: 2,
name: 'test',
section: 'customer',
activated: 2,
})
);
const processes = [
{ id: 1, name: 'test', section: 'customer', activated: 1 },
{ id: 2, name: 'test', section: 'customer', activated: 2 },
];
applicationServiceMock.getSection$.and.returnValue(of('customer'));
applicationServiceMock.getProcesses$.and.returnValue(of(processes));
await spectator.component.closeProcess(1);
expect(spectator.component.activateProcess).toHaveBeenCalledWith(2);
});
});
describe('activateProcess()', () => {
it('should get the last activated breadcrumb by key and if it is defined it navigates to its path with queryParams', async () => {
const crumb = { path: '/kunde/product', params: { id: 1 } };
spyOn(router, 'navigate');
breadcrumbServiceMock.getLastActivatedBreadcrumbByKey$.and.returnValue(of(crumb));
await spectator.component.activateProcess(1);
expect(router.navigate).toHaveBeenCalledWith([crumb.path], { queryParams: crumb.params });
});
it('should navigate to /kunde if no breadcrumb for this process exists', async () => {
breadcrumbServiceMock.getLastActivatedBreadcrumbByKey$.and.returnValue(of(undefined));
spyOn(router, 'navigate');
await spectator.component.activateProcess(1);
expect(router.navigate).toHaveBeenCalledWith(['/kunde', 1, 'product']);
});
});
describe('processAction()', () => {
it('should navigate to cart when process type is cart', () => {
spyOn(router, 'navigate');
const process: ApplicationProcess = { id: 1, name: 'Vorgang', section: 'customer', type: 'cart' };
spectator.component.processAction(process);
expect(router.navigate).toHaveBeenCalledWith(['/kunde', process.id, 'cart']);
});
it('should not navigate to when process type is not cart', () => {
spyOn(router, 'navigate');
const process: ApplicationProcess = { id: 1, name: 'Vorgang', section: 'customer', type: 'goods-out' };
spectator.component.processAction(process);
expect(router.navigate).not.toHaveBeenCalled();
});
});
describe('openNotifications()', () => {
it('should call modalService.open() with the ModalNotificationComponent', async () => {
const notifications: EnvelopeDTO<MessageBoardItemDTO[]> = {
data: [{}, {}, {}],
};
spectator.component.notifications$ = of(notifications);
await spectator.component.openNotifications();
expect(modalServiceMock.open).toHaveBeenCalledWith({
content: ModalNotificationsComponent,
data: notifications,
config: {
showScrollbarY: false,
},
});
});
});
});

View File

@@ -1,174 +0,0 @@
import { Component, ChangeDetectionStrategy, ViewChildren, QueryList, TrackByFunction, NgZone } from '@angular/core';
import { ApplicationProcess, ApplicationService } from '@core/application';
import { first, map, shareReplay, switchMap, take, tap } from 'rxjs/operators';
import { NotificationsHub } from '@hub/notifications';
import { ModalNotificationsComponent } from '@modal/notifications';
import { UiModalService } from '@ui/modal';
import { Router } from '@angular/router';
import { BreadcrumbService } from '@core/breadcrumb';
import { combineLatest } from 'rxjs';
import { AuthService } from '@core/auth';
import { DomainAvailabilityService } from '@domain/availability';
import { ShellProcessTabComponent } from '@shell/process';
import { Config } from '@core/config';
import { WrongDestinationModalService } from 'apps/page/package-inspection/src/lib/components/wrong-destination-modal/wrong-destination-modal.service';
@Component({
selector: 'app-shell',
templateUrl: 'shell.component.html',
styleUrls: ['shell.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ShellComponent {
isDevelopment = Boolean(this._config.get('debug'));
debugOpen = false;
@ViewChildren('processTabs')
readonly processTabs: QueryList<ShellProcessTabComponent>;
notifications$ = this._notificationsHub.notifications$;
notificationCount$ = this.notifications$.pipe(map((message) => message?.data?.length));
get activatedProcessId$() {
return this._appService.getActivatedProcessId$().pipe(
tap((activatedProcessId) => {
this.processTabs?.find((process) => process?.process?.id === activatedProcessId && !process?.isActive)?.slideIntoView();
})
);
}
customerBasePath$ = this.activatedProcessId$.pipe(
switchMap((processId) => this._appService.getProcessById$(processId)),
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';
}
})
);
get section$() {
return this._appService.getSection$().pipe(shareReplay());
}
get processes$() {
return this.section$.pipe(switchMap((section) => this._appService.getProcesses$(section)));
}
get remissionProcess$() {
return this._appService.getProcessById$(this._config.get('process.ids.remission'));
}
get remissionUrl$() {
return this.remissionProcess$.pipe(
map((process) => (process?.data?.active ? `/filiale/remission/${process.data.active}/list` : '/filiale/remission'))
);
}
get remissionQueryParams$() {
return this.remissionProcess$.pipe(
map((process) => (process?.data?.active && process?.data?.queryParams ? process.data.queryParams : {}))
);
}
get addProcessLabel$() {
return combineLatest([this.section$, this.processes$]).pipe(
map(([section, processes]) => (section === 'customer' && processes.length === 0 ? 'VORGANG STARTEN' : ''))
);
}
get canAddProcess$() {
return this.section$.pipe(map((section) => section === 'customer'));
}
get currentBranch$() {
return this._availabilityService.getDefaultBranch();
}
constructor(
private readonly _appService: ApplicationService,
private readonly _config: Config,
private readonly _notificationsHub: NotificationsHub,
private readonly _modal: UiModalService,
private readonly _router: Router,
private readonly _breadcrumbService: BreadcrumbService,
private readonly _authService: AuthService,
private readonly _availabilityService: DomainAvailabilityService,
private readonly _zone: NgZone,
private readonly _wrongDestinationModalService: WrongDestinationModalService
) {}
async setSection(section: 'customer' | 'branch') {
this._appService.setSection(section);
const lastProcessId = (await this._appService.getLastActivatedProcessWithSection$(section).pipe(first()).toPromise())?.id;
if (lastProcessId) {
this.activateProcess(lastProcessId);
} else {
this._router.navigate([section === 'customer' ? '/kunde' : '/filiale']);
}
}
// Process werden über Guards erstellt und aktiviert. An dieser Stelle wird nur navigiert
async addProcess() {
const processId = Date.now();
await this._router.navigate(['/kunde', processId, 'product']);
}
async activateProcess(activatedProcessId: number) {
try {
const latestCrumb = await this._breadcrumbService?.getLastActivatedBreadcrumbByKey$(activatedProcessId)?.pipe(take(1)).toPromise();
await this._zone.run(async () => {
if (latestCrumb) {
await this._router.navigate([latestCrumb.path], { queryParams: latestCrumb.params });
} else {
await this._router.navigate(['/kunde', activatedProcessId, 'product']);
}
});
} catch (error) {}
}
async closeProcess(processId: number) {
this._appService.removeProcess(processId);
const processes = await this.processes$.pipe(first()).toPromise();
if (processes.length === 0) {
await this._router.navigate(['/kunde', 'dashboard']);
return;
}
const section = await this.section$.pipe(first()).toPromise();
const lastActivatedProcess = await this._appService.getLastActivatedProcessWithSection$(section).pipe(first()).toPromise();
this.activateProcess(lastActivatedProcess?.id);
}
processAction(process: ApplicationProcess) {
if (process?.type === 'cart') {
this._router.navigate(['/kunde', process.id, 'cart']);
}
}
async logout() {
await this._authService.logout();
}
async openNotifications() {
const notifications = await this.notifications$.pipe(first()).toPromise();
this._modal.open({
content: ModalNotificationsComponent,
data: notifications,
config: {
showScrollbarY: false,
},
});
}
trackByIdFn: TrackByFunction<ApplicationProcess> = (_, process) => process.id;
fetchAndOpenPackages = () => this._wrongDestinationModalService.fetchAndOpen();
}

View File

@@ -1,31 +0,0 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { OverlayModule } from '@angular/cdk/overlay';
import { ShellHeaderModule } from '@shell/header';
import { ShellProcessModule } from '@shell/process';
import { ShellFooterModule } from '@shell/footer';
import { ShellComponent } from './shell.component';
import { UiIconModule } from '@ui/icon';
import { RouterModule } from '@angular/router';
import { AuthModule } from '@core/auth';
import { DebugComponent } from '../debug/debug.component';
@NgModule({
imports: [
RouterModule,
CommonModule,
ShellHeaderModule,
ShellProcessModule,
ShellFooterModule,
UiIconModule,
OverlayModule,
AuthModule,
DebugComponent,
],
exports: [ShellComponent],
declarations: [ShellComponent],
providers: [],
})
export class ShellModule {}

View File

@@ -1,20 +0,0 @@
@font-face {
font-family: 'Material Symbols Outlined';
font-style: normal;
font-weight: 100 700;
src: url(./materials-icons-outlined.woff2) format('woff2');
}
@font-face {
font-family: 'Material Symbols Rounded';
font-style: normal;
font-weight: 100 700;
src: url(./materials-icons-rounded.woff2) format('woff2');
}
@font-face {
font-family: 'Material Symbols Sharp';
font-style: normal;
font-weight: 100 700;
src: url(./materials-icons-sharp.woff2) format('woff2');
}

View File

Binary file not shown.

View File

File diff suppressed because one or more lines are too long

View File

File diff suppressed because one or more lines are too long

View File

File diff suppressed because one or more lines are too long

View File

@@ -7,7 +7,6 @@
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0" />
<link rel="icon" type="image/x-icon" href="favicon.ico" />
<link href="/assets/fonts/fonts.css" rel="stylesheet" />
<link href="/assets/icons/icons.css" rel="stylesheet" />
<link rel="manifest" href="manifest.webmanifest" />
<meta name="theme-color" content="#1976d2" />
</head>

View File

@@ -16,7 +16,7 @@
}
body {
background: var(--bg-color);
@apply bg-background;
}
@layer base {

View File

@@ -1,4 +1,5 @@
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { ApplicationService } from '@core/application';
import { Breadcrumb, BreadcrumbService } from '@core/breadcrumb';
import { Config } from '@core/config';
@@ -13,10 +14,11 @@ export class AssortmentComponent implements OnInit {
return this._config.get('process.ids.assortment');
}
constructor(private _config: Config, private _breadcrumb: BreadcrumbService) {}
constructor(private _config: Config, private _breadcrumb: BreadcrumbService, private _app: ApplicationService) {}
ngOnInit() {
this.createBreadcrumbIfNotExists();
this._app.setTitle('Sortiment');
}
async createBreadcrumbIfNotExists(): Promise<void> {

View File

@@ -20,7 +20,7 @@
<div class="page-price-update-item__item-details">
<div class="page-price-update-item__item-contributors flex flex-row">
{{ environment.isTablet() ? (item?.product?.contributors | substr: 42) : item?.product?.contributors }}
{{ environment.isTablet() ? (item?.product?.contributors | substr: 38) : item?.product?.contributors }}
</div>
<div
@@ -43,7 +43,7 @@
src="assets/images/Icon_{{ item?.product?.format }}.svg"
[alt]="item?.product?.formatDetail"
/>
{{ item?.product?.formatDetail }}
{{ environment.isTablet() ? (item?.product?.formatDetail | substr: 25) : item?.product?.formatDetail }}
</div>
</div>

View File

@@ -1,7 +1,7 @@
import { ChangeDetectionStrategy, Component, OnInit, ViewChild } from '@angular/core';
import { Config } from '@core/config';
import { provideComponentStore } from '@ngrx/component-store';
import { ShellFilterOverlayComponent } from '@shell/filter-overlay';
import { SharedFilterOverlayComponent } from '@shared/components/filter-overlay';
import { UiFilter, UiFilterComponent } from '@ui/filter';
import { PriceUpdateComponentStore } from './price-update.component.store';
import { combineLatest, Subject, Subscription } from 'rxjs';
@@ -26,8 +26,8 @@ export class PriceUpdateComponent implements OnInit {
hint$ = new Subject<string>();
@ViewChild(ShellFilterOverlayComponent)
filterOverlay: ShellFilterOverlayComponent;
@ViewChild(SharedFilterOverlayComponent)
filterOverlay: SharedFilterOverlayComponent;
/**
* Zeigt die liste an, wenn entweder keine items geladen werden oder wenn items geladen wurden

View File

@@ -1,6 +1,6 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { ShellFilterOverlayModule } from '@shell/filter-overlay';
import { SharedFilterOverlayModule } from '@shared/components/filter-overlay';
import { UiFilterNextModule } from '@ui/filter';
import { UiIconModule } from '@ui/icon';
import { UiSpinnerModule } from '@ui/spinner';
@@ -8,7 +8,7 @@ import { PriceUpdateListModule } from './price-update-list';
import { PriceUpdateComponent } from './price-update.component';
@NgModule({
imports: [CommonModule, PriceUpdateListModule, UiIconModule, UiFilterNextModule, ShellFilterOverlayModule, UiSpinnerModule],
imports: [CommonModule, PriceUpdateListModule, UiIconModule, UiFilterNextModule, SharedFilterOverlayModule, UiSpinnerModule],
exports: [PriceUpdateComponent],
declarations: [PriceUpdateComponent],
providers: [],

View File

@@ -1,124 +1,240 @@
<ng-container *ngIf="!showRecommendations">
<div #detailsContainer class="product-card">
<div class="page-article-details__container px-5 relative">
<ng-container *ngIf="store.item$ | async; let item">
<div class="product-details">
<div class="product-image">
<button class="image-button" (click)="showImages()">
<img (load)="loadImage()" [src]="item.imageId | productImage: 195:315:true" alt="product image" />
<ui-icon *ngIf="imageLoaded$ | async" icon="search_add" size="22px"></ui-icon>
</button>
<div class="page-article-details__product-details mb-3">
<div class="page-article-details__product-bookmark justify-self-end">
<div *ngIf="showArchivBadge$ | async" class="archiv-badge">
<button [uiOverlayTrigger]="archivTooltip" class="p-0 m-0 outline-none border-none bg-transparent relative -top-px-5">
<img src="/assets/images/bookmark_benachrichtigung_archiv.svg" alt="Archiv Badge" />
<ui-tooltip yPosition="above" xPosition="after" [yOffset]="-11" [xOffset]="-8" #archivTooltip [closeable]="true">
<ng-container *ngIf="isAvailable$ | async; else notAvailable">
Archivtitel. Wird nicht mehr gedruckt. Artikel ist bestellbar, weil lieferbar.
</ng-container>
<ng-template #notAvailable>
Archivtitel. Wird nicht mehr gedruckt. Nicht bestellbar.
</ng-template>
</ui-tooltip>
</button>
</div>
<div *ngIf="showSubscriptionBadge$ | async">
<button [uiOverlayTrigger]="subscribtionTooltip" class="p-0 m-0 outline-none border-none bg-transparent relative -top-px-5">
<img src="/assets/images/bookmark_subscription.svg" alt="Fortsetzungsartikel Badge" />
</button>
<ui-tooltip yPosition="above" xPosition="after" [yOffset]="-11" [xOffset]="-8" #subscribtionTooltip [closeable]="true"
>Artikel ist ein Fortsetzungsartikel,<br />
Artikel muss über eine Aboabteilung<br />
bestellt werden.
</ui-tooltip>
</div>
<div *ngIf="showPromotionBadge$ | async" class="promotion-badge">
<button [uiOverlayTrigger]="promotionTooltip" class="p-0 m-0 outline-none border-none bg-transparent relative -top-px-5">
<ui-icon-badge icon="gift" alt="Prämienkatalog Badge"></ui-icon-badge>
<ui-tooltip yPosition="above" xPosition="after" [yOffset]="-11" [xOffset]="-8" #promotionTooltip [closeable]="true">
Dieser Artikel befindet sich im Prämienkatalog.
</ui-tooltip>
</button>
</div>
</div>
<button (click)="showReviews()" class="recessions" *ngIf="item.reviews?.length > 0">
<div class="page-article-details__product-image-recessions flex flex-col items-center">
<div class="page-article-details__product-image">
<button class="border-none outline-none bg-transparent relative" (click)="showImages()">
<img
class="max-h-[19.6875rem] max-w-[12.1875rem] rounded-card"
(load)="loadImage()"
[src]="item.imageId | productImage: 195:315:true"
alt="product image"
/>
<ui-icon
class="absolute text-[#A7B9CB] inline-block bottom-[14px] right-[18px]"
*ngIf="imageLoaded$ | async"
icon="search_add"
size="25px"
></ui-icon>
</button>
</div>
<button
(click)="showReviews()"
class="page-article-details__product-recessions flex flex-col mt-2 items-center bg-transparent border-none outline-none"
*ngIf="item.reviews?.length > 0"
>
<ui-stars [rating]="store.reviewRating$ | async"></ui-stars>
<div class="cta-recessions">{{ item.reviews.length }} Rezensionen</div>
<div class="text-regular text-[#0556B4] font-bold">{{ item.reviews.length }} Rezensionen</div>
</button>
</div>
<div class="product-info">
<div class="row" [class.bookmark-badge-gap]="isBadgeVisible$ | async">
<div>
<a
*ngFor="let contributor of contributors$ | async; let last = last"
class="autor"
[routerLink]="['/kunde', applicationService.activatedProcessId, 'product', 'search', 'results']"
[queryParams]="{ main_qs: contributor, main_author: 'author' }"
>
{{ contributor }}{{ last ? '' : ';' }}
</a>
<div class="page-article-details__product-contributors">
<a
*ngFor="let contributor of contributors$ | async; let last = last"
class="text-[#0556B4] font-semibold no-underline text-base"
[routerLink]="resultsPath"
[queryParams]="{ main_qs: contributor, main_author: 'author' }"
>
{{ contributor }}{{ last ? '' : ';' }}
</a>
</div>
<div class="page-article-details__product-print justify-self-end" [class.mt-4]="isBadgeVisible$ | async">
<button class="bg-transparent text-brand font-bold text-lg outline-none border-none p-0" (click)="print()">Drucken</button>
</div>
<div class="page-article-details__product-title text-2xl font-bold mb-6">
{{ item.product?.name }}
</div>
<div class="page-article-details__product-misc flex flex-col mb-4">
<div
class="page-article-details__product-format flex items-center font-bold text-sm"
*ngIf="item?.product?.format && item?.product?.formatDetail"
>
<img
*ngIf="item?.product?.format !== '--'"
class="flex mr-2 h-[1.125rem]"
[src]="'/assets/images/Icon_' + item.product?.format + '.svg'"
[alt]="item.product?.formatDetail"
/>
{{ item.product?.formatDetail }}
</div>
<div class="page-article-details__product-volume" *ngIf="item?.product?.volume">Band/Reihe {{ item?.product?.volume }}</div>
<div class="page-article-details__product-publication">{{ publicationDate$ | async }}</div>
</div>
<div class="page-article-details__product-price-info flex flex-col mb-4">
<div
class="page-article-details__product-price font-bold text-xl self-end"
*ngIf="item.catalogAvailability?.price?.value?.value; else retailPrice"
>
{{ item.catalogAvailability?.price?.value?.value | currency: item.catalogAvailability?.price?.value?.currency:'code' }}
</div>
<ng-template #retailPrice>
<div
class="page-article-details__product-price font-bold text-xl self-end"
*ngIf="store.takeAwayAvailability$ | async; let takeAwayAvailability"
>
{{ takeAwayAvailability?.retailPrice?.value?.value | currency: takeAwayAvailability?.retailPrice?.value?.currency:'code' }}
</div>
</ng-template>
<button class="cta-print right" (click)="print()">Drucken</button>
</div>
<div class="title">
{{ item.product?.name }}
<div class="page-article-details__product-points self-end" *ngIf="store.promotionPoints$ | async; let promotionPoints">
{{ promotionPoints }} Lesepunkte
</div>
<div class="row">
<div>
<div class="format" *ngIf="item?.product?.format && item?.product?.formatDetail">
<img
*ngIf="item?.product?.format !== '--'"
class="format-icon"
[src]="'/assets/images/Icon_' + item.product?.format + '.svg'"
[alt]="item.product?.formatDetail"
/>
{{ item.product?.formatDetail }}
<!-- TODO: Ticket PREISGEBUNDEN -->
<div class="page-article-details__product-price-bound self-end"></div>
</div>
<div class="page-article-details__product-origin-infos flex flex-col mb-4">
<div class="page-article-details__product-manufacturer" data-name="product-manufacturer">{{ item.product?.manufacturer }}</div>
<div class="page-article-details__product-language" *ngIf="item?.product?.locale" data-name="product-language">
{{ item?.product?.locale }}
</div>
</div>
<div class="page-article-details__product-stock flex justify-end items-center">
<div class="h-5 w-16 bg-[#e6eff9] animate-[load_0.75s_linear_infinite]" *ngIf="store.fetchingTakeAwayAvailability$ | async"></div>
<div
class="flex flex-row py-4 pl-4"
[uiOverlayTrigger]="tooltip"
[overlayTriggerDisabled]="!(stockTooltipText$ | async)"
*ngIf="!(store.fetchingTakeAwayAvailability$ | async)"
>
<ng-container *ngIf="store.takeAwayAvailability$ | async; let takeAwayAvailability">
<ui-icon class="mr-2 mb-1" icon="home" size="15px"></ui-icon>
<span class="font-bold text-sm">{{ takeAwayAvailability.inStock || 0 }}x</span>
</ng-container>
</div>
</div>
<ui-tooltip #tooltip yPosition="above" xPosition="after" [yOffset]="-8" [closeable]="true">
{{ stockTooltipText$ | async }}
</ui-tooltip>
<div class="page-article-details__product-ean-specs flex flex-col">
<div class="page-article-details__product-ean" data-name="product-ean">{{ item.product?.ean }}</div>
<div class="page-article-details__product-specs">
<ng-container *ngIf="item?.specs?.length > 0">
{{ (item?.specs)[0]?.value }}
</ng-container>
</div>
</div>
<div class="page-article-details__product-availabilities flex flex-row items-center justify-end mt-4">
<div
class="h-5 w-6 bg-[#e6eff9] animate-[load_0.75s_linear_infinite]"
*ngIf="store.fetchingTakeAwayAvailability$ | async; else showAvailabilityTakeAwayIcon"
></div>
<ng-template #showAvailabilityTakeAwayIcon>
<div
*ngIf="store.isTakeAwayAvailabilityAvailable$ | async"
class="w-[2.25rem] h-[2.25rem] bg-[#D8DFE5] rounded-[5px_5px_0px_5px] flex items-center justify-center"
>
<ui-icon class="mx-1" icon="shopping_bag" size="18px"> </ui-icon>
</div>
</ng-template>
<div
class="h-5 w-6 bg-[#e6eff9] animate-[load_0.75s_linear_infinite]"
*ngIf="store.fetchingPickUpAvailability$ | async; else showAvailabilityPickUpIcon"
></div>
<ng-template #showAvailabilityPickUpIcon>
<div
*ngIf="store.isPickUpAvailabilityAvailable$ | async"
class="w-[2.25rem] h-[2.25rem] bg-[#D8DFE5] rounded-[5px_5px_0px_5px] flex items-center justify-center ml-3"
>
<ui-icon class="mx-1" icon="box_out" size="18px"></ui-icon>
</div>
</ng-template>
<div
class="h-5 w-6 bg-[#e6eff9] animate-[load_0.75s_linear_infinite]"
*ngIf="store.fetchingDeliveryAvailability$ | async; else showAvailabilityDeliveryIcon"
></div>
<ng-template #showAvailabilityDeliveryIcon>
<div
*ngIf="showDeliveryTruck$ | async"
class="w-[2.25rem] h-[2.25rem] bg-[#D8DFE5] rounded-[5px_5px_0px_5px] flex items-center justify-center ml-3"
>
<ui-icon class="-mb-px-5 -mt-px-5 mx-1" icon="truck" size="30px"></ui-icon>
</div>
</ng-template>
<div
class="h-5 w-6 bg-[#e6eff9] animate-[load_0.75s_linear_infinite]"
*ngIf="store.fetchingDeliveryB2BAvailability$ | async; else showAvailabilityDeliveryB2BIcon"
></div>
<ng-template #showAvailabilityDeliveryB2BIcon>
<div
*ngIf="showDeliveryB2BTruck$ | async"
class="w-[2.25rem] h-[2.25rem] bg-[#D8DFE5] rounded-[5px_5px_0px_5px] flex items-center justify-center ml-3"
>
<ui-icon class="-mb-px-10 -mt-px-10 mx-1" icon="truck_b2b" size="30px"> </ui-icon>
</div>
</ng-template>
<span *ngIf="store.isDownload$ | async" class="flex flex-row items-center">
<div class="w-[2.25rem] h-[2.25rem] bg-[#D8DFE5] rounded-[5px_5px_0px_5px] flex items-center justify-center ml-3">
<ui-icon class="mx-1" icon="download" size="18px"></ui-icon>
<span class="font-bold">Download</span>
</div>
</span>
</div>
<div class="page-article-details__shelf-ssc">
<div class="page-article-details__ssc flex justify-end my-2 font-bold text-lg">
<div class="w-52 h-px-20 bg-[#e6eff9] animate-[load_0.75s_linear_infinite]" *ngIf="fetchingAvailabilities$ | async"></div>
<ng-container *ngIf="!(fetchingAvailabilities$ | async)">
<div *ngIf="store.sscText$ | async; let sscText">
{{ sscText }}
</div>
<div *ngIf="item?.product?.volume">Band/Reihe {{ item?.product?.volume }}</div>
<div>{{ publicationDate$ | async }}</div>
</div>
<div class="right">
<div class="price" *ngIf="item.catalogAvailability?.price?.value?.value; else retailPrice">
{{ item.catalogAvailability?.price?.value?.value | currency: item.catalogAvailability?.price?.value?.currency:'code' }}
</div>
<ng-template #retailPrice>
<div class="price" *ngIf="store.takeAwayAvailability$ | async; let takeAwayAvailability">
{{
takeAwayAvailability?.retailPrice?.value?.value | currency: takeAwayAvailability?.retailPrice?.value?.currency:'code'
}}
</div>
</ng-template>
<div *ngIf="store.promotionPoints$ | async; let promotionPoints">{{ promotionPoints }} Lesepunkte</div>
</div>
</ng-container>
</div>
<div class="row stock">
<div data-name="product-manufacturer">{{ item.product?.manufacturer }}</div>
<div class="right quantity" [uiOverlayTrigger]="tooltip" [overlayTriggerDisabled]="!(stockTooltipText$ | async)">
<div class="fetching small" *ngIf="store.fetchingTakeAwayAvailability$ | async"></div>
<ng-container *ngIf="!(store.fetchingTakeAwayAvailability$ | async)">
<ng-container *ngIf="store.takeAwayAvailability$ | async; let takeAwayAvailability">
<ui-icon icon="home" size="22px"></ui-icon>
{{ takeAwayAvailability.inStock || 0 }}x
</ng-container>
</ng-container>
</div>
<ui-tooltip #tooltip yPosition="above" xPosition="after" [yOffset]="-8" [closeable]="true">
{{ stockTooltipText$ | async }}
</ui-tooltip>
</div>
<div *ngIf="item?.product?.locale" data-name="product-language">{{ item?.product?.locale }}</div>
<div class="row">
<div data-name="product-ean">{{ item.product?.ean }}</div>
<div class="right">
<div class="availability-icons">
<div class="fetching xsmall" *ngIf="store.fetchingTakeAwayAvailability$ | async; else showAvailabilityTakeAwayIcon"></div>
<ng-template #showAvailabilityTakeAwayIcon>
<ui-icon *ngIf="store.isTakeAwayAvailabilityAvailable$ | async" icon="shopping_bag" size="18px"> </ui-icon>
</ng-template>
<div class="fetching xsmall" *ngIf="store.fetchingPickUpAvailability$ | async; else showAvailabilityPickUpIcon"></div>
<ng-template #showAvailabilityPickUpIcon>
<ui-icon *ngIf="store.isPickUpAvailabilityAvailable$ | async" icon="box_out" size="18px"></ui-icon>
</ng-template>
<div class="fetching xsmall" *ngIf="store.fetchingDeliveryAvailability$ | async; else showAvailabilityDeliveryIcon"></div>
<ng-template #showAvailabilityDeliveryIcon>
<ui-icon *ngIf="showDeliveryTruck$ | async" class="truck" icon="truck" size="30px"></ui-icon>
</ng-template>
<div
class="fetching xsmall"
*ngIf="store.fetchingDeliveryB2BAvailability$ | async; else showAvailabilityDeliveryB2BIcon"
></div>
<ng-template #showAvailabilityDeliveryB2BIcon>
<ui-icon *ngIf="showDeliveryB2BTruck$ | async" class="truck_b2b" icon="truck_b2b" size="40px"> </ui-icon>
</ng-template>
<span *ngIf="store.isDownload$ | async" class="download-icon">
<ui-icon icon="download" size="18px"></ui-icon>
<span class="label">Download</span>
</span>
</div>
</div>
</div>
<div class="shelfinfo right" *ngIf="store.isDownload$ | async">
<div class="page-article-details__shelfinfo" *ngIf="store.isDownload$ | async">
<ng-container
*ngIf="
item?.stockInfos && item?.shelfInfos && (item?.stockInfos)[0]?.compartment && (item?.shelfInfos)[0]?.label;
@@ -145,24 +261,7 @@
</ng-container>
</ng-template>
</div>
<div class="row">
<div class="specs">
<ng-container *ngIf="item?.specs?.length > 0">
{{ (item?.specs)[0]?.value }}
</ng-container>
</div>
<div class="right ssc">
<div class="fetching" *ngIf="fetchingAvailabilities$ | async"></div>
<ng-container *ngIf="!(fetchingAvailabilities$ | async)">
<div *ngIf="store.sscText$ | async; let sscText">
{{ sscText }}
</div>
</ng-container>
</div>
</div>
<div class="shelfinfo right" *ngIf="!(store.isDownload$ | async)">
<div class="page-article-details__shelfinfo text-right" *ngIf="!(store.isDownload$ | async)">
<ng-container
*ngIf="
item?.stockInfos && item?.shelfInfos && (item?.stockInfos)[0]?.compartment && (item?.shelfInfos)[0]?.label;
@@ -186,109 +285,115 @@
</ng-template>
</div>
</div>
</div>
<div class="bookmark">
<div *ngIf="showArchivBadge$ | async" class="archiv-badge">
<button [uiOverlayTrigger]="archivTooltip" class="bookmark-badge">
<img src="/assets/images/bookmark_benachrichtigung_archiv.svg" alt="Archiv Badge" />
<ui-tooltip yPosition="above" xPosition="after" [yOffset]="-11" [xOffset]="-8" #archivTooltip [closeable]="true">
<ng-container *ngIf="isAvailable$ | async; else notAvailable">
Archivtitel. Wird nicht mehr gedruckt. Artikel ist bestellbar, weil lieferbar.
</ng-container>
<ng-template #notAvailable>
Archivtitel. Wird nicht mehr gedruckt. Nicht bestellbar.
</ng-template>
</ui-tooltip>
</button>
</div>
<div *ngIf="showSubscriptionBadge$ | async">
<button [uiOverlayTrigger]="subscribtionTooltip" class="bookmark-badge">
<img src="/assets/images/bookmark_subscription.svg" alt="Fortsetzungsartikel Badge" />
</button>
<ui-tooltip yPosition="above" xPosition="after" [yOffset]="-11" [xOffset]="-8" #subscribtionTooltip [closeable]="true"
>Artikel ist ein Fortsetzungsartikel,<br />
Artikel muss über eine Aboabteilung<br />
bestellt werden.
</ui-tooltip>
</div>
<div *ngIf="showPromotionBadge$ | async" class="promotion-badge">
<button [uiOverlayTrigger]="promotionTooltip" class="bookmark-badge">
<ui-icon-badge icon="gift" alt="Prämienkatalog Badge"></ui-icon-badge>
<ui-tooltip yPosition="above" xPosition="after" [yOffset]="-11" [xOffset]="-8" #promotionTooltip [closeable]="true">
Dieser Artikel befindet sich im Prämienkatalog.
</ui-tooltip>
</button>
<div class="page-article-details__product-formats-container mt-3" *ngIf="item.family?.length > 0">
<hr class="bg-[#E6EFF9] border-t-2" />
<div class="pt-3">
<div class="page-article-details__product-formats">
<span class="mr-2">Auch verfügbar als</span>
<ui-slider [scrollDistance]="250">
<a
class="mr-4 text-[#0556B4] font-bold no-underline px-2"
*ngFor="let format of item.family"
[routerLink]="getDetailsPath(format.product.ean)"
[queryParamsHandling]="!(isTablet$ | async) ? 'preserve' : ''"
>
<span class="flex items-center">
<img
class="mr-2"
*ngIf="!!format.product?.format"
[src]="'/assets/images/OF_Icon_' + format.product?.format + '.svg'"
alt="format icon"
/>
{{ format.product?.formatDetail }}
<span class="ml-1">{{ format.catalogAvailability?.price?.value?.value | currency: '€' }}</span>
</span>
</a>
</ui-slider>
</div>
</div>
</div>
<div class="product-actions">
<button *ngIf="!(store.isDownload$ | async)" class="cta-availabilities" (click)="showAvailabilities()">
Vorrätig in anderer Filiale?
</button>
<button
class="cta-continue"
(click)="showPurchasingModal()"
[disabled]="
!(isAvailable$ | async) || (fetchingAvailabilities$ | async) || (item?.features && (item?.features)[0]?.key === 'PFO')
"
>
In den Warenkorb
</button>
</div>
<hr class="bg-[#E6EFF9] border-t-2 my-3" />
<hr />
<ng-container *ngIf="item.family?.length > 0">
<div class="product-formats">
<span class="label">Auch verfügbar als</span>
<ui-slider [scrollDistance]="250">
<a
class="product-family"
*ngFor="let format of item.family"
[routerLink]="['/kunde', applicationService.activatedProcessId, 'product', 'details', 'ean', format.product.ean]"
>
<span class="format-detail">
<img
*ngIf="!!format.product?.format"
[src]="'/assets/images/OF_Icon_' + format.product?.format + '.svg'"
alt="format icon"
/>
{{ format.product?.formatDetail }}
<span class="price">{{ format.catalogAvailability?.price?.value?.value | currency: '€' }}</span>
</span>
</a>
</ui-slider>
</div>
<hr />
</ng-container>
<div class="product-description" *ngIf="item.texts?.length > 0">
<div class="info">
<div
#description
class="page-article-details__product-description flex flex-col flex-grow overflow-hidden overflow-y-scroll"
*ngIf="item.texts?.length > 0"
>
<div class="whitespace-pre-line">
{{ item.texts[0].value }}
</div>
<div class="product-text">
<button class="font-bold flex flex-row text-[#0556B4] items-center mt-2" *ngIf="!showMore" (click)="showMore = !showMore">
Mehr <ui-icon class="ml-2" size="15px" icon="arrow"></ui-icon>
</button>
<div
*ngIf="showMore"
class="page-article-details__product-description-text flex flex-col whitespace-pre-line mb-px-100 break-words"
>
<span *ngFor="let text of item.texts | slice: 1">
<h3 class="header">{{ text.label }}</h3>
<h3 class="my-4 text-regular font-bold">{{ text.label }}</h3>
{{ text.value }}
</span>
<button class="scroll-top-cta" (click)="scrollTop()">
<ui-icon class="arrow" icon="arrow" size="20px"></ui-icon>
<button class="font-bold flex flex-row text-[#0556B4] items-center mt-2" (click)="showMore = !showMore">
<ui-icon class="transform ml-0 mr-2 rotate-180" size="15px" icon="arrow"></ui-icon> Weniger
</button>
<button class="page-article-details__scroll-top-cta" (click)="scrollTop(description)">
<ui-icon class="text-[#0556B4]" icon="arrow" size="20px"></ui-icon>
</button>
</div>
</div>
<ng-container *ngIf="!showRecommendations">
<div
*ngIf="store.item$ | async; let item"
class="page-article-details__actions w-full absolute text-center left-0 bottom-10 z-fixed"
>
<button
*ngIf="!(store.isDownload$ | async)"
class="text-brand border-2 border-brand bg-white font-bold text-lg px-[1.375rem] py-4 rounded-full mr-px-30"
(click)="showAvailabilities()"
>
Bestände in anderen Filialen
</button>
<button
class="text-white bg-brand border-brand font-bold text-lg px-[1.375rem] py-4 rounded-full border-none no-underline"
(click)="showPurchasingModal()"
[disabled]="
!(isAvailable$ | async) || (fetchingAvailabilities$ | async) || (item?.features && (item?.features)[0]?.key === 'PFO')
"
>
In den Warenkorb
</button>
</div>
</ng-container>
<div class="page-article-details__product-recommendations -mx-5">
<button
*ngIf="store.item$ | async; let item"
class="shadow-[#dce2e9_0px_-2px_18px_0px] sticky bottom-4 border-none outline-none left-0 right-0 flex items-center px-5 h-14 min-h-[3.5rem] bg-white w-full"
(click)="showRecommendations = true"
>
<span class="uppercase text-[#0556B4] font-bold text-small">Empfehlungen</span>
<img class="absolute right-5 bottom-3 h-12" src="assets/images/recommendation_tag.png" alt="recommendation icon" />
</button>
</div>
</ng-container>
</div>
<button *ngIf="store.item$ | async; let item" class="product-recommendations" (click)="showRecommendations = true">
<span class="label">Empfehlungen</span>
<img src="assets/images/recommendation_tag.png" alt="recommendation icon" />
</button>
</ng-container>
<div class="recommendations-overlay" @slideYAnimation *ngIf="showRecommendations">
<button class="product-button" (click)="showRecommendations = false">{{ (store.item$ | async)?.product?.name }}</button>
<div class="page-article-details__recommendations-overlay absolute top-16 rounded-t-card" @slideYAnimation *ngIf="showRecommendations">
<button
class="h-[3.75rem] shadow-[0_-2px_24px_0_#dce2e9] flex flex-row justify-center items-center w-full text-xl bg-white text-ucla-blue font-bold border-none outline-none rounded-t-card"
(click)="showRecommendations = false"
>
{{ (store.item$ | async)?.product?.name }}
</button>
<page-article-recommendations (close)="showRecommendations = false"></page-article-recommendations>
</div>

View File

@@ -1,269 +1,99 @@
:host {
@apply flex flex-col;
@apply box-border block h-[calc(100vh-16.5rem)] desktop-small:h-[calc(100vh-15.1rem)];
}
.product-card {
@apply flex flex-col bg-white w-full rounded-card shadow-card;
.product-details {
@apply flex flex-row p-5;
.bookmark {
@apply absolute flex;
top: 52px;
right: 25px;
z-index: 100;
}
.bookmark-badge {
@apply p-0 m-0 outline-none border-none bg-transparent relative;
}
.promotion-badge {
margin-top: -1px;
}
.bookmark-badge-gap {
@apply mt-px-35;
}
.product-image {
@apply flex flex-col items-center justify-start mr-5;
.recessions {
@apply flex flex-col items-center mt-4 bg-transparent border-none outline-none;
.cta-recessions {
@apply text-regular text-dark-cerulean font-bold mt-2;
}
}
.image-button {
@apply border-none outline-none bg-transparent relative;
ui-icon {
@apply absolute text-dark-cerulean inline-block;
bottom: 1rem;
right: 1rem;
}
}
img {
@apply rounded-xl shadow-card;
box-shadow: 0 0 18px 0 #b8b3b7;
max-height: 315px;
max-width: 195px;
}
}
.product-info {
@apply w-full;
.title {
@apply text-3xl font-bold mb-6;
}
.format,
.ssc,
.quantity {
@apply font-bold text-lg;
}
.stock {
min-height: 44px;
}
.quantity {
@apply flex justify-end mt-4;
ui-icon {
@apply mr-1 text-ucla-blue;
}
}
.format {
@apply flex items-center;
.format-icon {
@apply flex mr-2;
height: 18px;
}
}
.ssc {
@apply flex justify-end my-2;
}
.price {
@apply font-bold text-xl;
}
.shelfinfo {
@apply text-ucla-blue;
}
.fetching {
@apply w-52 h-px-20;
background-color: #e6eff9;
animation: load 0.75s linear infinite;
}
.xsmall {
@apply w-6;
}
.small {
@apply w-16;
}
.medium {
@apply w-40;
}
.availability-icons {
@apply flex flex-row items-center justify-end text-dark-cerulean mt-4;
ui-icon {
@apply mx-1;
}
.truck {
@apply -mb-px-5 -mt-px-5;
}
.truck_b2b {
@apply -mb-px-10 -mt-px-10;
}
.label {
@apply font-bold;
}
.download-icon {
@apply flex flex-row items-center;
}
}
.cta-print {
@apply bg-transparent text-brand font-bold text-xl outline-none border-none p-0;
}
}
.row {
@apply grid items-end;
grid-template-columns: auto auto;
}
}
.right {
@apply text-right self-start;
}
hr {
@apply bg-glitter h-1;
}
.product-description {
@apply flex flex-col flex-grow px-5 py-5;
min-height: calc(100vh - 769px);
.info {
@apply whitespace-pre-line;
}
.product-text {
@apply flex flex-col whitespace-pre-line mb-px-100 break-words;
h3 {
@apply my-4;
}
.header {
@apply text-regular font-bold;
}
.scroll-top-cta {
@apply flex items-center justify-center self-end border-none outline-none bg-white relative rounded p-0 mt-8 mr-4;
box-shadow: 0px 0px 20px 0px rgba(89, 100, 112, 0.5);
transform: rotate(-90deg);
border-radius: 100%;
width: 58px;
height: 58px;
.arrow {
color: #1f466c;
}
}
}
}
.product-actions {
@apply text-right px-5 py-4;
.cta-availabilities {
@apply text-brand border-none border-brand bg-white font-bold text-lg px-4 py-2 rounded-full;
}
.cta-continue {
@apply text-white bg-brand font-bold text-lg px-4 py-2 rounded-full border-none ml-4 no-underline;
&:disabled {
@apply bg-inactive-branch;
}
}
}
.product-formats {
@apply grid whitespace-nowrap items-center px-5 py-4;
grid-template-rows: auto;
grid-template-columns: auto 1fr;
max-width: 100%;
.label {
@apply mr-2;
}
.product-family {
@apply mr-4 text-active-customer font-bold no-underline px-2;
.format-detail {
@apply flex items-center;
img {
@apply mr-2;
}
}
.price {
@apply ml-1;
}
}
}
.page-article-details__container {
@apply h-full w-full bg-white rounded-card shadow-card flex flex-col;
}
.product-recommendations {
@apply sticky bottom-0 border-none outline-none left-0 right-0 flex items-center px-5 h-16 bg-white w-full;
box-shadow: #dce2e9 0px -2px 18px 0px;
.label {
@apply uppercase text-active-customer font-bold text-small;
}
img {
@apply absolute right-5 bottom-5 h-12;
}
.page-article-details__product-details {
@apply grid gap-x-5;
grid-template-columns: max-content auto;
grid-template-rows: 2.1875rem repeat(11, minmax(auto, max-content));
grid-template-areas:
'. . . bookmark'
'image contributors contributors contributors'
'image title title print'
'image title title .'
'image misc misc price'
'image misc misc price'
'image origin origin stock'
'image origin origin stock'
'image specs availabilities availabilities'
'image specs ssc ssc'
'image . ssc ssc'
'image . ssc ssc';
}
.recommendations-overlay {
@apply absolute w-full top-0 rounded-t-card;
top: 56px;
.page-article-details__product-bookmark {
grid-area: bookmark;
}
.product-button {
@apply flex flex-row justify-center items-center w-full text-xl bg-white text-ucla-blue font-bold border-none outline-none rounded-t-card;
box-shadow: 0 -2px 24px 0 #dce2e9;
height: 60px;
.page-article-details__product-image-recessions {
grid-area: image;
}
.page-article-details__product-contributors {
grid-area: contributors;
}
.page-article-details__product-print {
grid-area: print;
}
.page-article-details__product-title {
grid-area: title;
}
.page-article-details__product-misc {
grid-area: misc;
}
.page-article-details__product-price-info {
grid-area: price;
}
.page-article-details__product-origin-infos {
grid-area: origin;
}
.page-article-details__product-stock {
grid-area: stock;
}
.page-article-details__product-ean-specs {
grid-area: specs;
}
.page-article-details__product-availabilities {
grid-area: availabilities;
}
.page-article-details__shelf-ssc {
grid-area: ssc;
}
.page-article-details__product-description-text {
word-break: break-word;
}
.page-article-details__product-formats {
@apply grid whitespace-nowrap items-center max-w-full;
grid-template-rows: auto;
grid-template-columns: auto 1fr;
}
.page-article-details__scroll-top-cta {
@apply flex items-center justify-center self-end border-none outline-none bg-white relative rounded p-0 mt-8 mr-4;
box-shadow: 0px 0px 20px 0px rgba(89, 100, 112, 0.5);
transform: rotate(-90deg);
border-radius: 100%;
width: 58px;
height: 58px;
}
.page-article-details__actions {
&:disabled {
@apply bg-inactive-branch;
}
}
.autor {
@apply text-active-customer font-bold no-underline;
}

View File

@@ -8,7 +8,7 @@ import { BranchDTO } from '@swagger/checkout';
import { UiModalService } from '@ui/modal';
import { ModalReviewsComponent } from '@modal/reviews';
import { BehaviorSubject, combineLatest, Subscription } from 'rxjs';
import { debounceTime, filter, first, map, shareReplay, switchMap } from 'rxjs/operators';
import { debounceTime, filter, first, map, shareReplay, switchMap, withLatestFrom } from 'rxjs/operators';
import { ArticleDetailsStore } from './article-details.store';
import { ModalImagesComponent } from 'apps/modal/images/src/public-api';
import { ProductImageService } from 'apps/cdn/product-image/src/public-api';
@@ -20,6 +20,8 @@ import { DateAdapter } from '@ui/common';
import { DatePipe } from '@angular/common';
import { PurchaseOptionsModalService } from '@shared/modals/purchase-options-modal';
import { DomainAvailabilityService } from '@domain/availability';
import { EnvironmentService } from '@core/environment';
import { ProductCatalogNavigationService } from '@shared/services';
@Component({
selector: 'page-article-details',
@@ -113,6 +115,19 @@ export class ArticleDetailsComponent implements OnInit, OnDestroy {
shareReplay(1)
);
get isTablet$() {
return this._environment.matchTablet$.pipe(
map((state) => state?.matches),
shareReplay()
);
}
get resultsPath() {
return this._navigationService.getArticleSearchResultsPath(this.applicationService.activatedProcessId);
}
showMore: boolean = false;
constructor(
public readonly applicationService: ApplicationService,
private activatedRoute: ActivatedRoute,
@@ -126,6 +141,8 @@ export class ArticleDetailsComponent implements OnInit, OnDestroy {
public elementRef: ElementRef,
private _purchaseOptionsModalService: PurchaseOptionsModalService,
private _availability: DomainAvailabilityService,
private _navigationService: ProductCatalogNavigationService,
private _environment: EnvironmentService,
private _router: Router
) {}
@@ -161,16 +178,30 @@ export class ArticleDetailsComponent implements OnInit, OnDestroy {
filter((f) => !!f)
);
const more$ = this.activatedRoute.params.subscribe(() => (this.showMore = false));
this.subscriptions.add(processIdSubscription);
this.subscriptions.add(more$);
this.subscriptions.add(this.store.loadItemById(id$));
this.subscriptions.add(this.store.loadItemByEan(ean$));
this.subscriptions.add(this.store.item$.pipe(filter((item) => !!item)).subscribe((item) => this.updateBreadcrumb(item)));
this.subscriptions.add(
this.store.item$
.pipe(
withLatestFrom(this.isTablet$),
filter(([item, isTablet]) => !!item)
)
.subscribe(([item, isTablet]) => (isTablet ? this.updateBreadcrumb(item) : this.updateBreadcrumbDesktop(item)))
);
}
ngOnDestroy() {
this.subscriptions.unsubscribe();
}
getDetailsPath(ean?: string) {
return this._navigationService.getArticleDetailsPath({ processId: this.applicationService.activatedProcessId, ean });
}
async updateBreadcrumb(item: ItemDTO) {
const crumbs = await this.breadcrumb
.getBreadcrumbsByKeyAndTags$(this.applicationService.activatedProcessId, ['catalog', 'details', `${item.id}`])
@@ -184,13 +215,41 @@ export class ArticleDetailsComponent implements OnInit, OnDestroy {
this.breadcrumb.addBreadcrumbIfNotExists({
key: this.applicationService.activatedProcessId,
name: item.product?.name,
path: `/kunde/${this.applicationService.activatedProcessId}/product/details/${item.id}`,
path: this._navigationService.getArticleDetailsPath({ processId: this.applicationService.activatedProcessId, itemId: item.id }),
params: this.activatedRoute.snapshot.queryParams,
tags: ['catalog', 'details', `${item.id}`],
section: 'customer',
});
}
async updateBreadcrumbDesktop(item: ItemDTO) {
const crumbs = await this.breadcrumb
.getBreadcrumbsByKeyAndTags$(this.applicationService.activatedProcessId, ['catalog', 'details'])
.pipe(first())
.toPromise();
if (crumbs.length === 0) {
this.breadcrumb.addBreadcrumbIfNotExists({
key: this.applicationService.activatedProcessId,
name: item.product?.name,
path: this._navigationService.getArticleDetailsPath({ processId: this.applicationService.activatedProcessId, itemId: item.id }),
params: this.activatedRoute.snapshot.queryParams,
tags: ['catalog', 'details', `${item.id}`],
section: 'customer',
});
} else {
const crumb = crumbs.find((_) => true);
this.breadcrumb.patchBreadcrumb(crumb.id, {
key: this.applicationService.activatedProcessId,
name: item.product?.name,
path: this._navigationService.getArticleDetailsPath({ processId: this.applicationService.activatedProcessId, itemId: item.id }),
params: this.activatedRoute.snapshot.queryParams,
tags: ['catalog', 'details', `${item.id}`],
section: 'customer',
});
}
}
async print() {
const item = await this.store.item$.pipe(first()).toPromise();
this.uiModal.open({
@@ -302,9 +361,8 @@ export class ArticleDetailsComponent implements OnInit, OnDestroy {
}
}
scrollTop() {
const element = this.elementRef.nativeElement.closest('.main-wrapper');
element?.scrollTo({ top: 0, behavior: 'smooth' });
scrollTop(div: HTMLDivElement) {
div?.scrollTo({ top: 0, behavior: 'smooth' });
}
loadImage() {

View File

@@ -1,7 +1,6 @@
:host {
@apply flex flex-col bg-white;
@apply flex flex-col bg-white h-[calc(100vh-16.5rem-3.75rem)] desktop-small:h-[calc(100vh-15.1rem-3.75rem)];
box-shadow: 0px -2px 24px 0px #dce2e9;
height: calc(100vh - 342px);
}
h1 {

View File

@@ -1,13 +1 @@
<button class="filter" [class.active]="hasFilter$ | async" (click)="filterActive$.next(true); shellFilterOverlay.open()">
<ui-icon size="20px" icon="filter_alit"></ui-icon>
<span class="label">Filter</span>
</button>
<router-outlet></router-outlet>
<shell-filter-overlay #shellFilterOverlay>
<page-article-search-filter
*ngIf="filterActive$ | async"
(close)="filterActive$.next(false); shellFilterOverlay.close()"
></page-article-search-filter>
</shell-filter-overlay>

View File

@@ -1,17 +1,3 @@
:host {
@apply flex flex-col w-full box-content relative;
}
.filter {
@apply font-sans flex self-end items-center mb-4 font-bold bg-wild-blue-yonder border-0 text-regular py-px-8 px-px-15 rounded-filter justify-center z-sticky;
width: 106px;
min-width: 106px;
.label {
@apply ml-px-5;
}
&.active {
@apply bg-active-customer text-white ml-px-5;
}
@apply flex flex-col w-full h-[calc(100vh-16.5rem)] desktop-small:h-[calc(100vh-15.1rem)] box-content relative;
}

View File

@@ -1,13 +1,15 @@
import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { ActivatedRoute } from '@angular/router';
import { BreadcrumbService } from '@core/breadcrumb';
import { UiFilterAutocompleteProvider } from '@ui/filter';
import { isEqual } from 'lodash';
import { BehaviorSubject, Observable, Subject } from 'rxjs';
import { filter, first, map, takeUntil, withLatestFrom } from 'rxjs/operators';
import { Observable, Subject } from 'rxjs';
import { map, takeUntil, withLatestFrom } from 'rxjs/operators';
import { ArticleSearchService } from './article-search.store';
import { FocusSearchboxEvent } from './focus-searchbox.event';
import { ArticleSearchMainAutocompleteProvider } from './providers';
import { ProductCatalogNavigationService } from '@shared/services';
import { FilterAutocompleteProvider } from 'apps/shared/components/filter/src/lib';
import { isEqual } from 'lodash';
import { EnvironmentService } from '@core/environment';
@Component({
selector: 'page-article-search',
@@ -15,9 +17,8 @@ import { ArticleSearchMainAutocompleteProvider } from './providers';
styleUrls: ['article-search.component.scss'],
providers: [
FocusSearchboxEvent,
ArticleSearchService,
{
provide: UiFilterAutocompleteProvider,
provide: FilterAutocompleteProvider,
useClass: ArticleSearchMainAutocompleteProvider,
multi: true,
},
@@ -28,22 +29,16 @@ export class ArticleSearchComponent implements OnInit, OnDestroy {
private _onDestroy$ = new Subject();
private _processId$: Observable<number>;
initialFilter$ = this._articleSearch.filter$.pipe(
filter((filter) => !!filter),
first()
);
get isTablet() {
return this._environmentService.matchTablet();
}
hasFilter$ = this._articleSearch.filter$.pipe(
withLatestFrom(this.initialFilter$),
map(([filter, initialFilter]) => !isEqual(filter?.getQueryParams(), initialFilter?.getQueryParams()))
);
filterActive$ = new BehaviorSubject<boolean>(false);
constructor(
private _breadcrumb: BreadcrumbService,
private _router: Router,
private _articleSearch: ArticleSearchService,
private _activatedRoute: ActivatedRoute
private _activatedRoute: ActivatedRoute,
private _navigationService: ProductCatalogNavigationService,
private _environmentService: EnvironmentService
) {}
ngOnInit() {
@@ -60,12 +55,17 @@ export class ArticleSearchComponent implements OnInit, OnDestroy {
.pipe(takeUntil(this._onDestroy$), withLatestFrom(this._processId$))
.subscribe(([state, processId]) => {
if (state.searchState === '') {
const params = state.filter.getQueryParams();
if (state.hits === 1) {
const item = state.items.find((f) => f);
this._router.navigate(['/kunde', processId, 'product', 'details', item.id]);
this._navigationService.navigateToDetails({
processId,
itemId: item.id,
queryParams: this.isTablet ? undefined : params,
});
} else {
const params = state.filter.getQueryParams();
this._router.navigate(['/kunde', processId, 'product', 'search', 'results'], {
this._navigationService.navigateToResults({
processId,
queryParams: params,
});
}
@@ -73,6 +73,20 @@ export class ArticleSearchComponent implements OnInit, OnDestroy {
});
}
cleanupQueryParams(params: Record<string, string> = {}) {
const clean = { ...params };
for (const key in clean) {
if (Object.prototype.hasOwnProperty.call(clean, key)) {
if (clean[key] == undefined) {
delete clean[key];
}
}
}
return clean;
}
initProcessId() {
this._processId$ = this._activatedRoute.parent.data.pipe(map((data) => Number(data.processId)));
}
@@ -96,7 +110,7 @@ export class ArticleSearchComponent implements OnInit, OnDestroy {
await this._breadcrumb.addBreadcrumbIfNotExists({
key: processId,
name: 'Artikelsuche',
path: `/kunde/${processId}/product`,
path: this._navigationService.getArticleSearchBasePath(processId),
params: queryParams,
tags: ['catalog', 'main'],
section: 'customer',

View File

@@ -6,12 +6,13 @@ import { ArticleSearchComponent } from './article-search.component';
import { SearchResultsModule } from './search-results/search-results.module';
import { SearchMainModule } from './search-main/search-main.module';
import { SearchFilterModule } from './search-filter/search-filter.module';
import { ShellFilterOverlayModule } from '@shell/filter-overlay';
import { ArticleSearchService } from './article-search.store';
import { SharedFilterOverlayModule } from '@shared/components/filter-overlay';
@NgModule({
imports: [CommonModule, RouterModule, UiIconModule, SearchResultsModule, SearchMainModule, SearchFilterModule, ShellFilterOverlayModule],
imports: [CommonModule, RouterModule, UiIconModule, SearchResultsModule, SearchMainModule, SearchFilterModule, SharedFilterOverlayModule],
exports: [ArticleSearchComponent],
declarations: [ArticleSearchComponent],
providers: [],
providers: [ArticleSearchService],
})
export class ArticleSearchModule {}

View File

@@ -3,24 +3,31 @@ import { DomainCatalogService } from '@domain/catalog';
import { Observable, Subject } from 'rxjs';
import { debounceTime, map, switchMap, tap, withLatestFrom } from 'rxjs/operators';
import { UiFilter } from '@ui/filter';
import { ComponentStore, tapResponse } from '@ngrx/component-store';
import { ItemDTO, QueryTokenDTO } from '@swagger/cat';
import { ItemDTO, QueryTokenDTO, UISettingsDTO } from '@swagger/cat';
import { ApplicationService } from '@core/application';
import { BranchDTO } from '@swagger/checkout';
import { Filter } from 'apps/shared/components/filter/src/lib';
export interface ArticleSearchState {
processId: number;
filter: UiFilter;
filter: Filter;
searchState: '' | 'fetching' | 'empty' | 'error';
items: ItemDTO[];
hits: number;
selectedBranch: BranchDTO;
selectedItemIds: number[];
defaultSettings?: UISettingsDTO;
}
@Injectable()
export class ArticleSearchService extends ComponentStore<ArticleSearchState> {
get defaultSettings() {
return this.get((s) => s.defaultSettings);
}
readonly defaultSettings$ = this.select((s) => s.defaultSettings);
get processId() {
return this.get((s) => s.processId);
}
@@ -100,19 +107,19 @@ export class ArticleSearchService extends ComponentStore<ArticleSearchState> {
}
async setDefaultFilter(defaultQueryParams?: Record<string, string>) {
const filter = await this.catalog
.getSettings()
.pipe(map((settings) => UiFilter.create(settings)))
.toPromise();
const defaultSettings = await this.catalog.getSettings().toPromise();
const filter = Filter.create(defaultSettings);
if (!!defaultQueryParams) {
filter?.fromQueryParams(defaultQueryParams);
}
this.setFilter(filter);
this.patchState({ defaultSettings });
}
setFilter(filter: UiFilter) {
setFilter(filter: Filter) {
this.patchState({ filter });
}

View File

@@ -1,18 +1,18 @@
import { Injectable } from '@angular/core';
import { DomainCatalogService } from '@domain/catalog';
import { UiFilterAutocomplete, UiFilterAutocompleteProvider, UiInput } from '@ui/filter';
import { FilterAutocomplete, FilterAutocompleteProvider, FilterInput } from 'apps/shared/components/filter/src/lib';
import { Observable, of } from 'rxjs';
import { catchError, map } from 'rxjs/operators';
@Injectable()
export class ArticleSearchMainAutocompleteProvider extends UiFilterAutocompleteProvider {
export class ArticleSearchMainAutocompleteProvider extends FilterAutocompleteProvider {
for = 'catalog';
constructor(private domainCatalogSearch: DomainCatalogService) {
super();
}
complete(input: UiInput): Observable<UiFilterAutocomplete[]> {
complete(input: FilterInput): Observable<FilterAutocomplete[]> {
const token = input?.parent?.parent?.getQueryToken();
const filter = token?.filter;
const type = Object.keys(token?.input).join(';');

View File

@@ -1,30 +1,33 @@
<ng-container *ngIf="filter$ | async; let filter">
<div class="catalog-search-filter-content">
<button class="btn-close" type="button" (click)="close.emit()">
<ui-icon icon="close" size="20px"></ui-icon>
</button>
<div class="w-full flex flex-row justify-end items-center">
<button (click)="clearFilter(filter)" class="text-[#0556B4] mr-[0.8125rem]">Alle Filter entfernen</button>
<button class="text-black p-4 outline-none border-none bg-transparent" type="button" (click)="closeFilter()">
<ui-icon icon="close" size="15px"></ui-icon>
</button>
</div>
<div class="catalog-search-filter-content-main">
<h1 class="text-3xl font-bold text-center py-4">Filter</h1>
<ui-filter
<div class="catalog-search-filter-content-main -mt-12">
<h1 class="text-2xl text-[1.625rem] font-bold text-center pt-6 pb-10">Filter</h1>
<shared-filter
[filter]="filter"
[loading]="fetching$ | async"
(search)="applyFilter(filter)"
[hint]="searchboxHint$ | async"
resizeInputOptionsToElement="page-article-search-filter .cta-wrapper"
[scanner]="true"
></ui-filter>
></shared-filter>
</div>
<div class="cta-wrapper">
<button class="cta-reset-filter" (click)="resetFilter(filter)" [disabled]="fetching$ | async">
Filter zurücksetzen
</button>
<button class="cta-apply-filter" (click)="applyFilter(filter)" [disabled]="fetching$ | async">
<ui-spinner [show]="fetching$ | async">
Filter anwenden
</ui-spinner>
</button>
</div>
</div>
<div class="cta-wrapper">
<button class="cta-reset-filter" (click)="resetFilter(filter)" [disabled]="fetching$ | async">
Filter zurücksetzen
</button>
<button class="cta-apply-filter" (click)="applyFilter(filter)" [disabled]="fetching$ | async">
<ui-spinner [show]="fetching$ | async">
Filter anwenden
</ui-spinner>
</button>
</div>
</ng-container>

View File

@@ -1,15 +1,11 @@
:host {
@apply block bg-glitter;
@apply block bg-white h-[calc(100vh-16.5rem)] desktop-small:h-[calc(100vh-15.1rem)];
}
.catalog-search-filter-content {
@apply relative mx-auto p-4;
}
.btn-close {
@apply absolute text-cool-grey top-3 p-4 right-4 outline-none border-none bg-transparent;
}
.catalog-search-filter-content-main {
h1.title {
@apply text-center;
@@ -17,14 +13,12 @@
}
.cta-wrapper {
@apply fixed bottom-8 whitespace-nowrap;
left: 50%;
transform: translateX(-50%);
@apply text-center whitespace-nowrap absolute bottom-8 left-0 w-full;
}
.cta-reset-filter,
.cta-apply-filter {
@apply text-lg font-bold px-6 py-3 rounded-full border-solid border-2 border-brand outline-none mx-2;
@apply text-lg font-bold px-6 py-[0.85rem] rounded-full border-solid border-2 border-brand outline-none mx-2;
&:disabled {
@apply bg-inactive-branch cursor-not-allowed border-none text-white;
@@ -38,3 +32,7 @@
.cta-apply-filter {
@apply text-white bg-brand;
}
::ng-deep page-article-search-filter shared-filter shared-filter-input-group-main {
@apply desktop:hidden px-16;
}

View File

@@ -1,8 +1,13 @@
import { ChangeDetectionStrategy, Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core';
import { UiFilter, UiFilterComponent } from '@ui/filter';
import { Observable } from 'rxjs';
import { map, take } from 'rxjs/operators';
import { ChangeDetectionStrategy, Component, EventEmitter, OnDestroy, OnInit, Output, ViewChild } from '@angular/core';
import { ApplicationService } from '@core/application';
import { EnvironmentService } from '@core/environment';
import { Observable, Subject } from 'rxjs';
import { first, map, takeUntil, withLatestFrom } from 'rxjs/operators';
import { ArticleSearchService } from '../article-search.store';
import { ActivatedRoute } from '@angular/router';
import { ProductCatalogNavigationService } from '@shared/services';
import { Filter, FilterComponent } from 'apps/shared/components/filter/src/lib';
import { BreadcrumbService } from '@core/breadcrumb';
@Component({
selector: 'page-article-search-filter',
@@ -10,51 +15,104 @@ import { ArticleSearchService } from '../article-search.store';
styleUrls: ['search-filter.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ArticleSearchFilterComponent implements OnInit {
export class ArticleSearchFilterComponent implements OnInit, OnDestroy {
@Output()
close = new EventEmitter();
_processId$ = this._activatedRoute.parent.data.pipe(map((data) => Number(data.processId)));
fetching$: Observable<boolean>;
filter$: Observable<UiFilter>;
filter$: Observable<Filter>;
searchboxHint$ = this.articleSearch.searchboxHint$;
@ViewChild(UiFilterComponent, { static: false })
uiFilterComponent: UiFilterComponent;
@ViewChild(FilterComponent, { static: false })
uiFilterComponent: FilterComponent;
constructor(private articleSearch: ArticleSearchService) {}
get isTablet() {
return this._environment.matchTablet();
}
private _onDestroy$ = new Subject();
constructor(
private articleSearch: ArticleSearchService,
private _environment: EnvironmentService,
private _activatedRoute: ActivatedRoute,
public application: ApplicationService,
private _navigationService: ProductCatalogNavigationService,
private _breadcrumb: BreadcrumbService
) {}
ngOnInit() {
this.fetching$ = this.articleSearch.fetching$;
this.filter$ = this.articleSearch.filter$.pipe(
map((filter) => UiFilter.create(filter))
// tap((filter) =>
// filter.fromQueryParams({
// main_qs: 'harry potter',
// filter_format: 'eb;!hc',
// filter_dbhwgr: '110;121',
// main_author: 'author',
// filter_region: '9780*|9781*;97884*',
// 'filter_reading-age': '1-10',
// main_stock: '1-',
// })
// )
);
this.filter$ = this.articleSearch.filter$.pipe(map((filter) => Filter.create(filter)));
}
applyFilter(value: UiFilter) {
ngOnDestroy(): void {
this._onDestroy$.next();
this._onDestroy$.complete();
}
async closeFilter(): Promise<void> {
const processId = await this._processId$.pipe(first()).toPromise();
const itemId = this._navigationService.getOutletParams(this._activatedRoute)?.right?.id;
if (this.isTablet) {
return this.closeFilterTablet(processId);
}
if (!itemId) {
if (this._navigationService.getOutletLocations(this._activatedRoute)?.left === 'search') {
return await this._navigationService.navigateToProductSearch({ processId, queryParamsHandling: 'preserve' });
}
return await this._navigationService.navigateToResults({ processId, queryParamsHandling: 'preserve' });
} else {
return await this._navigationService.navigateToDetails({ processId, itemId, queryParamsHandling: 'preserve' });
}
}
async closeFilterTablet(processId: number): Promise<void> {
const latestBreadcrumb = await this._breadcrumb.getLastActivatedBreadcrumbByKey$(processId).pipe(first()).toPromise();
if (latestBreadcrumb?.tags?.find((tag) => tag === 'results')) {
return await this._navigationService.navigateToResults({ processId, queryParamsHandling: 'preserve' });
} else {
return await this._navigationService.navigateToProductSearch({ processId, queryParamsHandling: 'preserve' });
}
}
applyFilter(value: Filter) {
this.uiFilterComponent?.cancelAutocomplete();
this.articleSearch.setFilter(value);
this.articleSearch.search({ clear: true });
this.articleSearch.searchCompleted.pipe(take(1)).subscribe((s) => {
if (s.searchState === '') {
this.close.emit();
}
});
this.articleSearch.searchCompleted
.pipe(takeUntil(this._onDestroy$), withLatestFrom(this._processId$))
.subscribe(([state, processId]) => {
if (state.searchState === '') {
const params = state.filter.getQueryParams();
if (state.hits === 1 && this.isTablet) {
const item = state.items.find((f) => f);
this._navigationService.navigateToDetails({
processId,
itemId: item.id,
queryParams: this.isTablet ? undefined : params,
});
} else if (this.isTablet) {
this._navigationService.navigateToResults({
processId,
queryParams: params,
});
}
}
});
}
resetFilter(value: UiFilter) {
clearFilter(value: Filter) {
value.unselectAll();
}
resetFilter(value: Filter) {
const queryParams = { main_qs: value?.getQueryParams()?.main_qs || '' };
this.articleSearch.setDefaultFilter(queryParams);
}

View File

@@ -1,13 +1,13 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { UiFilterNextModule } from '@ui/filter';
import { RouterModule } from '@angular/router';
import { UiIconModule } from '@ui/icon';
import { UiSpinnerModule } from '@ui/spinner';
import { ArticleSearchFilterComponent } from './search-filter.component';
import { FilterNextModule } from 'apps/shared/components/filter/src/lib';
@NgModule({
imports: [CommonModule, UiFilterNextModule, UiIconModule, UiSpinnerModule],
imports: [CommonModule, RouterModule, FilterNextModule, UiIconModule, UiSpinnerModule],
exports: [ArticleSearchFilterComponent],
declarations: [ArticleSearchFilterComponent],
providers: [],

View File

@@ -1,26 +1,50 @@
<div class="card-search-article">
<h1 class="title">Artikelsuche</h1>
<p class="info">
<div class="bg-white rounded py-10 px-4 text-center shadow-[0_-2px_24px_0_#dce2e9] h-full">
<h1 class="text-2xl text-[1.625rem] font-bold mb-[0.375rem]">Artikelsuche</h1>
<p class="text-lg mb-10">
Welchen Artikel suchen Sie?
</p>
<ng-container *ngIf="filter$ | async; let filter">
<ui-filter-filter-group-main [inputGroup]="filter?.filter | group: 'main'"></ui-filter-filter-group-main>
<ui-filter-input-group-main
[hint]="searchboxHint$ | async"
[loading]="fetching$ | async"
[inputGroup]="filter?.input | group: 'main'"
(search)="search(filter)"
[showDescription]="false"
[scanner]="true"
></ui-filter-input-group-main>
<shared-filter-filter-group-main
class="mb-8 w-full"
*ngIf="!(isDesktop$ | async)"
[inputGroup]="filter?.filter | group: 'main'"
></shared-filter-filter-group-main>
<div class="flex flex-row px-12 justify-center desktop:px-0">
<shared-filter-input-group-main
class="block w-full mr-3 desktop:mx-auto"
[hint]="searchboxHint$ | async"
[loading]="fetching$ | async"
[inputGroup]="filter?.input | group: 'main'"
(search)="search(filter)"
[showDescription]="false"
[scanner]="true"
></shared-filter-input-group-main>
<a
*ngIf="!(isDesktop$ | async)"
class="page-search-main__filter w-[6.75rem] h-14 rounded-card font-bold px-5 mb-4 text-lg bg-[#AEB7C1] flex flex-row flex-nowrap items-center justify-center"
[class.active]="hasFilter$ | async"
[routerLink]="filterRoute"
queryParamsHandling="preserve"
>
<ui-svg-icon class="mr-2" icon="filter-variant"></ui-svg-icon>
Filter
</a>
</div>
<div class="recent-searches-wrapper">
<h3 class="recent-searches-header">Deine letzten Suchanfragen</h3>
<ul>
<li class="recent-searches-items" *ngFor="let recentQuery of history$ | async">
<button (click)="setQueryHistory(filter, recentQuery.friendlyName)">
<ui-icon icon="search" size="15px"></ui-icon>
<p>{{ recentQuery.friendlyName }}</p>
<div class="flex flex-col items-start ml-12 desktop:ml-8 py-6 bg-white">
<h3 class="text-sm font-bold mb-3">Deine letzten Suchanfragen</h3>
<ul class="flex flex-col justify-start overflow-hidden items-start m-0 p-0 bg-white w-full">
<li class="list-none pb-3" *ngFor="let recentQuery of history$ | async">
<button
class="flex flex-row items-center outline-none border-none bg-white text-black text-base m-0 p-0"
(click)="setQueryHistory(filter, recentQuery.friendlyName)"
>
<ui-icon
class="flex w-8 h-8 justify-center items-center mr-3 rounded-full text-black bg-[#edeff0]"
icon="search"
size="0.875rem"
></ui-icon>
<p class="m-0 p-0 whitespace-nowrap overflow-hidden overflow-ellipsis max-w-[25rem]">{{ recentQuery.friendlyName }}</p>
</button>
</li>
</ul>

View File

@@ -1,71 +1,13 @@
:host {
@apply flex flex-col box-border;
@apply flex flex-col box-border h-full;
}
.title {
@apply text-page-heading font-bold;
}
.info {
@apply text-2xl mt-1 mb-px-30;
}
.filter-chips {
@apply flex flex-row justify-center;
}
.card-search-article {
@apply bg-white rounded p-4 text-center;
box-shadow: 0 -2px 24px 0 #dce2e9;
}
.card-search-article {
min-height: calc(100vh - 380px);
}
ui-filter-filter-group-main {
@apply mb-8 w-full;
.page-search-main__filter {
&.active {
@apply bg-[#596470] text-white ml-px-5;
}
}
::ng-deep page-article-search-main ui-filter-filter-group-main .ui-filter-chip:not(.selected) {
@apply bg-glitter;
}
ui-filter-input-group-main {
@apply block mx-auto;
max-width: 600px;
}
.recent-searches-wrapper {
@apply flex flex-col mx-auto items-start py-6 bg-white;
width: 50%;
z-index: 0;
.recent-searches-header {
@apply text-sm font-bold mb-4;
}
ul {
@apply flex flex-col justify-start overflow-hidden items-start m-0 p-0 bg-white w-full;
z-index: 0;
.recent-searches-items {
@apply list-none pb-px-15;
button {
@apply flex flex-row items-center outline-none border-none bg-white text-black text-base m-0 p-0;
ui-icon {
@apply flex w-px-35 h-px-35 justify-center items-center mr-3 rounded-full text-black;
background-color: #e6eff9;
}
p {
@apply m-0 p-0 whitespace-nowrap overflow-hidden overflow-ellipsis;
max-width: 400px;
}
}
}
}
}

View File

@@ -3,11 +3,13 @@ import { ActivatedRoute } from '@angular/router';
import { BreadcrumbService } from '@core/breadcrumb';
import { ApplicationService } from '@core/application';
import { DomainCatalogService } from '@domain/catalog';
import { UiFilter, UiFilterInputGroupMainComponent } from '@ui/filter';
import { combineLatest, NEVER, Subscription } from 'rxjs';
import { catchError, debounceTime, first, switchMap } from 'rxjs/operators';
import { catchError, debounceTime, first, switchMap, map, shareReplay } from 'rxjs/operators';
import { ArticleSearchService } from '../article-search.store';
import { isEqual } from 'lodash';
import { EnvironmentService } from '@core/environment';
import { Filter, FilterInputGroupMainComponent } from 'apps/shared/components/filter/src/lib';
import { ProductCatalogNavigationService } from '@shared/services';
@Component({
selector: 'page-article-search-main',
@@ -26,15 +28,39 @@ export class ArticleSearchMainComponent implements OnInit, OnDestroy {
subscriptions = new Subscription();
@ViewChild(UiFilterInputGroupMainComponent, { static: false })
uiInputGroupMain: UiFilterInputGroupMainComponent;
hasFilter$ = combineLatest([this.searchService.filter$, this.searchService.defaultSettings$]).pipe(
map(([filter, defaultFilter]) => {
const filterQueryParams = filter?.getQueryParams();
return !isEqual(this.resetQueryParamsQueryAndOrderBy(filterQueryParams), Filter.create(defaultFilter).getQueryParams());
})
);
@ViewChild(FilterInputGroupMainComponent, { static: false })
sharedFilterInputGroupMain: FilterInputGroupMainComponent;
get isDesktop$() {
return this._environment.matchDesktop$.pipe(
map((state) => state?.matches),
shareReplay()
);
}
get filterRoute() {
const itemId = this._navigationService?.getOutletParams(this.route)?.right?.id;
return this._navigationService.getArticleSearchResultsAndFilterPath({
processId: this.application.activatedProcessId,
itemId,
});
}
constructor(
private searchService: ArticleSearchService,
private catalog: DomainCatalogService,
private route: ActivatedRoute,
private application: ApplicationService,
private breadcrumb: BreadcrumbService
private breadcrumb: BreadcrumbService,
private _environment: EnvironmentService,
private _navigationService: ProductCatalogNavigationService
) {}
ngOnInit() {
@@ -44,7 +70,7 @@ export class ArticleSearchMainComponent implements OnInit, OnDestroy {
.subscribe(async ([processId, queryParams]) => {
const processChanged = processId !== this.searchService.processId;
if (!(this.searchService.filter instanceof UiFilter)) {
if (!(this.searchService.filter instanceof Filter)) {
await this.searchService.setDefaultFilter();
}
@@ -57,6 +83,7 @@ export class ArticleSearchMainComponent implements OnInit, OnDestroy {
const cleanQueryParams = this.cleanupQueryParams(queryParams);
if (!isEqual(cleanQueryParams, this.cleanupQueryParams(this.searchService.filter.getQueryParams()))) {
// Reset Filter on Product Search Shell Navigation click
await this.searchService.setDefaultFilter(queryParams);
}
@@ -85,16 +112,30 @@ export class ArticleSearchMainComponent implements OnInit, OnDestroy {
this.updateBreadcrumb(this.searchService.processId, this.searchService.filter.getQueryParams());
}
search(filter: UiFilter) {
this.uiInputGroupMain.cancelAutocomplete();
search(filter: Filter) {
this.sharedFilterInputGroupMain.cancelAutocomplete();
this.searchService.setFilter(filter);
this.searchService.search({ clear: true });
}
setQueryHistory(filter: UiFilter, query: string) {
setQueryHistory(filter: Filter, query: string) {
filter.fromQueryParams({ main_qs: query });
}
resetQueryParamsQueryAndOrderBy(params: Record<string, string> = {}) {
const clean = { ...params };
for (const key in clean) {
if (key === 'main_qs' || key?.includes('order_by')) {
clean[key] = undefined;
} else if (key?.includes('order_by')) {
delete clean[key];
}
}
return clean;
}
cleanupQueryParams(params: Record<string, string> = {}) {
const clean = { ...params };

View File

@@ -1,11 +1,12 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { UiFilterNextModule } from '@ui/filter';
import { UiIconModule } from '@ui/icon';
import { ArticleSearchMainComponent } from './search-main.component';
import { FilterNextModule } from 'apps/shared/components/filter/src/lib';
import { RouterModule } from '@angular/router';
@NgModule({
imports: [CommonModule, UiIconModule, UiFilterNextModule],
imports: [CommonModule, RouterModule, UiIconModule, FilterNextModule],
exports: [ArticleSearchMainComponent],
declarations: [ArticleSearchMainComponent],
providers: [],

View File

@@ -1,17 +1,43 @@
<div class="thumbnail animation"></div>
<div class="col">
<div class="author animation"></div>
<div class="row">
<div class="title animation"></div>
<div class="price animation"></div>
<ng-container *ngIf="!(mainOutletActive$ | async); else mainOutlet">
<div class="bg-ucla-blue rounded-card w-[4.375rem] h-[5.625rem] animate-[load_1s_linear_infinite]"></div>
<div class="flex flex-col flex-grow">
<div class="h-4 bg-ucla-blue ml-4 mb-2 w-[7.8125rem] animate-[load_1s_linear_infinite]"></div>
<div class="flex flex-row justify-between flex-grow">
<div class="h-6 bg-ucla-blue ml-4 w-[12.5rem] animate-[load_1s_linear_infinite]"></div>
<div class="h-6 bg-ucla-blue ml-4 w-[4.6875rem] animate-[load_1s_linear_infinite]"></div>
</div>
<div class="flex-grow"></div>
<div class="flex flex-row justify-between flex-grow">
<div class="h-4 bg-ucla-blue ml-4 w-[7.8125rem] animate-[load_1s_linear_infinite]"></div>
<div class="h-4 bg-ucla-blue ml-4 w-[3.125rem] animate-[load_1s_linear_infinite]"></div>
</div>
<div class="flex flex-row justify-between flex-grow">
<div class="h-4 bg-ucla-blue ml-4 w-[7.8125rem] animate-[load_1s_linear_infinite]"></div>
<div class="h-4 bg-ucla-blue ml-4 w-[7.8125rem] animate-[load_1s_linear_infinite]"></div>
</div>
</div>
<div class="space"></div>
<div class="row">
<div class="category animation"></div>
<div class="stock animation"></div>
</ng-container>
<ng-template #mainOutlet>
<div class="bg-ucla-blue rounded-card w-[3rem] h-[4.125rem] animate-[load_1s_linear_infinite]"></div>
<div class="flex flex-col ml-4 w-[30%]">
<div class="h-4 bg-ucla-blue mb-2 w-[7.8125rem] animate-[load_1s_linear_infinite]"></div>
<div class="h-6 bg-ucla-blue mb-2 w-[12.5rem] animate-[load_1s_linear_infinite]"></div>
<div class="h-6 bg-ucla-blue w-[12.5rem] animate-[load_1s_linear_infinite]"></div>
</div>
<div class="row">
<div class="manufacturer animation"></div>
<div class="ava animation"></div>
<div class="flex flex-col ml-14 w-[30%]">
<div class="h-4 bg-ucla-blue mb-2 w-[7.8125rem] animate-[load_1s_linear_infinite]"></div>
<div class="h-4 bg-ucla-blue mb-2 w-[7.8125rem] animate-[load_1s_linear_infinite]"></div>
<div class="h-4 bg-ucla-blue w-[7.8125rem] animate-[load_1s_linear_infinite]"></div>
</div>
</div>
<div class="flex flex-col ml-10 w-[20%]">
<div class="h-4 bg-ucla-blue mb-2 w-[7.8125rem] animate-[load_1s_linear_infinite]"></div>
<div class="h-4 bg-ucla-blue mb-2 w-[7.8125rem] animate-[load_1s_linear_infinite]"></div>
<div class="h-4 bg-ucla-blue w-[7.8125rem] animate-[load_1s_linear_infinite]"></div>
</div>
<div class="flex flex-col ml-2 w-[20%] items-end">
<div class="h-4 bg-ucla-blue mb-2 w-[7.8125rem] animate-[load_1s_linear_infinite]"></div>
<div class="h-4 bg-ucla-blue mb-2 w-[7.8125rem] animate-[load_1s_linear_infinite]"></div>
<div class="h-4 bg-ucla-blue w-[7.8125rem] animate-[load_1s_linear_infinite]"></div>
</div>
</ng-template>

View File

@@ -1,61 +1,3 @@
:host {
@apply flex flex-row rounded-card bg-white mb-2 p-4;
height: 187px;
}
.thumbnail {
width: 70px;
height: 90px;
@apply bg-ucla-blue rounded-card;
}
.space {
@apply flex-grow;
}
.col {
@apply flex flex-col flex-grow;
}
.row {
@apply flex flex-row justify-between flex-grow;
}
.author {
width: 150px;
@apply h-4 bg-ucla-blue ml-4 mb-2;
}
.title {
width: 300px;
@apply h-6 bg-ucla-blue ml-4;
}
.price {
width: 100px;
@apply h-6 bg-ucla-blue ml-4;
}
.category {
width: 200px;
@apply h-4 bg-ucla-blue ml-4;
}
.manufacturer {
width: 200px;
@apply h-4 bg-ucla-blue ml-4;
}
.stock {
width: 75px;
@apply h-4 bg-ucla-blue ml-4;
}
.ava {
width: 150px;
@apply h-4 bg-ucla-blue ml-4;
}
.animation {
animation: load 1s linear infinite;
@apply flex flex-row rounded-card bg-white mb-2 p-4 w-full h-[212px] desktop-small:h-[181px];
}

View File

@@ -1,4 +1,7 @@
import { Component, ChangeDetectionStrategy } from '@angular/core';
import { Component, ChangeDetectionStrategy, HostBinding } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { ProductCatalogNavigationService } from '@shared/services';
import { shareReplay } from 'rxjs/operators';
@Component({
selector: 'page-search-result-item-loading',
@@ -7,5 +10,13 @@ import { Component, ChangeDetectionStrategy } from '@angular/core';
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class SearchResultItemLoadingComponent {
constructor() {}
get mainOutletActive$() {
return this._navigationService?.mainOutletActive$(this._activatedRoute).pipe(shareReplay());
}
constructor(private _navigationService: ProductCatalogNavigationService, private _activatedRoute: ActivatedRoute) {}
@HostBinding('style') get class() {
return this._navigationService.mainOutletActive(this._activatedRoute) ? { height: '6.125rem' } : '';
}
}

View File

@@ -1,81 +1,122 @@
<a class="product-list-result-content" [routerLink]="['/kunde', applicationService.activatedProcessId, 'product', 'details', item?.id]">
<div class="item-thumbnail">
<img loading="lazy" *ngIf="item?.imageId | thumbnailUrl; let thumbnailUrl" [src]="thumbnailUrl" [alt]="item?.product?.name" />
</div>
<div class="item-contributors">
<a
*ngFor="let contributor of contributors; let last = last"
[routerLink]="['/kunde', applicationService.activatedProcessId, 'product', 'search', 'results']"
[queryParams]="{ main_qs: contributor, main_author: 'author' }"
(click)="$event?.stopPropagation()"
>
{{ contributor }}{{ last ? '' : ';' }}
</a>
<a
class="page-search-result-item__item-card hover p-5 desktop-small:px-4 desktop-small:py-[0.625rem] h-[13.25rem] desktop-small:h-[11.3125rem] bg-white border border-solid border-transparent rounded-card"
[class.page-search-result-item__item-card-main]="mainOutletActive$ | async"
[routerLink]="detailsPath"
[routerLinkActive]="!isTablet && !(mainOutletActive$ | async) ? 'active' : ''"
[queryParamsHandling]="!isTablet ? 'preserve' : ''"
>
<div class="page-search-result-item__item-thumbnail text-center mr-4 w-[50px] h-[79px]">
<img
class="page-search-result-item__item-image w-[50px] h-[79px]"
loading="lazy"
*ngIf="item?.imageId | thumbnailUrl; let thumbnailUrl"
[src]="thumbnailUrl"
[alt]="item?.product?.name"
/>
</div>
<div
class="item-title"
[class.xl]="item?.product?.name?.length >= 35"
[class.lg]="item?.product?.name?.length >= 40"
[class.md]="item?.product?.name?.length >= 50"
[class.sm]="item?.product?.name?.length >= 60"
[class.xs]="item?.product?.name?.length >= 100"
class="page-search-result-item__item-grid-container"
[class.page-search-result-item__item-grid-container-main]="mainOutletActive$ | async"
>
{{ item?.product?.name }}
</div>
<div
class="page-search-result-item__item-contributors desktop-small:text-sm font-bold text-[#0556B4] text-ellipsis overflow-hidden max-w-[24rem] whitespace-nowrap"
>
<a
*ngFor="let contributor of contributors; let last = last"
[routerLink]="resultsPath"
[queryParams]="{ main_qs: contributor, main_author: 'author' }"
(click)="$event?.stopPropagation()"
>
{{ contributor }}{{ last ? '' : ';' }}
</a>
</div>
<div class="item-price">
{{ item?.catalogAvailability?.price?.value?.value | currency: 'EUR':'code' }}
</div>
<div
class="page-search-result-item__item-title font-bold text-2xl"
[class.text-xl]="item?.product?.name?.length >= 35 && isTablet"
[class.text-lg]="item?.product?.name?.length >= 40 && isTablet"
[class.text-md]="item?.product?.name?.length >= 50 && isTablet"
[class.text-sm]="item?.product?.name?.length >= 60 || !isTablet"
[class.text-xs]="item?.product?.name?.length >= 100 || (!isTablet && item?.product?.name?.length >= 70)"
>
{{ item?.product?.name }}
</div>
<div *ngIf="selectable" class="item-data-selector">
<ui-select-bullet [ngModel]="selected" (ngModelChange)="setSelected($event)"></ui-select-bullet>
</div>
<div class="item-stock z-dropdown" [uiOverlayTrigger]="tooltip" [overlayTriggerDisabled]="!(stockTooltipText$ | async)">
<ng-container *ngIf="isOrderBranch$ | async">
<div class="flex flex-row items-center justify-between">
<ui-icon icon="home" size="1em"></ui-icon>
<span
*ngIf="inStock$ | async; let stock"
[class.skeleton]="stock.inStock === undefined"
class="min-w-[1rem] text-right inline-block"
>{{ stock?.inStock }}</span
>
<span>x</span>
<div class="page-search-result-item__item-format desktop-small:text-sm">
<div *ngIf="item?.product?.format && item?.product?.formatDetail" class="font-bold flex flex-row">
<img
class="mr-3"
*ngIf="item?.product?.format !== '--'"
loading="lazy"
src="assets/images/Icon_{{ item?.product?.format }}.svg"
[alt]="item?.product?.formatDetail"
/>
{{ item?.product?.formatDetail | substr: 25 }}
</div>
</ng-container>
<ng-container *ngIf="!(isOrderBranch$ | async)">
<div class="flex flex-row items-center justify-between z-dropdown">
<ui-icon class="block" icon="home" size="1em"></ui-icon>
<span class="min-w-[1rem] text-center inline-block">-</span>
<span>x</span>
</div>
</ng-container>
</div>
<ui-tooltip #tooltip yPosition="above" xPosition="after" [yOffset]="-8" [closeable]="true">
{{ stockTooltipText$ | async }}
</ui-tooltip>
<!-- <div class="item-stock"><ui-icon icon="home" size="1em"></ui-icon> {{ item?.stockInfos | stockInfos }} x</div> -->
</div>
<div class="item-ssc" [class.xs]="item?.catalogAvailability?.sscText?.length >= 60">
{{ item?.catalogAvailability?.ssc }} - {{ item?.catalogAvailability?.sscText }}
</div>
<div class="page-search-result-item__item-manufacturer desktop-small:text-sm">
{{ item?.product?.manufacturer | substr: 18 }} | {{ item?.product?.ean }}
</div>
<div class="item-format" *ngIf="item?.product?.format && item?.product?.formatDetail">
<img
*ngIf="item?.product?.format !== '--'"
loading="lazy"
src="assets/images/Icon_{{ item?.product?.format }}.svg"
[alt]="item?.product?.formatDetail"
/>
{{ item?.product?.formatDetail }}
</div>
<div class="page-search-result-item__item-misc desktop-small:text-sm">
{{ item?.product?.volume }} <span *ngIf="item?.product?.volume && item?.product?.publicationDate">|</span>
{{ publicationDate }}
</div>
<div class="item-misc">
{{ item?.product?.manufacturer | substr: 18 }} | {{ item?.product?.ean }} <br />
{{ item?.product?.volume }} <span *ngIf="item?.product?.volume && item?.product?.publicationDate">|</span>
{{ publicationDate }}
<div class="page-search-result-item__item-price desktop-small:text-sm font-bold justify-self-end">
{{ item?.catalogAvailability?.price?.value?.value | currency: 'EUR':'code' }}
</div>
<div class="page-search-result-item__item-select-bullet justify-self-end">
<input
*ngIf="selectable"
(click)="$event.stopPropagation()"
[ngModel]="selected$ | async"
(ngModelChange)="setSelected()"
class="isa-select-bullet"
type="checkbox"
/>
</div>
<div
class="page-search-result-item__item-stock desktop-small:text-sm font-bold z-dropdown justify-self-start"
[class.justify-self-end]="!(mainOutletActive$ | async)"
[uiOverlayTrigger]="tooltip"
[overlayTriggerDisabled]="!(stockTooltipText$ | async)"
>
<ng-container *ngIf="isOrderBranch$ | async">
<div class="flex flex-row items-center justify-between">
<ui-icon icon="home" size="1em"></ui-icon>
<span
*ngIf="inStock$ | async; let stock"
[class.skeleton]="stock.inStock === undefined"
class="min-w-[1rem] text-right inline-block"
>{{ stock?.inStock }}</span
>
<span>x</span>
</div>
</ng-container>
<ng-container *ngIf="!(isOrderBranch$ | async)">
<div class="flex flex-row items-center justify-between z-dropdown">
<ui-icon class="block" icon="home" size="1em"></ui-icon>
<span class="min-w-[1rem] text-center inline-block">-</span>
<span>x</span>
</div>
</ng-container>
</div>
<ui-tooltip #tooltip yPosition="above" xPosition="after" [yOffset]="-8" [closeable]="true">
{{ stockTooltipText$ | async }}
</ui-tooltip>
<div
class="page-search-result-item__item-ssc desktop-small:text-sm justify-self-start"
[class.justify-self-end]="!(mainOutletActive$ | async)"
[class.xs]="item?.catalogAvailability?.sscText?.length >= 60"
>
<strong>{{ item?.catalogAvailability?.ssc }}</strong> -
{{ !isTablet ? item?.catalogAvailability?.sscText : (item?.catalogAvailability?.sscText | substr: 18) }}
</div>
</div>
</a>

View File

@@ -1,113 +1,90 @@
.product-list-result-content {
@apply text-black no-underline grid;
grid-template-columns: 102px 50% auto;
grid-template-rows: auto;
:host {
@apply flex flex-col w-full h-[13.25rem] desktop-small:h-[11.3125rem];
}
.page-search-result-item__item-card {
@apply grid grid-flow-col;
grid-template-columns: 3.9375rem auto;
box-shadow: 0px 0px 10px rgba(220, 226, 233, 0.5);
}
.page-search-result-item__item-grid-container {
@apply grid grid-flow-row gap-[0.375rem];
grid-template-areas:
'item-thumbnail item-contributors item-contributors'
'item-thumbnail item-title item-price'
'item-thumbnail item-title item-data-selector'
'item-thumbnail item-format item-stock'
'item-thumbnail item-misc item-ssc';
'contributors contributors contributors'
'title title price'
'title title price'
'title title select'
'format format select'
'manufacturer manufacturer stock'
'misc ssc ssc';
}
.item-thumbnail {
grid-area: item-thumbnail;
width: 70px;
@apply mr-8;
img {
max-width: 100%;
max-height: 150px;
@apply rounded-card shadow-cta;
}
}
.item-contributors {
grid-area: item-contributors;
height: 22px;
text-overflow: ellipsis;
overflow: hidden;
max-width: 600px;
white-space: nowrap;
a {
@apply text-active-customer font-bold no-underline;
}
}
.item-title {
grid-area: item-title;
@apply font-bold text-2xl;
height: 64px;
max-height: 64px;
}
.item-title.xl {
@apply font-bold text-xl;
}
.item-title.lg {
@apply font-bold text-lg;
}
.item-title.md {
@apply font-bold text-base;
}
.item-title.sm {
@apply font-bold text-sm;
}
.item-title.xs {
@apply font-bold text-xs;
}
.item-price {
grid-area: item-price;
@apply font-bold text-xl text-right;
}
.item-format {
grid-area: item-format;
@apply flex flex-row items-center font-bold text-lg whitespace-nowrap;
img {
@apply mr-2;
}
}
.item-stock {
grid-area: item-stock;
@apply flex flex-row justify-end items-baseline font-bold text-lg;
ui-icon {
@apply text-active-customer mr-2;
}
}
.item-misc {
grid-area: item-misc;
}
.item-ssc {
grid-area: item-ssc;
@apply font-bold text-right;
}
.item-ssc.xs {
@apply font-bold text-xs;
}
.item-data-selector {
@apply w-full flex justify-end;
grid-area: item-data-selector;
}
ui-select-bullet {
@apply p-4 -m-4 z-dropdown;
}
@media (min-width: 1025px) {
.item-contributors {
max-width: 780px;
.page-search-result-item__item-grid-container-main {
@apply gap-x-4;
grid-template-rows: 1.3125rem 1.3125rem auto;
grid-template-columns: 30% 30% 20% 8.8% auto;
grid-template-areas:
'contributors format stock price .'
'title manufacturer ssc . select'
'title misc . . .';
}
.page-search-result-item__item-contributors {
grid-area: contributors;
}
.page-search-result-item__item-price {
grid-area: price;
}
.page-search-result-item__item-title {
grid-area: title;
}
.page-search-result-item__item-format {
grid-area: format;
}
.page-search-result-item__item-manufacturer {
grid-area: manufacturer;
}
.page-search-result-item__item-misc {
grid-area: misc;
}
.page-search-result-item__item-select-bullet {
grid-area: select;
}
.page-search-result-item__item-stock {
grid-area: stock;
}
.page-search-result-item__item-ssc {
grid-area: ssc;
}
.page-search-result-item__item-image {
box-shadow: 0px 6px 18px rgba(0, 0, 0, 0.197935);
}
.active,
.hover:hover {
@apply bg-[#D8DFE5] border border-solid border-[#0556B4];
.page-search-result-item__item-select-bullet {
.isa-select-bullet::before {
@apply bg-[#fff];
}
.isa-select-bullet:checked:before {
@apply bg-[#596470];
}
.isa-select-bullet:hover::before {
@apply bg-[#778490];
}
}
}

View File

@@ -1,14 +1,17 @@
import { DatePipe } from '@angular/common';
import { Component, ChangeDetectionStrategy, Input, EventEmitter, Output } from '@angular/core';
import { Component, ChangeDetectionStrategy, Input, EventEmitter, Output, HostListener, HostBinding } from '@angular/core';
import { ApplicationService } from '@core/application';
import { EnvironmentService } from '@core/environment';
import { DomainAvailabilityService, DomainInStockService } from '@domain/availability';
import { ComponentStore } from '@ngrx/component-store';
import { ItemDTO } from '@swagger/cat';
import { DateAdapter } from '@ui/common';
import { isEqual } from 'lodash';
import { combineLatest } from 'rxjs';
import { debounceTime, switchMap, map, shareReplay, filter } from 'rxjs/operators';
import { debounceTime, switchMap, map, shareReplay, filter, first } from 'rxjs/operators';
import { ArticleSearchService } from '../article-search.store';
import { ProductCatalogNavigationService } from '@shared/services';
import { ActivatedRoute } from '@angular/router';
export interface SearchResultItemComponentState {
item?: ItemDTO;
@@ -36,16 +39,7 @@ export class SearchResultItemComponent extends ComponentStore<SearchResultItemCo
readonly item$ = this.select((s) => s.item);
@Input()
get selected() {
return this.get((s) => s.selected);
}
set selected(selected: boolean) {
if (this.selected !== selected) {
this.patchState({ selected });
}
}
readonly selected$ = this.select((s) => s.selected);
selected$ = this._articleSearchService.selectedItemIds$.pipe(map((selectedItemIds) => selectedItemIds.includes(this.item?.id)));
@Input()
get selectable() {
@@ -58,7 +52,7 @@ export class SearchResultItemComponent extends ComponentStore<SearchResultItemCo
}
@Output()
selectedChange = new EventEmitter<boolean>();
selectedChange = new EventEmitter<ItemDTO>();
get contributors() {
return this.item?.product?.contributors?.split(';').map((val) => val.trim());
@@ -77,6 +71,22 @@ export class SearchResultItemComponent extends ComponentStore<SearchResultItemCo
return '';
}
get isTablet() {
return this._environment.matchTablet();
}
get detailsPath() {
return this._navigationService.getArticleDetailsPath({ processId: this.applicationService.activatedProcessId, itemId: this.item?.id });
}
get resultsPath() {
return this._navigationService.getArticleSearchResultsPath(this.applicationService.activatedProcessId);
}
get mainOutletActive$() {
return this._navigationService?.mainOutletActive$(this._activatedRoute).pipe(shareReplay());
}
defaultBranch$ = this._availability.getDefaultBranch();
selectedBranchId$ = this.applicationService.activatedProcessId$.pipe(
@@ -123,7 +133,10 @@ export class SearchResultItemComponent extends ComponentStore<SearchResultItemCo
private _articleSearchService: ArticleSearchService,
public applicationService: ApplicationService,
private _stockService: DomainInStockService,
private _availability: DomainAvailabilityService
private _availability: DomainAvailabilityService,
private _environment: EnvironmentService,
private _navigationService: ProductCatalogNavigationService,
private _activatedRoute: ActivatedRoute
) {
super({
selected: false,
@@ -131,7 +144,16 @@ export class SearchResultItemComponent extends ComponentStore<SearchResultItemCo
});
}
setSelected(selected: boolean) {
this._articleSearchService.setSelected({ selected, itemId: this.item?.id });
setSelected() {
const isSelected = this._articleSearchService.selectedItemIds.includes(this.item?.id);
this._articleSearchService.setSelected({ selected: !isSelected, itemId: this.item?.id });
if (!this.isTablet) {
this.selectedChange.emit(this.item);
}
}
@HostBinding('style') get class() {
return this._navigationService.mainOutletActive(this._activatedRoute) ? { height: '6.125rem' } : '';
}
}

View File

@@ -1,32 +1,77 @@
<div class="filter-wrapper">
<div class="hits" *ngIf="hits$ | async; let hits">{{ hits }} Titel</div>
<ui-order-by-filter [orderBy]="(filter$ | async)?.orderBy" (selectedOrderByChange)="search(); updateBreadcrumbs()"> </ui-order-by-filter>
<div
class="page-search-results__header bg-background-liste flex items-end justify-between"
[class.pb-4]="!(mainOutletActive$ | async)"
[class.flex-col]="!(mainOutletActive$ | async)"
>
<div class="flex flex-row w-full desktop-small:w-min" [class.desktop:w-full]="!(mainOutletActive$ | async)">
<shared-filter-input-group-main
*ngIf="filter$ | async; let filter"
class="block mr-3 w-full desktop-small:w-[23.5rem]"
[class.desktop:w-full]="!(mainOutletActive$ | async)"
[hint]="searchboxHint$ | async"
[loading]="fetching$ | async"
[inputGroup]="filter?.input | group: 'main'"
(search)="search(filter)"
[showDescription]="false"
[scanner]="true"
></shared-filter-input-group-main>
<a
class="page-search-results__filter w-[6.75rem] h-14 rounded-card font-bold px-5 mb-4 text-lg bg-[#AEB7C1] flex flex-row flex-nowrap items-center justify-center"
[class.active]="hasFilter$ | async"
[routerLink]="filterRoute"
queryParamsHandling="preserve"
>
<ui-svg-icon class="mr-2" icon="filter-variant"></ui-svg-icon>
Filter
</a>
</div>
<div
*ngIf="hits$ | async; let hits"
class="page-search-results__items-count inline-flex flex-row items-center pr-5 text-sm"
[class.mb-4]="mainOutletActive$ | async"
>
{{ hits ??
0 }}
Titel
</div>
</div>
<div class="page-search-results__order-by" [class.page-search-results__order-by-main]="mainOutletActive$ | async">
<shared-order-by-filter
[groupBy]="(mainOutletActive$ | async) ? [2, 2, 1, 1] : []"
[orderBy]="(filter$ | async)?.orderBy"
(selectedOrderByChange)="search(); updateBreadcrumbs()"
>
</shared-order-by-filter>
</div>
<cdk-virtual-scroll-viewport
#scrollContainer
class="product-list scroll-bar scroll-bar-margin"
[itemSize]="187"
minBufferPx="1200"
maxBufferPx="1200"
class="product-list"
[itemSize]="(mainOutletActive$ | async) ? 187 : 98"
minBufferPx="2800"
maxBufferPx="2800"
(scrolledIndexChange)="scrolledIndexChange($event)"
>
<div class="product-list-result" *cdkVirtualFor="let item of results$ | async; trackBy: trackByItemId">
<search-result-item
[selected]="item | searchResultSelected: searchService.selectedItemIds"
[selectable]="isSelectable(item)"
[item]="item"
></search-result-item>
</div>
<search-result-item
class="page-search-results__result-item"
[class.page-search-results__result-item-main]="mainOutletActive$ | async"
*cdkVirtualFor="let item of results$ | async; trackBy: trackByItemId"
(selectedChange)="addToCart($event)"
[selectable]="isSelectable(item)"
[item]="item"
></search-result-item>
<page-search-result-item-loading *ngIf="fetching$ | async"></page-search-result-item-loading>
</cdk-virtual-scroll-viewport>
<div class="actions">
<div *ngIf="isTablet" class="actions z-fixed">
<button
[disabled]="loading$ | async"
*ngIf="(selectedItemIds$ | async)?.length > 0"
class="cta-cart cta-action-primary"
(click)="addSelectedItemsToCart()"
(click)="addToCart()"
>
<ui-spinner [show]="loading$ | async">In den Warenkorb legen</ui-spinner>
</button>

View File

@@ -1,34 +1,36 @@
:host {
@apply box-border grid;
max-height: calc(100vh - 364px);
height: 100vh;
grid-template-rows: auto 1fr;
@apply box-border grid h-[calc(100vh-16.5rem)] desktop-small:h-[calc(100vh-15.1rem)];
grid-template-rows: auto auto 1fr;
}
.product-list {
@apply m-0 p-0 mt-2;
@apply m-0 p-0 mt-px-2;
}
.product-list-result {
@apply list-none bg-white rounded-card p-4 mb-2;
height: 187px;
max-height: 187px;
.page-search-results__result-item {
@apply mb-px-10;
}
.filter-wrapper {
@apply block relative;
.page-search-results__result-item-main {
@apply mb-[5px];
}
.hits {
@apply text-inactive-branch font-semibold absolute top-2 right-0;
}
.page-search-results__order-by {
@apply bg-white rounded-t-card px-6 desktop-small:px-8;
}
ui-order-by-filter {
@apply mx-auto;
.page-search-results__order-by-main {
@apply pl-[4.9375rem] px-4;
}
.page-search-results__filter {
&.active {
@apply bg-[#596470] text-white ml-px-5;
}
}
.actions {
@apply fixed bottom-28 inline-grid grid-flow-col gap-7;
@apply fixed bottom-16 inline-grid grid-flow-col gap-7;
left: 50%;
transform: translateX(-50%);
@@ -44,3 +46,24 @@
@apply bg-brand text-white;
}
}
::ng-deep page-search-results .page-search-results__order-by-main shared-order-by-filter {
@apply grid grid-flow-col justify-items-start gap-x-4 justify-start;
grid-template-columns: 30% 30% 20% 8.8% auto;
.group {
@apply desktop-small:justify-start;
}
.group:last-child {
@apply justify-self-end;
.order-by-filter-button {
@apply ml-12 mr-0;
}
}
.order-by-filter-button {
@apply ml-0 mr-12;
}
}

View File

@@ -3,18 +3,20 @@ import { Component, ChangeDetectionStrategy, OnInit, OnDestroy, ViewChild, ViewC
import { ActivatedRoute } from '@angular/router';
import { ApplicationService } from '@core/application';
import { BreadcrumbService } from '@core/breadcrumb';
import { EnvironmentService } from '@core/environment';
import { DomainCheckoutService } from '@domain/checkout';
import { ItemDTO } from '@swagger/cat';
import { AddToShoppingCartDTO } from '@swagger/checkout';
import { UiFilter } from '@ui/filter';
import { UiErrorModalComponent, UiModalService } from '@ui/modal';
import { CacheService } from 'apps/core/cache/src/public-api';
import { isEqual } from 'lodash';
import { BehaviorSubject, combineLatest, Subscription } from 'rxjs';
import { debounceTime, first, map, switchMap } from 'rxjs/operators';
import { debounceTime, first, map, shareReplay, 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';
import { Filter, FilterInputGroupMainComponent } from 'apps/shared/components/filter/src/lib';
@Component({
selector: 'page-search-results',
@@ -27,6 +29,9 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy {
@ViewChild('scrollContainer', { static: true })
scrollContainer: CdkVirtualScrollViewport;
@ViewChild(FilterInputGroupMainComponent, { static: false })
sharedFilterInputGroupMain: FilterInputGroupMainComponent;
results$ = this.searchService.items$;
fetching$ = this.searchService.fetching$;
hits$ = this.searchService.hits$;
@@ -35,6 +40,8 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy {
selectedItemIds$ = this.searchService.selectedItemIds$;
searchboxHint$ = this.searchService.searchboxHint$;
selectedItems$ = combineLatest([this.results$, this.selectedItemIds$]).pipe(
map(([items, selectedItemIds]) => {
return items?.filter((item) => selectedItemIds?.find((selectedItemId) => item.id === selectedItemId));
@@ -47,14 +54,39 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy {
trackByItemId: TrackByFunction<ItemDTO> = (index, item) => item.id;
get isTablet() {
return this._environment.matchTablet();
}
hasFilter$ = combineLatest([this.searchService.filter$, this.searchService.defaultSettings$]).pipe(
map(([filter, defaultFilter]) => {
const filterQueryParams = filter?.getQueryParams();
return !isEqual(this.resetQueryParamsQueryAndOrderBy(filterQueryParams), Filter.create(defaultFilter).getQueryParams());
})
);
get filterRoute() {
const itemId = this._navigationService?.getOutletParams(this.route)?.right?.id;
return this._navigationService.getArticleSearchResultsAndFilterPath({
processId: this.application.activatedProcessId,
itemId,
});
}
get mainOutletActive$() {
return this._navigationService?.mainOutletActive$(this.route).pipe(shareReplay());
}
constructor(
public searchService: ArticleSearchService,
private route: ActivatedRoute,
private application: ApplicationService,
public application: ApplicationService,
private breadcrumb: BreadcrumbService,
private cache: CacheService,
private _uiModal: UiModalService,
private _checkoutService: DomainCheckoutService
private _checkoutService: DomainCheckoutService,
private _environment: EnvironmentService,
private _navigationService: ProductCatalogNavigationService
) {}
ngOnInit() {
@@ -72,7 +104,7 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy {
const branchChanged = selectedBranch?.id !== this.searchService?.selectedBranch?.id;
if (processChanged) {
if (!!this.searchService.processId && this.searchService.filter instanceof UiFilter) {
if (!!this.searchService.processId && this.searchService.filter instanceof Filter) {
this.cacheCurrentData(
this.searchService.processId,
this.searchService.filter.getQueryParams(),
@@ -87,7 +119,7 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy {
this.searchService.setBranch(selectedBranch);
}
if (!(this.searchService.filter instanceof UiFilter)) {
if (!(this.searchService.filter instanceof Filter)) {
await this.searchService.setDefaultFilter();
}
@@ -117,6 +149,27 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy {
await this.removeDetailsBreadcrumb(processId);
})
);
this.subscriptions.add(
this.searchService.searchCompleted.pipe(withLatestFrom(this.application.activatedProcessId$)).subscribe(([state, processId]) => {
if (state.searchState === '') {
const params = state.filter.getQueryParams();
if ((state.hits === 1 && this.isTablet) || (!this.isTablet && !this._navigationService.mainOutletActive(this.route))) {
const item = state.items.find((f) => f);
this._navigationService.navigateToDetails({
processId,
itemId: item.id,
queryParams: this.isTablet ? undefined : params,
});
} else if (this.isTablet || this._navigationService.mainOutletActive(this.route)) {
this._navigationService.navigateToResults({
processId,
queryParams: params,
});
}
}
})
);
}
ngOnDestroy() {
@@ -127,7 +180,26 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy {
this.unselectAll();
}
search() {
resetQueryParamsQueryAndOrderBy(params: Record<string, string> = {}) {
const clean = { ...params };
for (const key in clean) {
if (key === 'main_qs') {
clean[key] = undefined;
} else if (key?.includes('order_by')) {
delete clean[key];
}
}
return clean;
}
search(filter?: Filter) {
if (!!filter) {
this.sharedFilterInputGroupMain.cancelAutocomplete();
this.searchService.setFilter(filter);
}
this.searchService.search({ clear: true });
}
@@ -187,7 +259,7 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy {
await this.breadcrumb.addBreadcrumbIfNotExists({
key: processId,
name,
path: `/kunde/${this.application.activatedProcessId}/product/search/results`,
path: this._navigationService.getArticleSearchResultsPath(this.application.activatedProcessId),
params: queryParams,
section: 'customer',
tags: ['catalog', 'filter', 'results'],
@@ -250,29 +322,44 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy {
this.searchService.patchState({ selectedItemIds: [] });
}
async addSelectedItemsToCart() {
async addToCart(item?: ItemDTO) {
this.loading$.next(true);
const selectedItems = await this.selectedItems$.pipe(first()).toPromise();
if (!!item) {
await this.addItemsToCart(item);
} else {
await this.addItemsToCart();
}
this.loading$.next(false);
}
private _createShoppingCartItem(item: ItemDTO): AddToShoppingCartDTO {
return {
quantity: 1,
availability: {
availabilityType: item?.catalogAvailability?.status,
price: item?.catalogAvailability?.price,
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 },
};
}
async addItemsToCart(item?: ItemDTO) {
const selectedItems = !item ? await this.selectedItems$.pipe(first()).toPromise() : [item];
const items: AddToShoppingCartDTO[] = [];
const canAddItemsPayload = [];
for (const item of selectedItems) {
const shoppingCartItem = this._createShoppingCartItem(item);
const isDownload = item?.product?.format === 'EB' || item?.product?.format === 'DL';
const shoppingCartItem: AddToShoppingCartDTO = {
quantity: 1,
availability: {
availabilityType: item?.catalogAvailability?.status,
price: item?.catalogAvailability?.price,
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 },
};
if (isDownload) {
shoppingCartItem.destination = { data: { target: 16 } };
canAddItemsPayload.push({

View File

@@ -8,7 +8,6 @@ import { UiCommonModule } from '@ui/common';
import { UiIconModule } from '@ui/icon';
import { UiSelectBulletModule } from '@ui/select-bullet';
import { UiTooltipModule } from '@ui/tooltip';
import { UiOrderByFilterModule } from 'apps/ui/filter/src/lib/next/order-by-filter/order-by-filter.module';
import { UiSpinnerModule } from 'apps/ui/spinner/src/lib/ui-spinner.module';
import { AddedToCartModalComponent } from './added-to-cart-modal/added-to-cart-modal.component';
import { StockInfosPipe } from './order-by-filter/stick-infos.pipe';
@@ -16,6 +15,9 @@ import { SearchResultItemLoadingComponent } from './search-result-item-loading.c
import { SearchResultItemComponent } from './search-result-item.component';
import { ArticleSearchResultsComponent } from './search-results.component';
import { SearchResultSelectedPipe } from './selected/search-result-selected.pipe';
import { FilterAutocompleteProvider, FilterNextModule, OrderByFilterModule } from 'apps/shared/components/filter/src/lib';
import { FocusSearchboxEvent } from '../focus-searchbox.event';
import { ArticleSearchMainAutocompleteProvider } from '../providers';
@NgModule({
imports: [
@@ -27,9 +29,10 @@ import { SearchResultSelectedPipe } from './selected/search-result-selected.pipe
UiIconModule,
UiSelectBulletModule,
UiSpinnerModule,
UiOrderByFilterModule,
OrderByFilterModule,
ScrollingModule,
UiTooltipModule,
FilterNextModule,
],
exports: [ArticleSearchResultsComponent, SearchResultItemComponent],
declarations: [
@@ -40,6 +43,13 @@ import { SearchResultSelectedPipe } from './selected/search-result-selected.pipe
SearchResultSelectedPipe,
AddedToCartModalComponent,
],
providers: [],
providers: [
FocusSearchboxEvent,
{
provide: FilterAutocompleteProvider,
useClass: ArticleSearchMainAutocompleteProvider,
multi: true,
},
],
})
export class SearchResultsModule {}

View File

@@ -2,10 +2,65 @@ import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { ArticleDetailsComponent } from './article-details/article-details.component';
import { ArticleSearchComponent } from './article-search/article-search.component';
import { ArticleSearchFilterComponent } from './article-search/search-filter/search-filter.component';
import { ArticleSearchMainComponent } from './article-search/search-main/search-main.component';
import { ArticleSearchResultsComponent } from './article-search/search-results/search-results.component';
import { PageCatalogComponent } from './page-catalog.component';
const auxiliaryRoutes = [
{
path: 'search',
component: ArticleSearchComponent,
outlet: 'left',
children: [
{
path: '',
component: ArticleSearchMainComponent,
},
],
},
{
path: 'filter',
component: ArticleSearchFilterComponent,
outlet: 'right',
},
{
path: 'filter/:id',
component: ArticleSearchFilterComponent,
outlet: 'right',
},
{
path: 'results',
component: ArticleSearchResultsComponent,
outlet: 'left',
},
{
path: 'results',
component: ArticleSearchResultsComponent,
outlet: 'main',
},
{
path: 'results/:id',
component: ArticleSearchResultsComponent,
outlet: 'left',
},
{
path: 'results/ean/:ean',
component: ArticleSearchResultsComponent,
outlet: 'left',
},
{
path: 'details/ean/:ean',
component: ArticleDetailsComponent,
outlet: 'right',
},
{
path: 'details/:id',
component: ArticleDetailsComponent,
outlet: 'right',
},
];
const routes: Routes = [
{
path: '',
@@ -19,12 +74,16 @@ const routes: Routes = [
path: '',
component: ArticleSearchMainComponent,
},
{
path: 'results',
component: ArticleSearchResultsComponent,
},
],
},
{
path: 'results',
component: ArticleSearchResultsComponent,
},
{
path: 'filter',
component: ArticleSearchFilterComponent,
},
{
path: 'details/ean/:ean',
component: ArticleDetailsComponent,
@@ -33,6 +92,7 @@ const routes: Routes = [
path: 'details/:id',
component: ArticleDetailsComponent,
},
...auxiliaryRoutes,
{
path: '',
pathMatch: 'full',

View File

@@ -8,4 +8,25 @@
>
</shared-branch-selector>
</shared-breadcrumb>
<router-outlet></router-outlet>
<ng-container *ngIf="routerEvents$ | async">
<ng-container *ngIf="!(isDesktop$ | async); else desktop">
<router-outlet></router-outlet>
</ng-container>
<ng-template #desktop>
<ng-container *ngIf="showMainOutlet$ | async">
<router-outlet name="main"></router-outlet>
</ng-container>
<div class="grid grid-cols-[minmax(31rem,.5fr)_1fr] gap-6">
<div *ngIf="showLeftOutlet$ | async" class="block">
<router-outlet name="left"></router-outlet>
</div>
<div *ngIf="showRightOutlet$ | async">
<router-outlet name="right"></router-outlet>
</div>
</div>
</ng-template>
</ng-container>

View File

@@ -1,5 +1,5 @@
:host {
@apply block relative;
@apply block relative h-[calc(100vh-16.5rem)] desktop-small:h-[calc(100vh-15.1rem)];
}
shell-breadcrumb {

View File

@@ -1,4 +1,5 @@
import { AfterViewInit, ChangeDetectionStrategy, Component, ElementRef, OnDestroy, OnInit, Renderer2, ViewChild } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { ApplicationService } from '@core/application';
import { AuthService } from '@core/auth';
import { EnvironmentService } from '@core/environment';
@@ -6,8 +7,8 @@ import { BranchSelectorComponent } from '@shared/components/branch-selector';
import { BreadcrumbComponent } from '@shared/components/breadcrumb';
import { BranchDTO } from '@swagger/checkout';
import { UiErrorModalComponent, UiModalService } from '@ui/modal';
import { BehaviorSubject, from, fromEvent, Observable, Subject } from 'rxjs';
import { first, map, switchMap, takeUntil } from 'rxjs/operators';
import { fromEvent, Observable, Subject } from 'rxjs';
import { first, map, shareReplay, switchMap, takeUntil, withLatestFrom } from 'rxjs/operators';
@Component({
selector: 'page-catalog',
@@ -28,35 +29,62 @@ export class PageCatalogComponent implements OnInit, AfterViewInit, OnDestroy {
_onDestroy$ = new Subject<boolean>();
get isTablet$() {
return this._environmentService.matchTablet$.pipe(
map((state) => state.matches),
shareReplay()
);
}
get isDesktop$() {
return this._environmentService.matchDesktop$.pipe(
map((state) => {
return state.matches;
}),
shareReplay()
);
}
routerEvents$ = this._router.events.pipe(shareReplay());
showMainOutlet$ = this.routerEvents$.pipe(map((_) => !!this._activatedRoute?.children?.find((child) => child?.outlet === 'main')));
showLeftOutlet$ = this.routerEvents$.pipe(map((_) => !!this._activatedRoute?.children?.find((child) => child?.outlet === 'left')));
showRightOutlet$ = this.routerEvents$.pipe(map((_) => !!this._activatedRoute?.children?.find((child) => child?.outlet === 'right')));
constructor(
public application: ApplicationService,
private _uiModal: UiModalService,
public auth: AuthService,
private _environmentService: EnvironmentService,
private _renderer: Renderer2
private _renderer: Renderer2,
private _activatedRoute: ActivatedRoute,
private _router: Router
) {}
ngOnInit() {
// this.auth.getClaims();
this.activatedProcessId$ = this.application.activatedProcessId$.pipe(map((processId) => String(processId)));
this.selectedBranch$ = this.activatedProcessId$.pipe(switchMap((processId) => this.application.getSelectedBranch$(Number(processId))));
this.application.setTitle('Artikelsuche');
}
ngAfterViewInit(): void {
if (this._environmentService.isTablet()) {
fromEvent(this.branchSelectorRef.nativeElement, 'focusin')
.pipe(takeUntil(this._onDestroy$))
.subscribe((_) => {
fromEvent(this.branchSelectorRef.nativeElement, 'focusin')
.pipe(takeUntil(this._onDestroy$), withLatestFrom(this.isTablet$))
.subscribe(([_, isTablet]) => {
if (isTablet) {
this._renderer.setStyle(this.branchSelectorRef?.nativeElement, 'width', this.branchSelectorWidth);
});
}
});
fromEvent(this.branchSelectorRef.nativeElement, 'focusout')
.pipe(takeUntil(this._onDestroy$))
.subscribe((_) => {
this._renderer.removeStyle(this.branchSelectorRef?.nativeElement, 'width');
});
}
fromEvent(this.branchSelectorRef.nativeElement, 'focusout')
.pipe(takeUntil(this._onDestroy$))
.subscribe((_) => {
this._renderer.removeStyle(this.branchSelectorRef?.nativeElement, 'width');
});
}
ngOnDestroy(): void {

View File

@@ -2,22 +2,13 @@ import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { BranchSelectorComponent } from '@shared/components/branch-selector';
import { BreadcrumbModule } from '@shared/components/breadcrumb';
import { ShellBreadcrumbModule } from '@shell/breadcrumb';
import { ArticleDetailsModule } from './article-details/article-details.module';
import { ArticleSearchModule } from './article-search/article-search.module';
import { PageCatalogRoutingModule } from './page-catalog-routing.module';
import { PageCatalogComponent } from './page-catalog.component';
@NgModule({
imports: [
CommonModule,
PageCatalogRoutingModule,
ShellBreadcrumbModule,
ArticleSearchModule,
ArticleDetailsModule,
BreadcrumbModule,
BranchSelectorComponent,
],
imports: [CommonModule, PageCatalogRoutingModule, ArticleSearchModule, ArticleDetailsModule, BreadcrumbModule, BranchSelectorComponent],
exports: [],
declarations: [PageCatalogComponent],
})

View File

@@ -1 +1 @@
<shell-breadcrumb [key]="breadcrumbKey$ | async" [includesTags]="['checkout']"></shell-breadcrumb> <router-outlet></router-outlet>
<shared-breadcrumb [key]="breadcrumbKey$ | async" [tags]="['checkout']"></shared-breadcrumb> <router-outlet></router-outlet>

View File

@@ -1,4 +1,4 @@
import { Component, ChangeDetectionStrategy } from '@angular/core';
import { Component, ChangeDetectionStrategy, OnInit } from '@angular/core';
import { ApplicationService } from '@core/application';
import { map } from 'rxjs/operators';
@@ -8,8 +8,12 @@ import { map } from 'rxjs/operators';
styleUrls: ['page-checkout.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class PageCheckoutComponent {
export class PageCheckoutComponent implements OnInit {
readonly breadcrumbKey$ = this.applicationService.activatedProcessId$.pipe(map((processId) => String(processId)));
constructor(private applicationService: ApplicationService) {}
ngOnInit() {
this.applicationService.setTitle('Warenkorb');
}
}

View File

@@ -1,6 +1,6 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { ShellBreadcrumbModule } from '@shell/breadcrumb';
import { BreadcrumbModule } from '@shared/components/breadcrumb';
import { CheckoutDummyModule } from './checkout-dummy/checkout-dummy.module';
import { CheckoutReviewModule } from './checkout-review/checkout-review.module';
import { CheckoutSummaryModule } from './checkout-summary/checkout-summary.module';
@@ -8,14 +8,7 @@ import { PageCheckoutRoutingModule } from './page-checkout-routing.module';
import { PageCheckoutComponent } from './page-checkout.component';
@NgModule({
imports: [
CommonModule,
CheckoutSummaryModule,
PageCheckoutRoutingModule,
CheckoutReviewModule,
CheckoutDummyModule,
ShellBreadcrumbModule,
],
imports: [CommonModule, CheckoutSummaryModule, PageCheckoutRoutingModule, CheckoutReviewModule, CheckoutDummyModule, BreadcrumbModule],
declarations: [PageCheckoutComponent],
exports: [],
})

View File

@@ -1,6 +1,7 @@
import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { BreadcrumbService } from '@core/breadcrumb';
import { EnvironmentService } from '@core/environment';
import { DomainCustomerOrderService, DomainGoodsService, DomainOmsService, OrderItemsContext } from '@domain/oms';
import { ComponentStore, tapResponse } from '@ngrx/component-store';
import { ListResponseArgsOfOrderItemListItemDTO, OrderItemListItemDTO, OrderItemProcessingStatusValue } from '@swagger/oms';
@@ -63,13 +64,18 @@ export class CustomerOrderDetailsComponent extends ComponentStore<CustomerOrderD
private _onDestroy$ = new Subject();
get isTablet() {
return this._environment.matchTablet();
}
constructor(
private _activatedRoute: ActivatedRoute,
private _domainGoodsInService: DomainCustomerOrderService,
private _omsService: DomainOmsService,
private _breadcrumb: BreadcrumbService,
private _router: Router,
private _uiModal: UiModalService
private _uiModal: UiModalService,
private _environment: EnvironmentService
) {
super({
fetching: false,
@@ -187,19 +193,37 @@ export class CustomerOrderDetailsComponent extends ComponentStore<CustomerOrderD
}
navigateToEditPage(orderItem: OrderItemListItemDTO) {
// if (this.isTablet) {
this._router.navigate([this.getEditPath(orderItem)], {
queryParams: { orderNumber: orderItem.orderNumber, buyerNumber: orderItem.buyerNumber, archive: this.archive },
queryParams: { buyerNumber: orderItem.buyerNumber, archive: this.archive },
});
// } else {
// this._router.navigate(this.getEditPathDesktop(orderItem), {
// queryParamsHandling: 'preserve',
// queryParams: { buyerNumber: orderItem.buyerNumber, archive: this.archive },
// });
// }
}
navigateToDetailsPage(item: OrderItemListItemDTO) {
// if (this.isTablet) {
this._router.navigate([this.getDetailsPath(item)], {
queryParams: { buyerNumber: item.buyerNumber, archive: this.archive },
});
// } else {
// this._router.navigate(this.getDetailsPathDesktop(item), {
// queryParamsHandling: 'preserve',
// queryParams: { buyerNumber: item.buyerNumber, archive: this.archive },
// });
// }
}
navigateToLandingPage() {
// if (this.isTablet) {
this._router.navigate([`/kunde/${this.processId}/order`]);
// } else {
// this._router.navigate(['/kunde', this.processId, 'order', { outlets: { main: null, left: 'search', right: 'filter' } }]);
// }
}
async actionHandled(handler: { orderItemsContext: OrderItemsContext; command: string; navigation: 'details' | 'main' | 'reservation' }) {
@@ -218,9 +242,65 @@ export class CustomerOrderDetailsComponent extends ComponentStore<CustomerOrderD
: `/kunde/${this.processId}/order/details/order/${encodeURIComponent(item?.orderNumber)}/${item?.processingStatus}`;
}
// getDetailsPathDesktop(item: OrderItemListItemDTO) {
// return item?.compartmentCode
// ? [
// '/kunde',
// this.processId,
// 'order',
// {
// outlets: {
// main: null,
// left: 'results',
// right: ['details', 'compartment', encodeURIComponent(item?.compartmentCode), item?.processingStatus],
// },
// },
// ]
// : [
// '/kunde',
// this.processId,
// 'order',
// {
// outlets: {
// main: null,
// left: 'results',
// right: ['details', 'order', encodeURIComponent(item?.orderNumber), item?.processingStatus],
// },
// },
// ];
// }
getEditPath(item: OrderItemListItemDTO) {
return item?.compartmentCode
? `/kunde/${this.processId}/order/details/compartment/${encodeURIComponent(item?.compartmentCode)}/${item?.processingStatus}/edit`
: `/kunde/${this.processId}/order/details/order/${encodeURIComponent(item?.orderNumber)}/${item?.processingStatus}/edit`;
}
// getEditPathDesktop(item: OrderItemListItemDTO) {
// return item?.compartmentCode
// ? [
// '/kunde',
// this.processId,
// 'order',
// {
// outlets: {
// main: null,
// left: 'results',
// right: ['details', 'compartment', encodeURIComponent(item?.compartmentCode), item?.processingStatus, 'edit'],
// },
// },
// ]
// : [
// '/kunde',
// this.processId,
// 'order',
// {
// outlets: {
// main: null,
// left: 'results',
// right: ['details', 'order', encodeURIComponent(item?.orderNumber), item?.processingStatus, 'edit'],
// },
// },
// ];
// }
}

View File

@@ -1,6 +1,7 @@
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { BreadcrumbService } from '@core/breadcrumb';
import { EnvironmentService } from '@core/environment';
import { DomainCustomerOrderService, DomainGoodsService, DomainOmsService } from '@domain/oms';
import { OrderItemListItemDTO } from '@swagger/oms';
import { UiMessageModalComponent, UiModalService } from '@ui/modal';
@@ -48,12 +49,17 @@ export class CustomerOrderEditComponent implements OnInit {
shareReplay()
);
get isTablet() {
return this._environment.matchTablet();
}
constructor(
private _activatedRoute: ActivatedRoute,
private _breadcrumb: BreadcrumbService,
private _domainGoodsInService: DomainCustomerOrderService,
private _router: Router,
private _uiModal: UiModalService
private _uiModal: UiModalService,
private _environment: EnvironmentService
) {}
ngOnInit() {
@@ -84,6 +90,8 @@ export class CustomerOrderEditComponent implements OnInit {
const processingStatus = options?.processingStatus ? options.processingStatus : this._activatedRoute.snapshot.params.processingStatus;
const buyerNumber = this._activatedRoute.snapshot.queryParams.buyerNumber;
const archive = this._activatedRoute.snapshot.queryParams.archive;
// if (this.isTablet) {
compartmentCode
? this._router.navigate(
[`/kunde/${this.processId}/order/details/compartment/${encodeURIComponent(compartmentCode)}/${processingStatus}`],
@@ -92,6 +100,42 @@ export class CustomerOrderEditComponent implements OnInit {
: this._router.navigate([`/kunde/${this.processId}/order/details/order/${encodeURIComponent(orderNumber)}/${processingStatus}`], {
queryParams: { buyerNumber, archive },
});
// } else {
// compartmentCode
// ? this._router.navigate(
// [
// '/kunde',
// this.processId,
// 'order',
// {
// outlets: {
// main: null,
// left: 'results',
// right: ['details', 'compartment', encodeURIComponent(compartmentCode), processingStatus],
// },
// },
// ],
// { queryParamsHandling: 'preserve', queryParams: { buyerNumber, archive } }
// )
// : this._router.navigate(
// [
// '/kunde',
// this.processId,
// 'order',
// {
// outlets: {
// main: null,
// left: 'results',
// right: ['details', 'order', encodeURIComponent(orderNumber), processingStatus],
// },
// },
// ],
// {
// queryParamsHandling: 'preserve',
// queryParams: { buyerNumber, archive },
// }
// );
// }
}
openModalIfItemsHaveDifferentCustomers(items: OrderItemListItemDTO[]) {

View File

@@ -2,11 +2,70 @@ import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { CustomerOrderDetailsComponent, CustomerOrderDetailsModule } from './customer-order-details';
import { CustomerOrderEditComponent } from './customer-order-edit';
import { CustomerOrderSearchComponent, CustomerOrderSearchModule } from './customer-order-search';
import { CustomerOrderSearchComponent, CustomerOrderSearchFilterComponent, CustomerOrderSearchModule } from './customer-order-search';
import { CustomerOrderSearchMainComponent, CustomerOrderSearchMainModule } from './customer-order-search/search-main';
import { CustomerOrderSearchResultsComponent, CustomerOrderSearchResultsModule } from './customer-order-search/search-results';
import { CustomerOrderComponent } from './customer-order.component';
// const auxiliaryRoutes = [
// {
// path: 'search',
// component: CustomerOrderSearchComponent,
// outlet: 'left',
// children: [
// {
// path: '',
// component: CustomerOrderSearchMainComponent,
// },
// ],
// },
// {
// path: 'filter',
// component: CustomerOrderSearchFilterComponent,
// outlet: 'right',
// },
// {
// path: 'filter/:compartmentCode/:processingStatus',
// component: CustomerOrderSearchFilterComponent,
// outlet: 'right',
// },
// {
// path: 'filter/:orderNumber/:processingStatus',
// component: CustomerOrderSearchFilterComponent,
// outlet: 'right',
// },
// {
// path: 'results',
// component: CustomerOrderSearchResultsComponent,
// outlet: 'left',
// },
// {
// path: 'results',
// component: CustomerOrderSearchResultsComponent,
// outlet: 'main',
// },
// {
// path: 'details/compartment/:compartmentCode/:processingStatus',
// component: CustomerOrderDetailsComponent,
// outlet: 'right',
// },
// {
// path: 'details/order/:orderNumber/:processingStatus',
// component: CustomerOrderDetailsComponent,
// outlet: 'right',
// },
// {
// path: 'details/compartment/:compartmentCode/:processingStatus/edit',
// component: CustomerOrderEditComponent,
// outlet: 'right',
// },
// {
// path: 'details/order/:orderNumber/:processingStatus/edit',
// component: CustomerOrderEditComponent,
// outlet: 'right',
// },
// ];
const routes: Routes = [
{
path: '',
@@ -20,6 +79,10 @@ const routes: Routes = [
{ path: 'results', component: CustomerOrderSearchResultsComponent },
],
},
{
path: 'filter',
component: CustomerOrderSearchFilterComponent,
},
{
path: 'details/compartment/:compartmentCode/:processingStatus',
component: CustomerOrderDetailsComponent,
@@ -36,6 +99,7 @@ const routes: Routes = [
path: 'details/order/:orderNumber/:processingStatus/edit',
component: CustomerOrderEditComponent,
},
// ...auxiliaryRoutes,
],
},
];

View File

@@ -1,31 +1,33 @@
<div class="goods-out-search-filter-content">
<button class="btn-close" type="button" (click)="close.emit()">
<ui-icon icon="close" size="20px"></ui-icon>
</button>
<ng-container *ngIf="filter$ | async; let filter">
<div class="goods-out-search-filter-content">
<a class="btn-close" type="button" [routerLink]="closeFilterRouterPath" queryParamsHandling="preserve">
<ui-icon icon="close" size="20px"></ui-icon>
</a>
<div class="goods-out-search-filter-content-main">
<h1 class="title">Filter</h1>
<ui-filter
[filter]="filter"
[loading]="loading$ | async"
(search)="applyFilter()"
[hint]="message"
resizeInputOptionsToElement="page-goods-out-search-filter .cta-wrapper"
[scanner]="true"
>
<page-order-branch-id-input *uiFilterCustomInput="'order_branch_id'; let input" [input]="input"> </page-order-branch-id-input>
</ui-filter>
<div class="goods-out-search-filter-content-main">
<h1 class="title">Filter</h1>
<ui-filter
[filter]="filter"
[loading]="loading$ | async"
(search)="applyFilter(filter)"
[hint]="message"
resizeInputOptionsToElement="page-goods-out-search-filter .cta-wrapper"
[scanner]="true"
>
<page-order-branch-id-input *uiFilterCustomInput="'order_branch_id'; let input" [input]="input"> </page-order-branch-id-input>
</ui-filter>
</div>
</div>
</div>
<div class="cta-wrapper">
<button class="cta-reset-filter" (click)="resetFilter()" [disabled]="loading$ | async">
Filter zurücksetzen
</button>
<div class="cta-wrapper">
<button class="cta-reset-filter" (click)="resetFilter(filter)" [disabled]="loading$ | async">
Filter zurücksetzen
</button>
<button class="cta-apply-filter" (click)="applyFilter()" [disabled]="loading$ | async">
<ui-spinner [show]="loading$ | async">
Filter anwenden
</ui-spinner>
</button>
</div>
<button class="cta-apply-filter" (click)="applyFilter(filter)" [disabled]="loading$ | async">
<ui-spinner [show]="loading$ | async">
Filter anwenden
</ui-spinner>
</button>
</div>
</ng-container>

View File

@@ -22,9 +22,7 @@
}
.cta-wrapper {
@apply fixed bottom-8 whitespace-nowrap;
left: 50%;
transform: translateX(-50%);
@apply text-center whitespace-nowrap;
}
.cta-reset-filter,

View File

@@ -9,13 +9,15 @@ import {
Input,
ViewChild,
} from '@angular/core';
import { Router } from '@angular/router';
import { ActivatedRoute, Params, Router } from '@angular/router';
import { BreadcrumbService } from '@core/breadcrumb';
import { OrderItemListItemDTO } from '@swagger/oms';
import { UiFilter, UiFilterComponent } from '@ui/filter';
import { Observable, Subject } from 'rxjs';
import { take, takeUntil } from 'rxjs/operators';
import { map, take, takeUntil } from 'rxjs/operators';
import { CustomerOrderSearchStore } from '../customer-order-search.store';
import { EnvironmentService } from '@core/environment';
import { ApplicationService } from '@core/application';
@Component({
selector: 'page-customer-order-search-filter',
@@ -27,12 +29,14 @@ export class CustomerOrderSearchFilterComponent implements OnInit, OnDestroy {
@Output()
close = new EventEmitter();
filter: UiFilter;
loading$: Observable<boolean>;
message: string;
get isTablet() {
return this._environment.matchTablet();
}
@Input()
processId: number;
@@ -41,11 +45,57 @@ export class CustomerOrderSearchFilterComponent implements OnInit, OnDestroy {
@ViewChild(UiFilterComponent, { static: false })
uiFilter: UiFilterComponent;
filter$: Observable<UiFilter>;
get leftOutletLocation(): string {
return this._activatedRoute?.parent?.children?.find((childRoute) => childRoute?.outlet === 'left')?.snapshot?.routeConfig?.path ?? '';
}
get rightOutletParams(): Params {
return this._activatedRoute?.parent?.children?.find((childRoute) => childRoute?.outlet === 'right')?.snapshot?.params;
}
get closeFilterRouterPath() {
// if (!this.isTablet) {
// if (this.leftOutletLocation.includes('results')) {
// const params = this.rightOutletParams;
// const orderNumber = params?.orderNumber;
// const compartmentCode = params?.compartmentCode;
// const processingStatus = params?.processingStatus;
// return orderNumber
// ? [
// '/kunde',
// this.application.activatedProcessId,
// 'order',
// { outlets: { main: null, left: 'results', right: ['details', 'order', orderNumber, processingStatus] } },
// ]
// : [
// '/kunde',
// this.application.activatedProcessId,
// 'order',
// {
// outlets: {
// main: null,
// left: 'results',
// right: ['details', 'compartment', compartmentCode, processingStatus],
// },
// },
// ];
// }
// return ['/kunde', this.application.activatedProcessId, 'order', { outlets: { main: null, left: 'search', right: 'filter' } }];
// } else {
return ['/kunde', this.application.activatedProcessId, 'order', 'results'];
// }
}
constructor(
private _goodsOutSearchStore: CustomerOrderSearchStore,
private _breadcrumb: BreadcrumbService,
private _cdr: ChangeDetectorRef,
private _router: Router
private _router: Router,
private _environment: EnvironmentService,
private _activatedRoute: ActivatedRoute,
public application: ApplicationService
) {}
ngOnInit() {
@@ -59,23 +109,23 @@ export class CustomerOrderSearchFilterComponent implements OnInit, OnDestroy {
}
private _initSettings() {
this.filter = UiFilter.create(this._goodsOutSearchStore.filter);
this.filter$ = this._goodsOutSearchStore.filter$.pipe(map((filter) => UiFilter.create(filter)));
}
private _initLoading$() {
this.loading$ = this._goodsOutSearchStore.fetching$;
}
async resetFilter() {
const queryParams = { main_qs: this.filter?.getQueryParams()?.main_qs || '' };
async resetFilter(filter: UiFilter) {
const queryParams = { main_qs: filter?.getQueryParams()?.main_qs || '' };
this._goodsOutSearchStore.resetFilter(queryParams);
this._initSettings();
this._cdr.markForCheck();
}
async applyFilter() {
async applyFilter(filter: UiFilter) {
this.uiFilter?.cancelAutocomplete();
this._goodsOutSearchStore.setFilter(this.filter);
this._goodsOutSearchStore.setFilter(filter);
this.message = undefined;
await this.updateQueryParams();
@@ -84,6 +134,7 @@ export class CustomerOrderSearchFilterComponent implements OnInit, OnDestroy {
if (result.results.error) {
} else {
if (result.results.hits > 0) {
// if (this.isTablet) {
if (result.results.hits === 1) {
const orderItem = result.results.result[0];
this._router.navigate([this.getDetailsPath(orderItem)]);
@@ -92,6 +143,18 @@ export class CustomerOrderSearchFilterComponent implements OnInit, OnDestroy {
queryParams: this._goodsOutSearchStore.filter.getQueryParams(),
});
}
// } else {
// const orderItem = result.results.result[0];
// if (result.results.hits === 1) {
// this._router.navigate(this.getDetailsPathDesktop(orderItem, this.processId), {
// queryParams: this._goodsOutSearchStore.filter.getQueryParams(),
// });
// } else {
// this._router.navigate(this.getDetailsPathDesktop(orderItem, this.processId), {
// queryParams: this._goodsOutSearchStore.filter.getQueryParams(),
// });
// }
// }
this.close.emit();
} else {
@@ -126,4 +189,32 @@ export class CustomerOrderSearchFilterComponent implements OnInit, OnDestroy {
? `/kunde/${this.processId}/order/details/compartment/${encodeURIComponent(item?.compartmentCode)}/${item?.processingStatus}`
: `/kunde/${this.processId}/order/details/order/${encodeURIComponent(item?.orderNumber)}/${item?.processingStatus}`;
}
// getDetailsPathDesktop(item: OrderItemListItemDTO, processId: number) {
// return item?.compartmentCode
// ? [
// '/kunde',
// processId,
// 'order',
// {
// outlets: {
// main: null,
// left: 'results',
// right: ['details', 'compartment', encodeURIComponent(item?.compartmentCode), item?.processingStatus],
// },
// },
// ]
// : [
// '/kunde',
// processId,
// 'order',
// {
// outlets: {
// main: null,
// left: 'results',
// right: ['details', 'order', encodeURIComponent(item?.orderNumber), item?.processingStatus],
// },
// },
// ];
// }
}

View File

@@ -25,18 +25,31 @@ export class OrderBranchIdInputComponent extends AbstractUiFilterInputDirective
ngOnInit() {
this.control.setValue({ id: Number(this.value) });
const uiInputChangesSub = this.uiInput?.changes?.subscribe((changes) => {
const controlValue = this.control?.value?.id;
const changesValue = Number(changes?.target?.value);
if (controlValue !== changesValue) {
this.control.setValue(changesValue && !isNaN(changesValue) ? { id: changesValue } : undefined);
}
});
const onInputChangeSub = this.onUiInputChange$.subscribe((input) => {
if (this.control.value !== input.value) {
this.control.setValue(input.value ? { id: Number(input.value) } : undefined);
const controlValue = this.control?.value?.id;
const inputValue = Number(input?.value);
if (controlValue !== inputValue) {
this.control.setValue(inputValue && !isNaN(inputValue) ? { id: inputValue } : undefined);
}
});
const onControlValueChangeSub = this.control.valueChanges.subscribe((value) => {
if (this.value !== value) {
this.setValue(value ? String(value?.id) : undefined);
if (!value) {
this.setValue(undefined);
} else if (this.value !== String(value?.id)) {
this.setValue(String(value?.id));
}
});
this._subscriptions.add(uiInputChangesSub);
this._subscriptions.add(onInputChangeSub);
this._subscriptions.add(onControlValueChangeSub);
}

View File

@@ -1,9 +1,9 @@
<div class="flex flex-row justify-end mb-4">
<!-- <div *ngIf="isTablet" class="flex flex-row justify-end mb-4">
<button class="filter" [class.active]="hasFilter$ | async" (click)="shellFilterOverlay.open()">
<ui-icon size="20px" icon="filter_alit"></ui-icon>
<span class="label">Filter</span>
</button>
</div>
</div> -->
<router-outlet></router-outlet>

View File

@@ -8,6 +8,7 @@ import { combineLatest, Subject } from 'rxjs';
import { map, takeUntil, withLatestFrom } from 'rxjs/operators';
import { CustomerOrderSearchStore } from './customer-order-search.store';
import { CustomerOrderSearchMainAutocompleteProvider } from './providers/customer-order-search-main-autocomplete.provider';
import { EnvironmentService } from '@core/environment';
@Component({
selector: 'page-customer-order-search',
@@ -15,7 +16,6 @@ import { CustomerOrderSearchMainAutocompleteProvider } from './providers/custome
styleUrls: ['customer-order-search.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [
CustomerOrderSearchStore,
{
provide: UiFilterAutocompleteProvider,
useClass: CustomerOrderSearchMainAutocompleteProvider,
@@ -32,28 +32,25 @@ import { CustomerOrderSearchMainAutocompleteProvider } from './providers/custome
export class CustomerOrderSearchComponent implements OnInit, OnDestroy {
private _onDestroy$ = new Subject();
get isTablet() {
return this._environment.matchTablet();
}
hasFilter$ = combineLatest([this._goodsOutSearchStore.filter$, this._goodsOutSearchStore.defaultSettings$]).pipe(
map(([filter, defaultFilter]) => !isEqual(filter?.getQueryParams(), UiFilter.create(defaultFilter).getQueryParams()))
);
processId$ = this._activatedRoute.data.pipe(map((data) => +data.processId));
processId$ = this._activatedRoute.parent.data.pipe(map((data) => +data.processId));
constructor(
private _goodsOutSearchStore: CustomerOrderSearchStore,
private _breadcrumb: BreadcrumbService,
private _activatedRoute: ActivatedRoute
private _activatedRoute: ActivatedRoute,
private _environment: EnvironmentService
) {}
ngOnInit() {
this.processId$.pipe(takeUntil(this._onDestroy$), withLatestFrom(this._activatedRoute.queryParams)).subscribe(([processId]) => {
// if (params && Object.keys(params).length === 0) {
// console.log('params is empty');
// this._goodsOutSearchStore.setQueryParams(params);
// this._goodsOutSearchStore.loadSettings();
// } else {
// // this._goodsOutSearchStore.resetFilter(params);
// }
this._breadcrumb.addOrUpdateBreadcrumbIfNotExists({
key: processId,
name: 'Kundenbestellung',

View File

@@ -6,8 +6,9 @@ import { CustomerOrderSearchFilterComponent, OrderBranchIdInputComponent } from
import { UiIconModule } from '@ui/icon';
import { RouterModule } from '@angular/router';
import { UiFilterNextModule } from '@ui/filter';
import { ShellFilterOverlayModule } from '@shell/filter-overlay';
import { SharedFilterOverlayModule } from '@shared/components/filter-overlay';
import { UiSpinnerModule } from '@ui/spinner';
import { CustomerOrderSearchStore } from './customer-order-search.store';
@NgModule({
imports: [
@@ -15,11 +16,12 @@ import { UiSpinnerModule } from '@ui/spinner';
UiIconModule,
RouterModule,
UiFilterNextModule,
ShellFilterOverlayModule,
SharedFilterOverlayModule,
UiSpinnerModule,
OrderBranchIdInputComponent,
],
exports: [CustomerOrderSearchComponent],
providers: [CustomerOrderSearchStore],
declarations: [CustomerOrderSearchComponent, CustomerOrderSearchFilterComponent],
})
export class CustomerOrderSearchModule {}

View File

@@ -7,7 +7,7 @@
}
.search-main {
@apply bg-white text-center rounded-t-card py-5 shadow-card;
@apply bg-white text-center rounded-t-card p-5 shadow-card;
height: calc(100vh - 380px);
.search-main-title {

View File

@@ -7,6 +7,7 @@ import { debounce, isEqual } from 'lodash';
import { BehaviorSubject, combineLatest, Subscription } from 'rxjs';
import { debounceTime, first, map, withLatestFrom } from 'rxjs/operators';
import { CustomerOrderSearchStore } from '../customer-order-search.store';
import { EnvironmentService } from '@core/environment';
@Component({
selector: 'page-customer-order-search-main',
@@ -28,10 +29,14 @@ export class CustomerOrderSearchMainComponent implements OnInit, OnDestroy {
private _subscriptions = new Subscription();
get processId() {
return +this._activatedRoute.snapshot.data.processId;
return +this._activatedRoute.parent.parent.snapshot.data.processId;
}
processId$ = this._activatedRoute.data.pipe(map((data) => +data.processId));
processId$ = this._activatedRoute.parent.parent.data.pipe(map((data) => +data.processId));
get isTablet() {
return this._environment.matchTablet();
}
@ViewChild(UiFilterInputGroupMainComponent, { static: false })
filterInputGroup: UiFilterInputGroupMainComponent;
@@ -41,7 +46,8 @@ export class CustomerOrderSearchMainComponent implements OnInit, OnDestroy {
private _cdr: ChangeDetectorRef,
private _router: Router,
private _activatedRoute: ActivatedRoute,
private _breadcrumb: BreadcrumbService
private _breadcrumb: BreadcrumbService,
private _environment: EnvironmentService
) {}
ngOnInit() {
@@ -106,14 +112,29 @@ export class CustomerOrderSearchMainComponent implements OnInit, OnDestroy {
if (result.results.error) {
} else {
if (result.results.hits > 0) {
// if (this.isTablet) {
if (result.results.hits === 1) {
const orderItem = result.results.result[0];
this._router.navigate([this.getDetailsPath(orderItem, this.processId)]);
this._router.navigate([this.getDetailsPath(orderItem, this.processId)], {
queryParams: this._goodsOutSearchStore.filter.getQueryParams(),
});
} else {
this._router.navigate(['/kunde', this.processId, 'order', 'results'], {
queryParams: this._goodsOutSearchStore.filter.getQueryParams(),
});
}
// } else {
// const orderItem = result.results.result[0];
// if (result.results.hits === 1) {
// this._router.navigate(this.getDetailsPathDesktop(orderItem, this.processId), {
// queryParams: this._goodsOutSearchStore.filter.getQueryParams(),
// });
// } else {
// this._router.navigate(this.getDetailsPathDesktop(orderItem, this.processId), {
// queryParams: this._goodsOutSearchStore.filter.getQueryParams(),
// });
// }
// }
} else {
this.message = 'keine Suchergebnisse';
}
@@ -149,6 +170,34 @@ export class CustomerOrderSearchMainComponent implements OnInit, OnDestroy {
: `/kunde/${processId}/order/details/order/${encodeURIComponent(item?.orderNumber)}/${item?.processingStatus}`;
}
// getDetailsPathDesktop(item: OrderItemListItemDTO, processId: number) {
// return item?.compartmentCode
// ? [
// '/kunde',
// processId,
// 'order',
// {
// outlets: {
// main: null,
// left: 'results',
// right: ['details', 'compartment', encodeURIComponent(item?.compartmentCode), item?.processingStatus],
// },
// },
// ]
// : [
// '/kunde',
// processId,
// 'order',
// {
// outlets: {
// main: null,
// left: 'results',
// right: ['details', 'order', encodeURIComponent(item?.orderNumber), item?.processingStatus],
// },
// },
// ];
// }
queryChangeDebounce = debounce(async () => {
this.queryChanged$.next(true);
await this.updateQueryParams(this.processId);

View File

@@ -0,0 +1,45 @@
<a
class="page-customer-order-item__item-card p-5 desktop:p-px-10 h-[212px] desktop:h-[181px] bg-white rounded-card"
[routerLink]="detailsLink"
[queryParams]="queryParams"
[queryParamsHandling]="!isTablet ? 'preserve' : ''"
>
{{ item?.product?.name }}
<!-- <div class="page-search-result-item__item-thumbnail text-center mr-4 w-[50px] h-[79px]">
<img
class="page-search-result-item__item-image w-[50px] h-[79px]"
loading="lazy"
*ngIf="item?.imageId | thumbnailUrl; let thumbnailUrl"
[src]="thumbnailUrl"
[alt]="item?.product?.name"
/>
</div>
<div class="grid-container-test">
<div
class="page-search-result-item__item-title font-bold text-2xl"
[class.text-xl]="item?.product?.name?.length >= 35"
[class.text-lg]="item?.product?.name?.length >= 40"
[class.text-md]="item?.product?.name?.length >= 50"
[class.text-sm]="item?.product?.name?.length >= 60 || !isTablet"
[class.text-xs]="item?.product?.name?.length >= 100 || (!isTablet && item?.product?.name?.length >= 70)"
>
{{ item?.product?.name }}
</div>
<div class="page-search-result-item__item-special-comment font-bold text-sm">
{{ item?.specialComment }}
</div>
<div class="page-search-result-item__item-select-bullet justify-self-end">
<input
*ngIf="selectable"
(click)="$event.stopPropagation()"
[ngModel]="selected$ | async"
(ngModelChange)="setSelected()"
class="isa-select-bullet"
type="checkbox"
/>
</div>
</div> -->
</a>

View File

@@ -0,0 +1,3 @@
:host {
@apply flex flex-col w-full h-[212px] desktop:h-[181px] bg-white;
}

View File

@@ -0,0 +1,124 @@
import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { Router } from '@angular/router';
import { ApplicationService } from '@core/application';
import { EnvironmentService } from '@core/environment';
import { ComponentStore } from '@ngrx/component-store';
import { OrderItemListItemDTO } from '@swagger/oms';
import { isEqual } from 'lodash';
import { map } from 'rxjs/operators';
import { CustomerOrderSearchStore } from '../customer-order-search.store';
export interface CustomerOrderItemComponentState {
item?: OrderItemListItemDTO;
selected: boolean;
selectable: boolean;
}
@Component({
selector: 'page-customer-order-item',
templateUrl: 'customer-order-item.component.html',
styleUrls: ['customer-order-item.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CustomerOrderItemComponent extends ComponentStore<CustomerOrderItemComponentState> implements OnInit {
@Input()
get item() {
return this.get((s) => s.item);
}
set item(item: OrderItemListItemDTO) {
if (!isEqual(this.item, item)) {
this.patchState({ item });
}
}
readonly item$ = this.select((s) => s.item);
// selected$ = this._articleSearchService.selectedItemIds$.pipe(map((selectedItemIds) => selectedItemIds.includes(this.item?.id)));
@Input()
selected: boolean;
@Input()
get selectable() {
return this.get((s) => s.selectable);
}
set selectable(selectable: boolean) {
if (this.selectable !== selectable) {
this.patchState({ selectable });
}
}
@Output()
selectedChange = new EventEmitter<boolean>();
get isTablet() {
return this._environment.matchTablet();
}
get queryParams() {
const compartmentCode = this.item?.compartmentCode;
const archive = !!this._customerOrderStore.filter?.getQueryParams()?.main_archive || false;
if (compartmentCode) {
return {
buyerNumber: this.item?.buyerNumber,
archive,
};
} else {
return {
archive,
};
}
}
get detailsLink() {
const orderNumber = this.item?.orderNumber;
const processingStatus = this.item?.processingStatus;
const compartmentCode = this.item?.compartmentCode;
// if (this.isTablet) {
if (compartmentCode) {
return [
`/kunde/${this._applicationService.activatedProcessId}/order/details/compartment/${encodeURIComponent(
compartmentCode
)}/${processingStatus}`,
];
} else {
return [
`/kunde/${this._applicationService.activatedProcessId}/order/details/order/${encodeURIComponent(orderNumber)}/${processingStatus}`,
];
}
// } else {
// if (compartmentCode) {
// return [
// '/kunde',
// this._applicationService.activatedProcessId,
// 'order',
// {
// outlets: {
// main: null,
// left: 'results',
// right: ['details', 'compartment', encodeURIComponent(compartmentCode), processingStatus],
// },
// },
// ];
// } else {
// return [
// '/kunde',
// this._applicationService.activatedProcessId,
// 'order',
// { outlets: { main: null, left: 'results', right: ['details', 'order', encodeURIComponent(orderNumber), processingStatus] } },
// ];
// }
// }
}
constructor(
private _environment: EnvironmentService,
private _customerOrderStore: CustomerOrderSearchStore,
private _applicationService: ApplicationService
) {
super({ selected: false, selectable: false });
}
ngOnInit() {}
}

View File

@@ -1,7 +1,28 @@
<div class="hits">{{ hits$ | async }} Titel</div>
<div class="page-customer-order-search-results__header bg-background-liste flex flex-col items-end pb-4">
<a
[class.active]="hasFilter$ | async"
class="page-customer-order-search-results__filter h-14 rounded-card font-bold px-5 mb-4 text-lg bg-cadet-blue flex flex-row flex-nowrap items-center justify-center"
[routerLink]="filterRoute"
queryParamsHandling="preserve"
>
<ui-svg-icon class="mr-2" icon="filter-variant"></ui-svg-icon>
Filter
</a>
<div
*ngIf="hits$ | async; let hits"
class="page-customer-order-search-results__items-count inline-flex flex-row items-center pr-5 text-sm"
>
{{ hits ??
0 }}
Titel
</div>
</div>
<ui-scroll-container
*ngIf="!(listEmpty$ | async); else emptyMessage"
[loading]="loading$ | async"
[showScrollbar]="false"
[showScrollArrow]="false"
(reachEnd)="loadMore()"
[deltaEnd]="150"
[itemLength]="itemLength$ | async"
@@ -10,6 +31,22 @@
>
<ng-container *ngIf="processId$ | async; let processId">
<shared-goods-in-out-order-group *ngFor="let bueryNumberGroup of items$ | async | groupBy: byBuyerNumberFn">
<!-- <div class="page-customer-order-search__items-list" *ngFor="let bueryNumberGroup of items$ | async | groupBy: byBuyerNumberFn">
<ng-container *ngIf="bueryNumberGroup.items[0]; let firstItem">
<div class="page-customer-order-search__item-header-group bg-white text-xl rounded-t-card p-4 font-bold mb-px-2">
<h3 class="mt-0 mb-4">
{{ firstItem?.organisation }}
<ng-container *ngIf="!!firstItem?.organisation && (!!firstItem?.firstName || !!firstItem?.lastName)"> - </ng-container>
{{ firstItem?.lastName }}
{{ firstItem?.firstName }}
</h3>
<h4 class="mt-0 mb-0">
{{ firstItem?.buyerNumber }}
ans lager (nicht abgeholt) bezahlt
</h4>
</div>
</ng-container> -->
<ng-container *ngFor="let orderNumberGroup of bueryNumberGroup.items | groupBy: byOrderNumberFn; let lastOrderNumber = last">
<ng-container
*ngFor="let processingStatusGroup of orderNumberGroup.items | groupBy: byProcessingStatusFn; let lastProcessingStatus = last"
@@ -26,6 +63,17 @@
[selected]="item | goodsOutItemSelected: selectedOrderItemSubsetIds"
(selectedChange)="setSelectedItem(item, $event)"
></shared-goods-in-out-order-group-item>
<!-- <page-customer-order-item
*ngFor="let item of compartmentCodeGroup.items; let firstItem = first; trackBy: trackByFn"
class="mb-px-10"
[selectable]="item | goodsOutItemSelectable: selectionRules:selectedItems"
[selected]="item | goodsOutItemSelected: selectedOrderItemSubsetIds"
[item]="item"
(click)="navigateToDetails(processId, item)"
(selectedChange)="setSelectedItem(item, $event)"
></page-customer-order-item> -->
<div class="divider" *ngIf="!lastCompartmentCode"></div>
</ng-container>
<div class="divider" *ngIf="!lastProcessingStatus"></div>

View File

@@ -1,11 +1,11 @@
// :host {
// @apply box-border block h-[100vh] max-h-[calc(100vh-364px)] desktop:max-h-[calc(100vh-300px)];
// }
:host {
@apply block relative;
}
.hits {
@apply text-active-customer text-right mb-3 font-semibold text-base;
}
.empty-message {
@apply bg-white text-center font-semibold text-inactive-customer py-10 rounded-card;
}
@@ -18,6 +18,10 @@ shared-goods-in-out-order-group-item {
@apply cursor-pointer;
}
// page-customer-order-item {
// @apply cursor-pointer;
// }
.actions {
@apply fixed bottom-28 inline-grid grid-flow-col gap-7;
left: 50%;
@@ -40,6 +44,10 @@ shared-goods-in-out-order-group-item {
}
}
// ::ng-deep page-customer-order-search-results ui-scroll-container .scroll-container {
// max-height: calc(100vh - 25.5rem) !important;
// }
::ng-deep .desktop page-goods-out-search-results ui-scroll-container {
.scrollbar-gap::-webkit-scrollbar-track {
margin-bottom: 7.25rem;

View File

@@ -1,7 +1,7 @@
import { Component, ChangeDetectionStrategy, OnInit, OnDestroy, ViewChild, TrackByFunction } from '@angular/core';
import { debounceTime, first, map, shareReplay, takeUntil, withLatestFrom } from 'rxjs/operators';
import { debounceTime, filter, first, map, shareReplay, takeUntil, withLatestFrom } from 'rxjs/operators';
import { KeyValueDTOOfStringAndString, OrderItemListItemDTO } from '@swagger/oms';
import { ActivatedRoute, Router } from '@angular/router';
import { ActivatedRoute, ChildrenOutletContexts, Params, Router } from '@angular/router';
import { CustomerOrderSearchStore } from '../customer-order-search.store';
import { BehaviorSubject, combineLatest, Observable, Subject, Subscription } from 'rxjs';
import { BreadcrumbService } from '@core/breadcrumb';
@@ -11,6 +11,8 @@ import { OrderItemsContext } from '@domain/oms';
import { UiErrorModalComponent, UiModalService } from '@ui/modal';
import { UiScrollContainerComponent } from '@ui/scroll-container';
import { UiFilter } from '@ui/filter';
import { EnvironmentService } from '@core/environment';
import { isEqual } from 'lodash';
export interface CustomerOrderSearchResultsState {
selectedOrderItemSubsetIds: number[];
@@ -80,13 +82,51 @@ export class CustomerOrderSearchResultsComponent extends ComponentStore<Customer
scrollTo: number;
filter$ = this._goodsOutSearchStore.filter$;
initialFilter$ = this.filter$.pipe(
filter((filter) => !!filter),
first()
);
hasFilter$ = this.filter$.pipe(
withLatestFrom(this.initialFilter$),
map(([filter, initialFilter]) => !isEqual(filter?.getQueryParams(), initialFilter?.getQueryParams()))
);
get isTablet() {
return this._environment.matchTablet();
}
get rightOutletParams(): Params {
return this._activatedRoute?.parent?.children?.find((childRoute) => childRoute?.outlet === 'right')?.snapshot?.params;
}
get filterRoute() {
const processId = this._activatedRoute?.parent?.snapshot?.data?.processId;
// if (!this.isTablet) {
// const orderNumber = this.rightOutletParams?.orderNumber;
// const compartmentCode = this.rightOutletParams?.compartmentCode;
// const processingStatus = this.rightOutletParams?.processingStatus;
// return [
// '/kunde',
// processId,
// 'order',
// { outlets: { main: null, left: 'results', right: ['filter', orderNumber ?? compartmentCode, processingStatus] } },
// ];
// } else {
return ['/kunde', processId, 'order', 'filter'];
// }
}
constructor(
private _goodsOutSearchStore: CustomerOrderSearchStore,
private _router: Router,
private _activatedRoute: ActivatedRoute,
private _breadcrumb: BreadcrumbService,
private _commandService: CommandService,
private _modal: UiModalService
private _modal: UiModalService,
private _environment: EnvironmentService
) {
super({
selectedOrderItemSubsetIds: [],
@@ -263,6 +303,7 @@ export class CustomerOrderSearchResultsComponent extends ComponentStore<Customer
const compartmentCode = orderItem.compartmentCode;
const archive = !!this._goodsOutSearchStore.filter?.getQueryParams()?.main_archive || false;
// if (this.isTablet) {
if (compartmentCode) {
this._router.navigate([`/kunde/${processId}/order/details/compartment/${encodeURIComponent(compartmentCode)}/${processingStatus}`], {
queryParams: {
@@ -277,6 +318,46 @@ export class CustomerOrderSearchResultsComponent extends ComponentStore<Customer
},
});
}
// } else {
// if (compartmentCode) {
// this._router.navigate(
// [
// '/kunde',
// processId,
// 'order',
// {
// outlets: {
// main: null,
// left: 'results',
// right: ['details', 'compartment', encodeURIComponent(compartmentCode), processingStatus],
// },
// },
// ],
// {
// queryParamsHandling: 'preserve',
// queryParams: {
// buyerNumber: orderItem.buyerNumber,
// archive,
// },
// }
// );
// } else {
// this._router.navigate(
// [
// '/kunde',
// processId,
// 'order',
// { outlets: { main: null, left: 'results', right: ['details', 'order', encodeURIComponent(orderNumber), processingStatus] } },
// ],
// {
// queryParamsHandling: 'preserve',
// queryParams: {
// archive,
// },
// }
// );
// }
// }
}
setSelectedItem(item: OrderItemListItemDTO, selected: boolean) {

View File

@@ -8,10 +8,18 @@ import { CustomerOrderItemSelectablePipe } from './customer-order-item-selectabl
import { CustomerOrderItemSelectedPipe } from './customer-order-item-selectede.pipe';
import { UiSpinnerModule } from 'apps/ui/spinner/src/lib/ui-spinner.module';
import { UiScrollContainerModule } from '@ui/scroll-container';
import { UiIconModule } from '@ui/icon';
import { RouterModule } from '@angular/router';
import { CustomerOrderItemComponent } from './customer-order-item.component';
@NgModule({
imports: [CommonModule, GoodsInOutOrderGroupModule, UiCommonModule, UiSpinnerModule, UiScrollContainerModule],
exports: [CustomerOrderSearchResultsComponent],
declarations: [CustomerOrderSearchResultsComponent, CustomerOrderItemSelectablePipe, CustomerOrderItemSelectedPipe],
imports: [CommonModule, RouterModule, GoodsInOutOrderGroupModule, UiCommonModule, UiIconModule, UiSpinnerModule, UiScrollContainerModule],
exports: [CustomerOrderSearchResultsComponent, CustomerOrderItemComponent],
declarations: [
CustomerOrderSearchResultsComponent,
CustomerOrderItemSelectablePipe,
CustomerOrderItemSelectedPipe,
CustomerOrderItemComponent,
],
})
export class CustomerOrderSearchResultsModule {}

View File

@@ -2,3 +2,4 @@ export * from './customer-order-item-selectable.pipe';
export * from './customer-order-item-selectede.pipe';
export * from './customer-order-search-results.component';
export * from './customer-order-search-results.module';
export * from './customer-order-item.component';

View File

@@ -8,4 +8,19 @@
</shared-branch-selector>
</shared-breadcrumb>
<!-- <ng-container *ngIf="isTablet; else desktop"> -->
<router-outlet></router-outlet>
<!-- </ng-container>
<ng-template #desktop>
<router-outlet name="main"></router-outlet>
<div class="grid desktop:grid-cols-[31rem_auto]">
<div class="mr-6 hidden desktop:block">
<router-outlet name="left"></router-outlet>
</div>
<div>
<router-outlet name="right"></router-outlet>
</div>
</div>
</ng-template> -->

View File

@@ -3,6 +3,7 @@ import { ActivatedRoute } from '@angular/router';
import { ApplicationService } from '@core/application';
import { AuthService } from '@core/auth';
import { BreadcrumbService } from '@core/breadcrumb';
import { EnvironmentService } from '@core/environment';
import { BranchDTO } from '@swagger/checkout';
import { UiErrorModalComponent, UiModalService } from '@ui/modal';
import { Observable } from 'rxjs';
@@ -19,6 +20,10 @@ export class CustomerOrderComponent implements OnInit {
selectedBranch$: Observable<BranchDTO>;
get isTablet() {
return this._environmentService.matchTablet();
}
onCustomerOrderDetailsPage$: Observable<boolean>;
constructor(
@@ -26,6 +31,7 @@ export class CustomerOrderComponent implements OnInit {
private _activatedRoute: ActivatedRoute,
private _uiModal: UiModalService,
private _breadcrumb: BreadcrumbService,
private _environmentService: EnvironmentService,
public auth: AuthService
) {}
@@ -40,6 +46,8 @@ export class CustomerOrderComponent implements OnInit {
this.selectedBranch$ = this.application.activatedProcessId$.pipe(
switchMap((processId) => this.application.getSelectedBranch$(Number(processId)))
);
this.application.setTitle('Kundenbestellung');
}
async patchProcessData(selectedBranch: BranchDTO) {

View File

@@ -11,7 +11,7 @@ import { UiCommonModule } from '@ui/common';
import { CustomerResultCardComponent } from './search-results/customer-result-card/customer-result-card.component';
import { CustomerSearchFilterComponent } from './search-filter/search-filter.component';
import { UiFilterNextModule } from '@ui/filter';
import { ShellFilterOverlayModule } from '@shell/filter-overlay';
import { SharedFilterOverlayModule } from '@shared/components/filter-overlay';
import { UiScrollContainerModule } from '@ui/scroll-container';
import { UiSpinnerModule } from '@ui/spinner';
@@ -23,7 +23,7 @@ import { UiSpinnerModule } from '@ui/spinner';
UiCommonModule,
UiIconModule,
ReactiveFormsModule,
ShellFilterOverlayModule,
SharedFilterOverlayModule,
UiFilterNextModule,
UiScrollContainerModule,
UiSpinnerModule,

View File

@@ -1,4 +1,4 @@
<shell-breadcrumb [key]="activatedProcessId$ | async" [includesTags]="['customer']"></shell-breadcrumb>
<shared-breadcrumb [key]="activatedProcessId$ | async" tags="customer"></shared-breadcrumb>
<div class="content-container">
<router-outlet></router-outlet>

View File

@@ -1,4 +1,4 @@
import { Component, ChangeDetectionStrategy } from '@angular/core';
import { Component, ChangeDetectionStrategy, OnInit } from '@angular/core';
import { ApplicationService } from '@core/application';
import { map } from 'rxjs/operators';
@@ -9,8 +9,12 @@ import { map } from 'rxjs/operators';
providers: [],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class PageCustomerComponent {
export class PageCustomerComponent implements OnInit {
activatedProcessId$ = this.application.activatedProcessId$.pipe(map((p) => String(p)));
constructor(public application: ApplicationService) {}
ngOnInit() {
this.application.setTitle('Kundensuche');
}
}

View File

@@ -3,7 +3,7 @@ import { CommonModule } from '@angular/common';
import { PageCustomerComponent } from './page-customer.component';
import { PageCustomerRoutingModule } from './page-customer-routing.module';
import { ShellBreadcrumbModule } from '@shell/breadcrumb';
import { BreadcrumbModule } from '@shared/components/breadcrumb';
import { CustomerSearchModule } from './customer-search/customer-search.module';
import { CustomerDetailsModule } from './customer-details/customer-details.module';
import { UiInputModule } from '@ui/input';
@@ -14,7 +14,7 @@ import { CustomerModalModuleModule } from './modals';
imports: [
CommonModule,
PageCustomerRoutingModule,
ShellBreadcrumbModule,
BreadcrumbModule,
CustomerSearchModule,
CustomerDetailsModule,
UiInputModule,

View File

@@ -1,7 +1,3 @@
:host {
@apply grid grid-flow-row grid-cols-1 gap-4 p-4;
@screen tablet {
@apply grid-cols-2;
}
@apply grid grid-flow-row grid-cols-2 gap-4 p-4;
}

View File

@@ -1,4 +1,5 @@
import { Component, OnInit } from '@angular/core';
import { ApplicationService } from '@core/application';
import { DomainDashboardService } from '@domain/isa';
import { of } from 'rxjs';
import { catchError, map } from 'rxjs/operators';
@@ -14,7 +15,12 @@ export class DashboardComponent implements OnInit {
catchError(() => of([]))
);
constructor(private readonly _domainIsaDashboardService: DomainDashboardService) {}
constructor(private readonly _domainIsaDashboardService: DomainDashboardService, private _app: ApplicationService) {}
ngOnInit(): void {}
ngOnInit(): void {
this._app.setSection('customer');
this._app.activateProcess(undefined);
this._app.setTitle('Dashboard');
}
}

View File

@@ -1,9 +1,5 @@
:host {
@apply grid grid-flow-row gap-4 bg-white rounded p-4 shadow relative;
@screen tablet {
@apply grid-flow-col grid-cols-2;
}
@apply grid grid-flow-col grid-cols-2 gap-4 bg-white rounded p-4 shadow relative;
}
.title {

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