RD fur customer bereich

This commit is contained in:
Lorenz Hilpert
2023-04-27 16:06:59 +02:00
parent 7b72532c9e
commit bf760677ef
69 changed files with 1175 additions and 19 deletions

View File

@@ -69,12 +69,12 @@ const routes: Routes = [
},
{
path: 'customer',
loadChildren: () => import('@page/customer').then((m) => m.PageCustomerModule),
loadChildren: () => import('@page/customer-rd').then((m) => m.CustomerModule),
canActivate: [CanActivateCustomerGuard],
},
{
path: ':processId/customer',
loadChildren: () => import('@page/customer').then((m) => m.PageCustomerModule),
loadChildren: () => import('@page/customer-rd').then((m) => m.CustomerModule),
canActivate: [CanActivateCustomerWithProcessIdGuard],
resolve: { processId: ProcessIdResolver },
},

View File

@@ -1,3 +1,4 @@
export const environment = {
production: true,
debug: false,
};

View File

@@ -4,6 +4,7 @@
export const environment = {
production: false,
debug: false,
};
/*

View File

@@ -14,26 +14,28 @@ if (environment.production) {
const debugService = new DebugService();
const consoleLog = console.log;
if (environment.debug) {
const consoleLog = console.log;
console.log = (...args) => {
debugService.add({ type: 'log', args });
consoleLog(...args);
};
console.log = (...args) => {
debugService.add({ type: 'log', args });
consoleLog(...args);
};
const consoleWarn = console.warn;
const consoleWarn = console.warn;
console.warn = (...args) => {
debugService.add({ type: 'warn', args });
consoleWarn(...args);
};
console.warn = (...args) => {
debugService.add({ type: 'warn', args });
consoleWarn(...args);
};
const consoleError = console.error;
const consoleError = console.error;
console.error = (...args) => {
debugService.add({ type: 'error', args });
consoleError(...args);
};
console.error = (...args) => {
debugService.add({ type: 'error', args });
consoleError(...args);
};
}
platformBrowserDynamic([{ provide: DebugService, useValue: debugService }])
.bootstrapModule(AppModule)

View File

@@ -0,0 +1,7 @@
{
"$schema": "../../../node_modules/ng-packagr/ng-package.schema.json",
"dest": "../../../dist/page/customer",
"lib": {
"entryFile": "src/public-api.ts"
}
}

View File

@@ -0,0 +1,3 @@
:host {
@apply grid grid-flow-row gap-[5px] h-[6.125rem] bg-surface text-surface-content overflow-hidden mb-px-2 px-4 py-3;
}

View File

@@ -0,0 +1,24 @@
<div class="flex items-start justify-between">
<div class="isa-label">
{{ label?.description }}
</div>
<div class="mr-20">
{{ customer?.created | date: 'dd.MM.yy' }}
</div>
</div>
<div class="grid grid-cols-2 gap-4">
<span class="text-[22px] font-bold"> {{ customer?.lastName }} {{ customer?.firstName }} </span>
<div>
<div class="flex flex-col">
<div class="flex flex-row items-center">
<span class="w-32">PLZ und Ort</span>
<span class="font-bold grow-1">{{ customer?.address?.zipCode }} {{ customer?.address?.city }}</span>
</div>
<div class="flex flex-row items-center">
<span class="w-32">E-Mail</span>
<span class="font-bold grow-1">{{ customer?.communicationDetails?.email }}</span>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,19 @@
import { Component, ChangeDetectionStrategy, Input } from '@angular/core';
import { CustomerInfoDTO } from '@swagger/crm';
@Component({
selector: 'page-customer-result-list-item-full',
templateUrl: 'customer-result-list-item-full.component.html',
styleUrls: ['customer-result-list-item-full.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CustomerResultListItemFullComponent {
get label() {
return this.customer?.features?.find((f) => f.enabled);
}
@Input()
customer: CustomerInfoDTO;
constructor() {}
}

View File

@@ -0,0 +1,3 @@
:host {
@apply flex flex-col bg-surface text-surface-content h-[11.313rem] mb-[0.625rem] p-4;
}

View File

@@ -0,0 +1,24 @@
<div class="flex items-start justify-between">
<div class="isa-label">
{{ label?.description }}
</div>
<div>
{{ customer?.created | date: 'dd.MM.yy' }}
</div>
</div>
<div class="flex flex-col justify-between grow">
<span class="text-[22px] font-bold"> {{ customer?.lastName }} {{ customer?.firstName }} </span>
<div>
<div class="flex flex-col">
<div class="flex flex-row items-center">
<span class="w-32">PLZ und Ort</span>
<span class="font-bold grow-1">{{ customer?.address?.zipCode }} {{ customer?.address?.city }}</span>
</div>
<div class="flex flex-row items-center">
<span class="w-32">E-Mail</span>
<span class="font-bold grow-1">{{ customer?.communicationDetails?.email }}</span>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,19 @@
import { Component, ChangeDetectionStrategy, Input } from '@angular/core';
import { CustomerInfoDTO } from '@swagger/crm';
@Component({
selector: 'page-customer-result-list-item',
templateUrl: 'customer-result-list-item.component.html',
styleUrls: ['customer-result-list-item.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CustomerResultListItemComponent {
get label() {
return this.customer?.features?.find((f) => f.enabled);
}
@Input()
customer: CustomerInfoDTO;
constructor() {}
}

View File

@@ -0,0 +1,17 @@
:host {
--shadow: 0px 0px 10px rgba(220, 226, 233, 0.5);
--border-radius: 0.313rem;
@apply grid grid-flow-row;
border-radius: var(--border-radius);
}
.customer-result-list-header {
box-shadow: var(--shadow);
border-radius: var(--border-radius) var(--border-radius) 0 0;
}
page-customer-result-list-item-full,
page-customer-result-list-item {
cursor: pointer;
border-radius: var(--border-radius);
}

View File

@@ -0,0 +1,36 @@
<div
class="customer-result-list-header bg-surface-2 text-surface-2-content flex flex-row justify-between pb-4"
[class.flex-row]="!compact"
[class.flex-col]="compact"
>
<div class="grid grid-flow-col gap-3 items-center justify-start grow-0">
<shared-searchbox class="w-[20.892rem]">
<input type="text" sharedSearchboxInput />
</shared-searchbox>
<button type="button" class="btn btn-light w-[5.813rem]">
Filter
</button>
</div>
<span class="mr-5 self-end text-sm" [class.mt-4]="compact">
xxx Treffer
</span>
</div>
<cdk-virtual-scroll-viewport itemSize="98" class="h-[calc(100vh-18.875rem)]" *ngIf="!compact">
<a
*cdkVirtualFor="let customer of customers$ | async; trackBy: trackByFn"
[routerLink]="customerSearchNavigation.detailsRoute({ processId: processId, customerId: customer.id })"
>
<page-customer-result-list-item-full [customer]="customer"></page-customer-result-list-item-full>
</a>
</cdk-virtual-scroll-viewport>
<cdk-virtual-scroll-viewport itemSize="98" class="h-[calc(100vh-20.225rem)]" *ngIf="compact">
<a
*cdkVirtualFor="let customer of customers$ | async; trackBy: trackByFn"
[routerLink]="customerSearchNavigation.detailsRoute({ processId: processId, customerId: customer.id })"
>
<page-customer-result-list-item [customer]="customer" (click)="select(customer)"></page-customer-result-list-item>
</a>
</cdk-virtual-scroll-viewport>

View File

@@ -0,0 +1,49 @@
import { Component, ChangeDetectionStrategy, Input, EventEmitter, Output } from '@angular/core';
import { CustomerInfoDTO } from '@swagger/crm';
import { CrmCustomerService } from '@domain/crm';
import { map } from 'rxjs/operators';
import { BooleanInput, NumberInput, coerceBooleanProperty, coerceNumberProperty } from '@angular/cdk/coercion';
import { CustomerSearchNavigation } from '../../navigations';
@Component({
selector: 'page-customer-result-list',
templateUrl: 'customer-result-list.component.html',
styleUrls: ['customer-result-list.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CustomerResultListComponent {
private _compact: boolean;
@Input()
get compact() {
return this._compact;
}
set compact(value: BooleanInput) {
this._compact = coerceBooleanProperty(value);
}
customers$ = this._customerService.getCustomers('Lorenz Hilpert').pipe(map((res) => res.result));
@Input()
selected: CustomerInfoDTO;
@Output()
selectedChange = new EventEmitter<CustomerInfoDTO>();
private _processId: NumberInput;
@Input()
get processId() {
return this._processId;
}
set processId(value: NumberInput) {
this._processId = coerceNumberProperty(value);
}
trackByFn = (_: number, item: CustomerInfoDTO) => item?.id;
constructor(private _customerService: CrmCustomerService, public customerSearchNavigation: CustomerSearchNavigation) {}
select(customer: CustomerInfoDTO) {
this.selected = customer;
this.selectedChange.emit(customer);
}
}

View File

@@ -0,0 +1,22 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ScrollingModule } from '@angular/cdk/scrolling';
import { SearchboxModule } from '@shared/components/searchbox';
import { CustomerResultListComponent } from './customer-result-list.component';
import { CustomerResultListItemFullComponent } from './customer-result-list-item-full/customer-result-list-item-full.component';
import { CustomerResultListItemComponent } from './customer-result-list-item/customer-result-list-item.component';
import { RouterModule } from '@angular/router';
@NgModule({
imports: [CommonModule, SearchboxModule, ScrollingModule, RouterModule],
exports: [CustomerResultListComponent, CustomerResultListComponent, CustomerResultListItemFullComponent, CustomerResultListItemComponent],
declarations: [
CustomerResultListComponent,
CustomerResultListComponent,
CustomerResultListItemFullComponent,
CustomerResultListItemComponent,
],
})
export class CustomerResultListModule {}

View File

View File

@@ -0,0 +1,3 @@
<shared-breadcrumb [key]="processId$ | async" [tags]="['customer']" class="mb-9"></shared-breadcrumb>
<router-outlet></router-outlet>

View File

@@ -0,0 +1,15 @@
import { Component, ChangeDetectionStrategy } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { map } from 'rxjs/operators';
@Component({
selector: 'page-customer',
templateUrl: 'customer-page.component.html',
styleUrls: ['customer-page.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CustomerComponent {
processId$ = this._activatedRoute.data.pipe(map((data) => data.processId));
constructor(private _activatedRoute: ActivatedRoute) {}
}

View File

@@ -0,0 +1,16 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { CustomerComponent } from './customer-page.component';
import { RouterModule } from '@angular/router';
import { routes } from './routes';
import { BreadcrumbModule } from '@shared/components/breadcrumb';
import { CustomerSearchModule } from './customer-search/customer-search.module';
@NgModule({
imports: [CommonModule, RouterModule.forChild(routes), BreadcrumbModule, CustomerSearchModule],
exports: [CustomerComponent],
declarations: [CustomerComponent],
})
export class CustomerModule {}

View File

@@ -0,0 +1,8 @@
<div class="grid grid-cols-[27.5rem_auto] max-h-[calc(100vh-13.875rem)] h-[calc(100vh-13.875rem)] gap-6">
<div *ngIf="showSide$ | async" [ngSwitch]="side$ | async">
<page-customer-results-side-view *ngSwitchCase="'results'"></page-customer-results-side-view>
</div>
<div [class.col-span-2]="hideSide$ | async" class="overflow-y-scroll">
<router-outlet></router-outlet>
</div>
</div>

View File

@@ -0,0 +1,108 @@
import { Component, ChangeDetectionStrategy, OnInit, OnDestroy } from '@angular/core';
import { ActivatedRoute, NavigationEnd, Router } from '@angular/router';
import { combineLatest, BehaviorSubject, Subscription } from 'rxjs';
import { map } from 'rxjs/operators';
import { BreakpointObserver } from '@angular/cdk/layout';
import { CustomerSearchStore } from './store/customer-search.store';
@Component({
selector: 'page-customer-search',
templateUrl: 'customer-search.component.html',
styleUrls: ['customer-search.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [CustomerSearchStore],
})
export class CustomerSearchComponent implements OnInit, OnDestroy {
isTablet$ = this._breakpointObserver.observe('(max-width: 1024px)').pipe(map((result) => result.matches));
side$ = new BehaviorSubject<string | undefined>(undefined);
get side() {
return this.side$.value;
}
showSide$ = combineLatest([this.isTablet$, this.side$]).pipe(map(([isTablet, side]) => !isTablet && side));
hideSide$ = this.showSide$.pipe(map((showSide) => !showSide));
get snapshot() {
return this._activatedRoute.snapshot;
}
get parentSnapshot() {
return this._activatedRoute.parent?.snapshot;
}
get firstChildSnapshot() {
return this._activatedRoute.firstChild?.snapshot;
}
private _eventsSubscription: Subscription;
constructor(
private _store: CustomerSearchStore,
private _activatedRoute: ActivatedRoute,
private _breakpointObserver: BreakpointObserver,
private _router: Router
) {}
ngOnInit(): void {
this.checkAndUpdateProcessId();
this.checkAndUpdateSide();
this.checkAndUpdateCustomerId();
this._eventsSubscription = this._router.events.subscribe((event) => {
if (event instanceof NavigationEnd) {
this.checkAndUpdateProcessId();
this.checkAndUpdateSide();
this.checkAndUpdateCustomerId();
}
});
}
ngOnDestroy(): void {
this._eventsSubscription.unsubscribe();
}
checkAndUpdateProcessId() {
let processId: number;
processId = this.snapshot.data?.processId;
if (!processId) {
processId = this.parentSnapshot?.data?.processId;
}
if (processId !== this._store.processId) {
this._store.setProcessId(processId);
}
}
checkAndUpdateSide() {
let side: string;
side = this.snapshot.data?.side;
if (!side) {
side = this.firstChildSnapshot.data?.side;
}
if (side !== this.side) {
this.side$.next(side);
}
}
checkAndUpdateCustomerId() {
let customerId: number;
customerId = this.snapshot.params.customerId;
if (!customerId) {
customerId = this.firstChildSnapshot?.params.customerId;
}
if (customerId !== this._store.customer?.id) {
this._store.selectCustomer(customerId);
}
}
}

View File

@@ -0,0 +1,23 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { CustomerSearchComponent } from './customer-search.component';
import { CustomerResultsSideViewModule } from './results-side-view/results-side-view.module';
import { RouterModule } from '@angular/router';
import { CustomerResultsMainViewModule } from './results-main-view/results-main-view.module';
import { CustomerDetailsViewMainModule } from './details-main-view/details-main-view.module';
import { CustomerHistoryMainViewModule } from './history-main-view/history-main-view.module';
@NgModule({
imports: [
CommonModule,
RouterModule,
CustomerResultsSideViewModule,
CustomerResultsMainViewModule,
CustomerDetailsViewMainModule,
CustomerHistoryMainViewModule,
],
exports: [CustomerSearchComponent],
declarations: [CustomerSearchComponent],
})
export class CustomerSearchModule {}

View File

@@ -0,0 +1,19 @@
:host {
@apply block bg-surface text-surface-content rounded-[0.313rem] mb-3;
}
.data-label {
@apply w-[10.75rem];
}
.data-value {
@apply grow font-bold;
}
.customer-details-customer-main-row {
@apply px-5 py-3 bg-surface text-surface-content border-t-2 border-solid border-surface-2 flex flex-row items-center;
}
.customer-details-customer-main-row .data-label {
@apply w-[6.875rem];
}

View File

@@ -0,0 +1,92 @@
<ng-container *ngIf="fetching$ | async; else customerTemplate"></ng-container>
<ng-template #customerTemplate>
<div class="customer-details-header grid grid-flow-row pt-1 px-1 pb-6">
<div class="customer-details-header-actions flex flex-row justify-end pt-1 px-1">
<a *ngIf="historyRoute$ | async; let historyRoute" class="btn btn-label font-bold text-brand" [routerLink]="historyRoute">Historie</a>
</div>
<div class="customer-details-header-body text-center -mt-3">
<h1 class="text-[1.625rem] font-bold">Kundendetails</h1>
<p>Sind Ihre Kundendaten korrekt?</p>
</div>
</div>
<div class="customer-details-customer-type flex flex-row justify-between items-center bg-surface-2 text-surface-2-content">
<div class="pl-4 font-bold">{{ customerType$ | async }}</div>
<button class="btn btn-label font-bold text-brand" type="button">Bearbeiten</button>
</div>
<div class="customer-details-customer-main-data px-5 py-3 grid grid-flow-row gap-3">
<div class="flex flex-row">
<div class="data-label">Erstellungsdatum</div>
<div class="data-value">{{ created$ | async | date: 'dd.MM.yyyy' }} | {{ created$ | async | date: 'hh:mm' }} Uhr</div>
</div>
<div class="flex flex-row">
<div class="data-label">Kundennummer</div>
<div class="data-value">{{ customerNumber$ | async }}</div>
</div>
<div class="flex flex-row" *ngIf="customerNumberDig$ | async; let customerNumberDig">
<div class="data-label">Kundennummer-DIG</div>
<div class="data-value">{{ customerNumberDig }}</div>
</div>
</div>
<div class="customer-details-customer-main-row">
<div class="data-label">Anrede</div>
<div class="data-value">{{ gender$ | async }}</div>
</div>
<div class="customer-details-customer-main-row">
<div class="data-label">Titel</div>
<div class="data-value">{{ title$ | async }}</div>
</div>
<div class="customer-details-customer-main-row">
<div class="data-label">Nachname</div>
<div class="data-value">{{ lastName$ | async }}</div>
</div>
<div class="customer-details-customer-main-row">
<div class="data-label">Vorname</div>
<div class="data-value">{{ firstName$ | async }}</div>
</div>
<div class="customer-details-customer-main-row">
<div class="data-label">E-Mail</div>
<div class="data-value">{{ email$ | async }}</div>
</div>
<div class="customer-details-customer-main-row">
<div class="data-label">Straße</div>
<div class="data-value">{{ street$ | async }}</div>
</div>
<div class="customer-details-customer-main-row">
<div class="data-label">Hausnr.</div>
<div class="data-value">{{ streetNumber$ | async }}</div>
</div>
<div class="customer-details-customer-main-row">
<div class="data-label">PLZ</div>
<div class="data-value">{{ zipCode$ | async }}</div>
</div>
<div class="customer-details-customer-main-row">
<div class="data-label">Ort</div>
<div class="data-value">{{ city$ | async }}</div>
</div>
<div class="customer-details-customer-main-row">
<div class="data-label">Adresszusatz</div>
<div class="data-value">{{ info$ | async }}</div>
</div>
<div class="customer-details-customer-main-row">
<div class="data-label">Land</div>
<div class="data-value">{{ country$ | async | country }}</div>
</div>
<div class="customer-details-customer-main-row">
<div class="data-label">Festnetznr.</div>
<div class="data-value">{{ landline$ | async }}</div>
</div>
<div class="customer-details-customer-main-row">
<div class="data-label">Mobilnr.</div>
<div class="data-value">{{ mobile$ | async }}</div>
</div>
<div class="customer-details-customer-main-row">
<div class="data-label">Abteilung</div>
<div class="data-value">{{ department$ | async }}</div>
</div>
<div class="customer-details-customer-main-row">
<div class="data-label">USt-ID</div>
<div class="data-value">{{ vatId$ | async }}</div>
</div>
</ng-template>

View File

@@ -0,0 +1,68 @@
import { Component, ChangeDetectionStrategy } from '@angular/core';
import { combineLatest } from 'rxjs';
import { map } from 'rxjs/operators';
import { CustomerSearchNavigation } from '../../navigations';
import { CustomerSearchStore } from '../store';
const GENDER_MAP = {
2: 'Herr',
4: 'Frau',
};
@Component({
selector: 'page-customer-details-main-view',
templateUrl: 'details-main-view.component.html',
styleUrls: ['details-main-view.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CustomerDetailsViewMainComponent {
fetching$ = this._store.fetchingCustomer$;
processId$ = this._store.processId$;
customerId$ = this._store.customerId$;
historyRoute$ = combineLatest([this.processId$, this.customerId$]).pipe(
map(([processId, customerId]) => this._navigation.historyRoute({ processId, customerId }))
);
customerType$ = this._store.select((s) => s.customer?.features?.find((f) => f.enabled)?.description);
created$ = this._store.select((s) => s.customer?.created);
customerNumber$ = this._store.select((s) => s.customer?.customerNumber);
customerNumberDig$ = this._store.select((s) => s.customer?.linkedRecords?.find((r) => r.repository === 'dig')?.number);
gender$ = this._store.select((s) => GENDER_MAP[s.customer?.gender]);
title$ = this._store.select((s) => s.customer?.title);
lastName$ = this._store.select((s) => s.customer?.lastName);
firstName$ = this._store.select((s) => s.customer?.firstName);
email$ = this._store.select((s) => s.customer?.communicationDetails?.email);
street$ = this._store.select((s) => s.customer?.address?.street);
streetNumber$ = this._store.select((s) => s.customer?.address?.streetNumber);
zipCode$ = this._store.select((s) => s.customer?.address?.zipCode);
city$ = this._store.select((s) => s.customer?.address?.city);
country$ = this._store.select((s) => s.customer?.address?.country);
info$ = this._store.select((s) => s.customer?.address?.info);
landline$ = this._store.select((s) => s.customer?.communicationDetails?.phone);
mobile$ = this._store.select((s) => s.customer?.communicationDetails?.mobile);
department$ = this._store.select((s) => s.customer?.organisation?.department);
vatId$ = this._store.select((s) => s.customer?.organisation?.vatId);
constructor(private _store: CustomerSearchStore, private _navigation: CustomerSearchNavigation) {}
}

View File

@@ -0,0 +1,13 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { CustomerDetailsViewMainComponent } from './details-main-view.component';
import { CountryPipe } from '@shared/pipes/country';
import { RouterModule } from '@angular/router';
@NgModule({
imports: [CommonModule, CountryPipe, RouterModule],
exports: [CustomerDetailsViewMainComponent],
declarations: [CustomerDetailsViewMainComponent],
})
export class CustomerDetailsViewMainModule {}

View File

@@ -0,0 +1,7 @@
:host {
@apply block bg-surface text-surface-content rounded-[0.313rem] mb-3;
}
::ng-deep page-customer-history-main-view shared-history-list .scroll-container {
@apply h-[calc(100vh-24rem)];
}

View File

@@ -0,0 +1,29 @@
<ng-container *ngIf="fetching$ | async; else historyTemplate"></ng-container>
<ng-template #historyTemplate>
<div>
<div class="customer-history-header">
<div class="customer-history-header-actions flex flex-row justify-end pt-1 px-1">
<a [routerLink]="detailsRoute$ | async" class="btn btn-label">
<ui-icon [icon]="'close'"></ui-icon>
</a>
</div>
<div class="customer-history-header-body text-center -mt-3">
<h1 class="text-[1.625rem] font-bold">Historie</h1>
</div>
<div class="customer-history-header-info flex flex-row justify-evenly items-center my-5">
<div class="flex flex-row">
<div class="w-36">Kundenname</div>
<div class="grow font-bold">{{ customerName$ | async }}</div>
</div>
<div class="flex flex-row">
<div class="w-36">Kundennummer</div>
<div class="grow font-bold">{{ customerNumber$ | async }}</div>
</div>
</div>
</div>
<div class="px-3 bg-surface-2 text-surface-2-content">
<shared-history-list [history]="history$ | async"> </shared-history-list>
</div>
</div>
</ng-template>

View File

@@ -0,0 +1,76 @@
import { coerceNumberProperty } from '@angular/cdk/coercion';
import { Component, ChangeDetectionStrategy, AfterViewInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { CrmCustomerService } from '@domain/crm';
import { ComponentStore, tapResponse } from '@ngrx/component-store';
import { HistoryDTO } from '@swagger/crm';
import { Observable, combineLatest } from 'rxjs';
import { map, switchMap, tap } from 'rxjs/operators';
import { CustomerSearchStore } from '../store';
import { CustomerSearchNavigation } from '../../navigations';
export interface CustomerHistoryViewMainState {
history?: HistoryDTO[];
fetching?: boolean;
}
@Component({
selector: 'page-customer-history-main-view',
templateUrl: 'history-main-view.component.html',
styleUrls: ['history-main-view.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CustomerHistoryMainViewComponent extends ComponentStore<CustomerHistoryViewMainState> implements AfterViewInit {
fetching$ = this.select((s) => s.fetching);
history$ = this.select((s) => s.history);
processId$ = this._store.processId$;
customerId$ = this._store.customerId$;
customer$ = this._store.customer$;
detailsRoute$ = combineLatest([this.processId$, this.customerId$]).pipe(
map(([processId, customerId]) => this._navigation.detailsRoute({ processId, customerId }))
);
customerName$ = this.customer$.pipe(map((customer) => `${customer?.lastName}, ${customer?.firstName}`));
customerNumber$ = this.customer$.pipe(map((customer) => customer?.customerNumber));
constructor(
private _store: CustomerSearchStore,
private _customerService: CrmCustomerService,
private _navigation: CustomerSearchNavigation
) {
super({});
}
ngAfterViewInit(): void {
this.fetchHistory(this.customerId$);
}
fetchHistory = this.effect((customerId$: Observable<number>) =>
customerId$.pipe(
tap(() => this.patchState({ fetching: true })),
switchMap((customerId) =>
this._customerService
.getCustomerHistory(customerId)
.pipe(tapResponse(this.handleFetchHistoryResponse, this.handleFetchHistoryError, this.handleFetchHistoryComplete))
)
)
);
handleFetchHistoryResponse = (history: HistoryDTO[]) => {
this.patchState({ history });
};
handleFetchHistoryError = (err: any) => {
console.error(err);
};
handleFetchHistoryComplete = () => {
this.patchState({ fetching: false });
};
}

View File

@@ -0,0 +1,14 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { SharedHistoryListModule } from '@shared/components/history';
import { CustomerHistoryMainViewComponent } from './history-main-view.component';
import { UiIconModule } from '@ui/icon';
import { RouterModule } from '@angular/router';
@NgModule({
imports: [CommonModule, RouterModule, SharedHistoryListModule, UiIconModule],
exports: [CustomerHistoryMainViewComponent],
declarations: [CustomerHistoryMainViewComponent],
})
export class CustomerHistoryMainViewModule {}

View File

@@ -0,0 +1 @@
<page-customer-result-list [processId]="processId$ | async"> </page-customer-result-list>

View File

@@ -0,0 +1,17 @@
import { NumberInput } from '@angular/cdk/coercion';
import { Component, ChangeDetectionStrategy, Input } from '@angular/core';
import { Router } from '@angular/router';
import { CustomerInfoDTO } from '@swagger/crm';
import { CustomerSearchStore } from '../store/customer-search.store';
@Component({
selector: 'page-customer-results-main-view',
templateUrl: 'results-main-view.component.html',
styleUrls: ['results-main-view.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CustomerResultsMainViewComponent {
processId$ = this._store.processId$;
constructor(private _store: CustomerSearchStore) {}
}

View File

@@ -0,0 +1,12 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { CustomerResultsMainViewComponent } from './results-main-view.component';
import { CustomerResultListModule } from '../../components/customer-result-list/customer-result-list.module';
@NgModule({
imports: [CommonModule, CustomerResultListModule],
exports: [CustomerResultsMainViewComponent],
declarations: [CustomerResultsMainViewComponent],
})
export class CustomerResultsMainViewModule {}

View File

@@ -0,0 +1 @@
<page-customer-result-list [processId]="processId$ | async" compact="true"> </page-customer-result-list>

View File

@@ -0,0 +1,17 @@
import { NumberInput } from '@angular/cdk/coercion';
import { Component, ChangeDetectionStrategy, Input } from '@angular/core';
import { Router } from '@angular/router';
import { CustomerInfoDTO } from '@swagger/crm';
import { CustomerSearchStore } from '../store/customer-search.store';
@Component({
selector: 'page-customer-results-side-view',
templateUrl: 'results-side-view.component.html',
styleUrls: ['results-side-view.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CustomerResultsSideViewComponent {
processId$ = this._store.processId$;
constructor(private _store: CustomerSearchStore) {}
}

View File

@@ -0,0 +1,12 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { CustomerResultsSideViewComponent } from './results-side-view.component';
import { CustomerResultListModule } from '../../components/customer-result-list/customer-result-list.module';
@NgModule({
imports: [CommonModule, CustomerResultListModule],
exports: [CustomerResultsSideViewComponent],
declarations: [CustomerResultsSideViewComponent],
})
export class CustomerResultsSideViewModule {}

View File

@@ -0,0 +1,7 @@
import { CustomerDTO } from '@swagger/crm';
export interface CustomerSearchState {
processId?: number;
customer?: CustomerDTO;
fetchingCustomer?: boolean;
}

View File

@@ -0,0 +1,66 @@
import { ComponentStore, tapResponse } from '@ngrx/component-store';
import { CustomerSearchState } from './customer-search.state';
import * as S from './selectors';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { distinctUntilChanged, switchMap, tap } from 'rxjs/operators';
import { CrmCustomerService } from '@domain/crm';
import { Result } from '@domain/defs';
import { CustomerDTO } from '@swagger/crm';
@Injectable()
export class CustomerSearchStore extends ComponentStore<CustomerSearchState> {
get processId() {
return this.get(S.selectProcessId);
}
processId$ = this.select(S.selectProcessId);
get fetchingCustomer() {
return this.get(S.selectFetchingCustomer);
}
fetchingCustomer$ = this.select(S.selectFetchingCustomer);
get customerId() {
return this.get(S.selectCustomerId);
}
customerId$ = this.select(S.selectCustomerId);
get customer() {
return this.get(S.selectCustomer);
}
customer$ = this.select(S.selectCustomer);
constructor(private _customerService: CrmCustomerService) {
super({});
}
setProcessId = this.updater((state, processId: number) => ({ ...state, processId }));
selectCustomer = this.effect((customerId$: Observable<number>) =>
customerId$.pipe(
distinctUntilChanged(),
tap((custoemrId) => this.patchState({ fetchingCustomer: true, customer: { id: custoemrId } })),
switchMap((customerId) =>
this._customerService
.getCustomer(customerId)
.pipe(tapResponse(this.handleSelectCustomerResponse, this.handleSelectCustomerError, this.handleSelectCustomerComplete))
)
)
);
handleSelectCustomerResponse = (result: Result<CustomerDTO>) => {
this.patchState({ customer: result.result });
};
handleSelectCustomerError = (err: any) => {
console.error(err);
};
handleSelectCustomerComplete = () => {
this.patchState({ fetchingCustomer: false });
};
}

View File

@@ -0,0 +1,3 @@
export * from './customer-search.state';
export * from './customer-search.store';
export * from './selectors';

View File

@@ -0,0 +1,18 @@
import { coerceNumberProperty } from '@angular/cdk/coercion';
import { CustomerSearchState } from './customer-search.state';
export function selectProcessId(s: CustomerSearchState): number {
return coerceNumberProperty(s.processId);
}
export function selectFetchingCustomer(s: CustomerSearchState) {
return s.fetchingCustomer;
}
export function selectCustomer(s: CustomerSearchState) {
return s.customer;
}
export function selectCustomerId(s: CustomerSearchState) {
return selectCustomer(s)?.id;
}

View File

@@ -0,0 +1,24 @@
import { NumberInput, coerceNumberProperty } from '@angular/cdk/coercion';
import { Injectable } from '@angular/core';
import { Router } from '@angular/router';
@Injectable({ providedIn: 'root' })
export class CustomerSearchNavigation {
constructor(private _router: Router) {}
detailsRoute(params: { processId: NumberInput; customerId: NumberInput }): any[] {
return ['/kunde', coerceNumberProperty(params.processId), 'customer', coerceNumberProperty(params.customerId)];
}
navigateToDetails(params: { processId: NumberInput; customerId: NumberInput }): Promise<boolean> {
return this._router.navigate(this.detailsRoute(params));
}
historyRoute(params: { processId: NumberInput; customerId: NumberInput }): any[] {
return ['/kunde', coerceNumberProperty(params.processId), 'customer', coerceNumberProperty(params.customerId), 'history'];
}
navigateToHistory(params: { processId: NumberInput; customerId: NumberInput }): Promise<boolean> {
return this._router.navigate(this.historyRoute(params));
}
}

View File

@@ -0,0 +1 @@
export * from './customer-search.navigation';

View File

@@ -0,0 +1,26 @@
import { Routes } from '@angular/router';
import { CustomerComponent } from './customer-page.component';
import { CustomerSearchComponent } from './customer-search/customer-search.component';
import { CustomerResultsMainViewComponent } from './customer-search/results-main-view/results-main-view.component';
import { CustomerDetailsViewMainComponent } from './customer-search/details-main-view/details-main-view.component';
import { CustomerHistoryMainViewComponent } from './customer-search/history-main-view/history-main-view.component';
export const routes: Routes = [
{
path: '',
component: CustomerComponent,
children: [
{
path: '',
component: CustomerSearchComponent,
children: [
{ path: '', component: CustomerResultsMainViewComponent },
{ path: ':customerId', component: CustomerDetailsViewMainComponent, data: { side: 'results' } },
{ path: ':customerId/history', component: CustomerHistoryMainViewComponent, data: { side: 'results' } },
// { path: ':customerId/edit', component: CustomerSearchComponent, data: { side: 'results' } },
// { path: ':customerId/orders', component: CustomerSearchComponent, data: { side: 'orderItems' } },
],
},
],
},
];

View File

@@ -0,0 +1,2 @@
export * from './lib/customer-page.module';
export * from './lib/customer-page.component';

View File

@@ -0,0 +1,6 @@
{
"$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json",
"lib": {
"entryFile": "src/public-api.ts"
}
}

View File

@@ -0,0 +1,5 @@
import { Observable } from 'rxjs';
export interface AutocompleteSource<T> {
complete(searchText: string): T[] | Promise<T[]> | Observable<T[]>;
}

View File

@@ -0,0 +1,75 @@
import { Directive, EventEmitter, HostBinding, HostListener, Input, Output } from '@angular/core';
import { ControlValueAccessor } from '@angular/forms';
import { BehaviorSubject } from 'rxjs';
@Directive({ selector: 'input[type=text][sharedSearchboxInput]' })
export class SearchboxInputDirective implements ControlValueAccessor {
value$ = new BehaviorSubject<string>('');
@Input()
@HostBinding('value')
value: string = '';
@Output()
valueChange = new EventEmitter<string>();
@Input()
@HostBinding('disabled')
disabled: boolean;
onChange = (_: string) => {};
onTouched = () => {};
onSearch = () => {};
constructor() {}
writeValue(obj: any): void {
this.value = obj ?? '';
this.value$.next(this.value);
}
registerOnChange(fn: any): void {
this.onChange = fn;
}
registerOnTouched(fn: any): void {
this.onChange = fn;
}
regsiterOnSearch(fn: any): void {
this.onSearch = fn;
}
setDisabledState?(isDisabled: boolean): void {
this.disabled = isDisabled;
}
@HostListener('input', ['$event.target.value'])
onInput(value: string) {
this.value = value;
this.value$.next(value);
this.valueChange.emit(value);
this.onChange(value);
this.onTouched();
}
@HostListener('blur')
onBlur() {
this.onTouched();
}
clear() {
this.value = '';
this.value$.next('');
this.valueChange.emit('');
this.onChange('');
this.onTouched();
}
@HostListener('keydown.enter')
onEnter() {
this.onSearch();
}
}

View File

@@ -0,0 +1,19 @@
.shared-searchbox {
@apply inline-block;
}
.shared-searchbox-input-wrapper {
@apply bg-surface text-surface-content flex flex-row items-center rounded-button;
}
input[sharedSearchboxInput] {
@apply bg-transparent text-surface-content text-lg font-bold h-12 px-4 outline-transparent grow;
}
.shared-searchbox-input-clear {
@apply text-surface-content h-12 w-12 outline-transparent inline-grid justify-center items-center grow-0;
}
.shared-searchbox-input-search {
@apply text-white h-12 w-12 outline-transparent inline-grid justify-center items-center bg-brand rounded-button grow-0;
}

View File

@@ -0,0 +1,15 @@
<div class="shared-searchbox-input-wrapper">
<ng-content select="[sharedSearchboxInput]"></ng-content>
<span class="shared-searchbox-input-warning"></span>
<button class="shared-searchbox-input-clear" *ngIf="input.value$ | async" type="button" (click)="input.clear()">
<ui-svg-icon icon="close" [size]="24"></ui-svg-icon>
</button>
<button
class="shared-searchbox-input-search"
type="button"
(keypress.enter)="search.emit(input.value)"
(click)="search.emit(input.value)"
>
<ui-icon icon="search"></ui-icon>
</button>
</div>

View File

@@ -0,0 +1,39 @@
import {
Component,
ChangeDetectionStrategy,
Output,
EventEmitter,
ViewEncapsulation,
ContentChild,
AfterContentInit,
Input,
} from '@angular/core';
import { SearchboxInputDirective } from './searchbox-input.directive';
import { AutocompleteSource } from './autocomplete-source';
@Component({
selector: 'shared-searchbox',
templateUrl: 'searchbox.component.html',
styleUrls: ['searchbox.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None,
host: {
class: 'shared-searchbox',
},
})
export class SearchboxComponent implements AfterContentInit {
@Output()
search = new EventEmitter<string>();
@ContentChild(SearchboxInputDirective, { static: true })
input: SearchboxInputDirective;
@Input()
autocompleteSource: AutocompleteSource<any>;
constructor() {}
ngAfterContentInit(): void {
this.input.regsiterOnSearch((value) => this.search.emit(value));
}
}

View File

@@ -0,0 +1,13 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { SearchboxComponent } from './searchbox.component';
import { SearchboxInputDirective } from './searchbox-input.directive';
import { UiIconModule } from '@ui/icon';
@NgModule({
imports: [CommonModule, UiIconModule],
exports: [SearchboxComponent, SearchboxInputDirective],
declarations: [SearchboxComponent, SearchboxInputDirective],
})
export class SearchboxModule {}

View File

@@ -0,0 +1,2 @@
export * from './lib/searchbox.component';
export * from './lib/searchbox.module';

View File

@@ -0,0 +1,6 @@
{
"$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json",
"lib": {
"entryFile": "src/public-api.ts"
}
}

View File

@@ -0,0 +1,40 @@
import { ChangeDetectorRef, OnDestroy, Pipe, PipeTransform } from '@angular/core';
import { CrmCustomerService } from '@domain/crm';
import { BehaviorSubject, combineLatest } from 'rxjs';
import { map } from 'rxjs/operators';
@Pipe({
name: 'country',
standalone: true,
pure: false,
})
export class CountryPipe implements PipeTransform, OnDestroy {
private result: string;
private value$ = new BehaviorSubject<string>(undefined);
countries$ = this.customerService.getCountries().pipe(map((res) => res.result));
subscriptions = combineLatest([this.value$, this.countries$]).subscribe(([value, countries]) => {
if (!!value && countries?.length > 0) {
const country = countries.find((c) => c.isO3166_A_3 === value);
if (country && this.result !== country.name) {
this.result = String(country.name);
this.cdr.markForCheck();
}
}
});
constructor(private customerService: CrmCustomerService, private cdr: ChangeDetectorRef) {}
ngOnDestroy() {
this.subscriptions.unsubscribe();
}
transform(value: string): any {
if (this.value$.value !== value) {
this.value$.next(value);
}
return this.result;
}
}

View File

@@ -0,0 +1 @@
export * from './lib/country.pipe';

View File

@@ -4,6 +4,6 @@
[class.translate-x-0]="sideMenuOpen$ | async"
></shell-side-menu>
<shell-process-bar class="fixed z-[149] top-[4.625rem] inset-x-0 desktop:left-[10.5rem]"></shell-process-bar>
<main class="ml-0 desktop:ml-[10.5rem] p-4" (click)="closeSideMenu()">
<main class="ml-0 desktop:ml-[10.5rem] pt-2 px-6" (click)="closeSideMenu()">
<ng-content></ng-content>
</main>

View File

@@ -23,6 +23,11 @@ module.exports = plugin(function ({ addComponents, theme, addBase, addUtilities
'--btn-background-color': theme('colors.button.light.DEFAULT'),
'--btn-color': theme('colors.button.light.content'),
},
'.btn-label': {
'--btn-background-color': 'transparent',
'--btn-hover-background-color': 'transparent',
'--btn-hover-color': 'inherit',
},
});
for (const key in theme('colors.accent')) {

View File

@@ -97,12 +97,16 @@ module.exports = {
'accent-darkblue': '#557596',
background: {
DEFAULT: '#F5F5F5',
DEFAULT: '#EDEFF0',
content: '#000000',
},
surface: {
DEFAULT: '#ffffff',
content: '#000000',
2: {
DEFAULT: '#F5F7FA',
content: '#000000',
},
},
components: {
menu: {
@@ -170,6 +174,7 @@ module.exports = {
card: '0px -2px 24px 0px rgba(220, 226, 233, 0.8)',
cta: '0px 0px 15px 0px rgba(0, 0, 0, 0.5)',
action: '0 0 20px 0 #596470',
s: '0px 6px 24px rgba(206, 212, 219, 0.8)',
},
borderRadius: {
button: '5px',