diff --git a/src/app/extensions/return-request/components/return-request-items/return-request-items.component.html b/src/app/extensions/return-request/components/return-request-items/return-request-items.component.html new file mode 100644 index 0000000000..bc9017a3bd --- /dev/null +++ b/src/app/extensions/return-request/components/return-request-items/return-request-items.component.html @@ -0,0 +1,41 @@ + + +

+ {{ 'toolineo.account.return_overview.tab.content.order_from' | translate : { '0': id } }} + {{ groupOrders[id][0].creationDate | ishDate }} +

+ + + + + + + + + + + + + + + + + + + + + + + +
{{ 'toolineo.account.return_overview.tab.content.column.return' | translate }}{{ 'toolineo.account.return_overview.tab.content.column.dealer' | translate }} + {{ 'toolineo.account.return_overview.tab.content.column.created_on' | translate }} + {{ 'toolineo.account.return_overview.tab.content.column.article' | translate }}{{ 'toolineo.account.return_overview.tab.content.column.status' | translate }} 
+ {{ item.productNumber }} + {{ item.supplierProductNumber }}{{ item.creationDate | ishDate }}{{ item.quantity }}{{ item.status }}{{ 'toolineo.account.return_overview.tab.content.download_return_label' | translate }}
+
+
+
+ + +

{{ 'toolineo.account.return_overview.no_result_found' | translate }}

+
diff --git a/src/app/extensions/return-request/components/return-request-items/return-request-items.component.spec.ts b/src/app/extensions/return-request/components/return-request-items/return-request-items.component.spec.ts new file mode 100644 index 0000000000..2a36e39290 --- /dev/null +++ b/src/app/extensions/return-request/components/return-request-items/return-request-items.component.spec.ts @@ -0,0 +1,29 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { TranslateModule } from '@ngx-translate/core'; + +import { ReturnRequestItemsComponent } from './return-request-items.component'; + +describe('Return Request Items Component', () => { + let component: ReturnRequestItemsComponent; + let fixture: ComponentFixture; + let element: HTMLElement; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [TranslateModule.forRoot()], + declarations: [ReturnRequestItemsComponent], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ReturnRequestItemsComponent); + component = fixture.componentInstance; + element = fixture.nativeElement; + }); + + it('should be created', () => { + expect(component).toBeTruthy(); + expect(element).toBeTruthy(); + expect(() => fixture.detectChanges()).not.toThrow(); + }); +}); diff --git a/src/app/extensions/return-request/components/return-request-items/return-request-items.component.ts b/src/app/extensions/return-request/components/return-request-items/return-request-items.component.ts new file mode 100644 index 0000000000..f7d6c60d49 --- /dev/null +++ b/src/app/extensions/return-request/components/return-request-items/return-request-items.component.ts @@ -0,0 +1,25 @@ +import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; + +import { ReturnRequest } from '../../models/return-request/return-request.model'; + +@Component({ + selector: 'ish-return-request-items', + templateUrl: './return-request-items.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ReturnRequestItemsComponent implements OnInit { + @Input() orders: ReturnRequest[] = []; + + groupOrders: { [key: string]: ReturnRequest[] }; + + orderIds: string[]; + + ngOnInit() { + this.groupOrders = this.orders.reduce<{ [key: string]: ReturnRequest[] }>((acc, nxt) => { + acc[nxt.orderId] = acc[nxt.orderId] ? [...acc[nxt.orderId], nxt] : [nxt]; + return acc; + }, {}); + + this.orderIds = Object.keys(this.groupOrders); + } +} diff --git a/src/app/extensions/return-request/components/return-request-modal/return-request-modal.component.html b/src/app/extensions/return-request/components/return-request-modal/return-request-modal.component.html new file mode 100644 index 0000000000..2905fc0bfb --- /dev/null +++ b/src/app/extensions/return-request/components/return-request-modal/return-request-modal.component.html @@ -0,0 +1,66 @@ + + + +
+
+ {{ 'toolineo.account.return_request.modal.orde_date' | translate }} + {{ order.creationDate | ishDate }} +
+
+ {{ 'toolineo.account.return_request.modal.orde_number' | translate }} + {{ order.documentNo }} +
+
+
+ + +
+ + +
+
+
+
+

{{ 'toolineo.account.return_request.modal.summary.title' | translate }}

+ {{ selectedQuantity }} {{ 'toolineo.account.return_request.modal.summary.items' | translate }} + +
+
+
+ +
+
+
+ + +

No Returnable Items found

+
diff --git a/src/app/extensions/return-request/components/return-request-modal/return-request-modal.component.spec.ts b/src/app/extensions/return-request/components/return-request-modal/return-request-modal.component.spec.ts new file mode 100644 index 0000000000..71281829a5 --- /dev/null +++ b/src/app/extensions/return-request/components/return-request-modal/return-request-modal.component.spec.ts @@ -0,0 +1,31 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { instance, mock } from 'ts-mockito'; + +import { ReturnRequestFacade } from '../../facades/return-request.facade'; + +import { ReturnRequestModalComponent } from './return-request-modal.component'; + +describe('Return Request Modal Component', () => { + let component: ReturnRequestModalComponent; + let fixture: ComponentFixture; + let element: HTMLElement; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ReturnRequestModalComponent], + providers: [{ provide: ReturnRequestFacade, useFactory: () => instance(mock(ReturnRequestFacade)) }], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ReturnRequestModalComponent); + component = fixture.componentInstance; + element = fixture.nativeElement; + }); + + it('should be created', () => { + expect(component).toBeTruthy(); + expect(element).toBeTruthy(); + expect(() => fixture.detectChanges()).not.toThrow(); + }); +}); diff --git a/src/app/extensions/return-request/components/return-request-modal/return-request-modal.component.ts b/src/app/extensions/return-request/components/return-request-modal/return-request-modal.component.ts new file mode 100644 index 0000000000..9f496c39dd --- /dev/null +++ b/src/app/extensions/return-request/components/return-request-modal/return-request-modal.component.ts @@ -0,0 +1,114 @@ +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + EventEmitter, + Input, + OnChanges, + OnInit, + Output, + ViewChild, +} from '@angular/core'; +import { FormControl, FormGroup, UntypedFormGroup } from '@angular/forms'; +import { Observable } from 'rxjs'; + +import { Order } from 'ish-core/models/order/order.model'; +import { ModalDialogComponent } from 'ish-shared/components/common/modal-dialog/modal-dialog.component'; + +import { ReturnRequestFacade } from '../../facades/return-request.facade'; +import { + CreateReturnRequestPayload, + CreateReturnRequestPosition, + ReturnablePosition, +} from '../../models/return-request/return-request.model'; +import { allowedStatus } from '../../util'; + +@Component({ + selector: 'ish-return-request-modal', + templateUrl: './return-request-modal.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ReturnRequestModalComponent implements OnInit, OnChanges { + @Input() order: Order; + @Input() isOpenModal: boolean; + @Input() isGuest: boolean; + @Output() closeModal = new EventEmitter(); + @ViewChild('returnRequestDialog') returnRequestDialog: ModalDialogComponent; + + form: UntypedFormGroup; + returnableItems$: Observable; + + positions: CreateReturnRequestPosition[]; + returnItemsLoaded = false; + selectedQuantity = 0; + + constructor(private returnRequestFacade: ReturnRequestFacade, private cdr: ChangeDetectorRef) {} + + ngOnInit() { + this.form = new FormGroup({ + checkAll: new FormControl(false), + items: new FormGroup({}), + comment: new FormControl(''), + }); + this.returnableItems$ = this.returnRequestFacade.getOrderReturnableItems$({ + email: this.order?.email, + documentNo: this.order?.documentNo, + orderId: this.order?.id, + isGuest: this.isGuest, + }); + } + + ngOnChanges() { + if (this.isOpenModal) { + this.showModal(); + } + } + + showModal() { + this.returnRequestDialog.show(); + } + + hideModal() { + this.returnRequestDialog.hide(); + this.closeModal.emit(); + } + + onItemsUpdate(data: CreateReturnRequestPosition[]) { + this.positions = data; + } + + onQuantityUpdate(data: number) { + this.selectedQuantity = data; + this.cdr.markForCheck(); + } + + private getRequest(): CreateReturnRequestPayload { + const customAttributes = []; + if (this.form.get('comment').value) { + customAttributes.push({ + key: 'comment', + value: this.form.get('comment').value, + }); + } + + return { + type: 'RETURN', + positions: this.positions, + customAttributes, + isGuest: this.isGuest, + orderId: this.order.id, + email: this.order.email, + documentNo: this.order.documentNo, + }; + } + + hasStatusCode(status: string) { + return allowedStatus(status); + } + + submit() { + this.returnRequestFacade.createRequest(this.getRequest()); + this.form.reset(); + this.hideModal(); + } +} diff --git a/src/app/extensions/return-request/components/return-request-product-info/return-request-product-info.component.html b/src/app/extensions/return-request/components/return-request-product-info/return-request-product-info.component.html new file mode 100644 index 0000000000..51cf12fb10 --- /dev/null +++ b/src/app/extensions/return-request/components/return-request-product-info/return-request-product-info.component.html @@ -0,0 +1,26 @@ +
+
+
+
+ +
+
+
+ +
+
+ {{ 'toolineo.account.return_request.modal.table.product.EAN' | translate }} + {{ getAttribute(product, 'EAN') }} +
+
+ {{ 'toolineo.account.return_request.modal.table.product.item_number' | translate }} + {{ getAttribute(product, 'artnr') }} +
+
+ {{ 'toolineo.account.return_request.modal.table.product.manufacturer_number' | translate }} + {{ getAttribute(product, 'mfg_number') }} +
+
+
+
+
diff --git a/src/app/extensions/return-request/components/return-request-product-info/return-request-product-info.component.spec.ts b/src/app/extensions/return-request/components/return-request-product-info/return-request-product-info.component.spec.ts new file mode 100644 index 0000000000..89c2ceda2b --- /dev/null +++ b/src/app/extensions/return-request/components/return-request-product-info/return-request-product-info.component.spec.ts @@ -0,0 +1,37 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MockComponent, MockDirective } from 'ng-mocks'; + +import { ProductContextDirective } from 'ish-core/directives/product-context.directive'; +import { ProductImageComponent } from 'ish-shared/components/product/product-image/product-image.component'; +import { ProductNameComponent } from 'ish-shared/components/product/product-name/product-name.component'; + +import { ReturnRequestProductInfoComponent } from './return-request-product-info.component'; + +describe('Return Request Product Info Component', () => { + let component: ReturnRequestProductInfoComponent; + let fixture: ComponentFixture; + let element: HTMLElement; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ + MockComponent(ProductImageComponent), + MockComponent(ProductNameComponent), + MockDirective(ProductContextDirective), + ReturnRequestProductInfoComponent, + ], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ReturnRequestProductInfoComponent); + component = fixture.componentInstance; + element = fixture.nativeElement; + }); + + it('should be created', () => { + expect(component).toBeTruthy(); + expect(element).toBeTruthy(); + expect(() => fixture.detectChanges()).not.toThrow(); + }); +}); diff --git a/src/app/extensions/return-request/components/return-request-product-info/return-request-product-info.component.ts b/src/app/extensions/return-request/components/return-request-product-info/return-request-product-info.component.ts new file mode 100644 index 0000000000..6a3afef96a --- /dev/null +++ b/src/app/extensions/return-request/components/return-request-product-info/return-request-product-info.component.ts @@ -0,0 +1,16 @@ +import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; + +import { ProductView } from 'ish-core/models/product-view/product-view.model'; + +@Component({ + selector: 'ish-return-request-product-info', + templateUrl: './return-request-product-info.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ReturnRequestProductInfoComponent { + @Input() sku: string; + + getAttribute(product: ProductView, name: string) { + return product.attributes?.find(attr => attr.name === name) ?? 'N/A'; + } +} diff --git a/src/app/extensions/return-request/components/returnable-items/returnable-items.component.html b/src/app/extensions/return-request/components/returnable-items/returnable-items.component.html new file mode 100644 index 0000000000..37201433a8 --- /dev/null +++ b/src/app/extensions/return-request/components/returnable-items/returnable-items.component.html @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ +
+
+ {{ 'toolineo.account.return_request.modal.table.heading.product_label' | translate }} + + + + {{ 'toolineo.account.return_request.modal.table.heading.returnable_qty' | translate }} + + {{ row.maxReturnQty }} + + {{ 'toolineo.account.return_request.modal.table.heading.return_qty' | translate }} + +
+ + +
+
+ {{ 'toolineo.account.return_request.modal.table.heading.reason' | translate }} + +
+ + +
+
diff --git a/src/app/extensions/return-request/components/returnable-items/returnable-items.component.spec.ts b/src/app/extensions/return-request/components/returnable-items/returnable-items.component.spec.ts new file mode 100644 index 0000000000..f0548a20a6 --- /dev/null +++ b/src/app/extensions/return-request/components/returnable-items/returnable-items.component.spec.ts @@ -0,0 +1,49 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormControl, FormGroup } from '@angular/forms'; +import { of } from 'rxjs'; +import { instance, mock, when } from 'ts-mockito'; + +import { AccountFacade } from 'ish-core/facades/account.facade'; + +import { ReturnRequestFacade } from '../../facades/return-request.facade'; + +import { ReturnableItemsComponent } from './returnable-items.component'; + +describe('Returnable Items Component', () => { + let component: ReturnableItemsComponent; + let fixture: ComponentFixture; + let element: HTMLElement; + let returnFacade: ReturnRequestFacade; + let accountFacade: AccountFacade; + + beforeEach(async () => { + returnFacade = mock(ReturnRequestFacade); + accountFacade = mock(AccountFacade); + when(returnFacade.getReturnReasons$()).thenReturn(of([])); + when(accountFacade.isBusinessCustomer$).thenReturn(of()); + await TestBed.configureTestingModule({ + declarations: [ReturnableItemsComponent], + providers: [ + { provide: AccountFacade, useFactory: () => instance(accountFacade) }, + { provide: ReturnRequestFacade, useFactory: () => instance(returnFacade) }, + ], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ReturnableItemsComponent); + component = fixture.componentInstance; + element = fixture.nativeElement; + + component.form = new FormGroup({ + checkAll: new FormControl(), + items: new FormControl(), + }); + }); + + it('should be created', () => { + expect(component).toBeTruthy(); + expect(element).toBeTruthy(); + expect(() => fixture.detectChanges()).not.toThrow(); + }); +}); diff --git a/src/app/extensions/return-request/components/returnable-items/returnable-items.component.ts b/src/app/extensions/return-request/components/returnable-items/returnable-items.component.ts new file mode 100644 index 0000000000..a3888e7a36 --- /dev/null +++ b/src/app/extensions/return-request/components/returnable-items/returnable-items.component.ts @@ -0,0 +1,168 @@ +import { + ChangeDetectionStrategy, + Component, + DestroyRef, + EventEmitter, + Input, + OnInit, + Output, + inject, +} from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { FormControl, FormGroup, UntypedFormGroup, Validators } from '@angular/forms'; + +import { AccountFacade } from 'ish-core/facades/account.facade'; +import { SelectOption } from 'ish-core/models/select-option/select-option.model'; + +import { ReturnRequestFacade } from '../../facades/return-request.facade'; +import { CreateReturnRequestPosition, ReturnablePosition } from '../../models/return-request/return-request.model'; + +interface FormItems { + [key: string]: { checked: boolean; qty: number; reason: 'string' }; +} + +@Component({ + selector: 'ish-returnable-items', + templateUrl: './returnable-items.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ReturnableItemsComponent implements OnInit { + @Input() returnableItems: ReturnablePosition[] = []; + @Input() form: UntypedFormGroup; + + returnReasons: SelectOption[] = []; + @Output() itemsUpdated = new EventEmitter(); + @Output() quantityUpdated = new EventEmitter(); + + private destroyRef = inject(DestroyRef); + isB2C = false; + + constructor(private returnRequestFacade: ReturnRequestFacade, private accountFacade: AccountFacade) {} + + totalQuantity = 0; + columnsToDisplay = ['select', 'product', 'returnable_qty', 'qty']; + + ngOnInit(): void { + this.generateFormGroup(); + this.accountFacade.isBusinessCustomer$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(isBusinessCustomer => { + if (isBusinessCustomer !== undefined && !false) { + this.isB2C = true; + this.columnsToDisplay = [...this.columnsToDisplay, 'reason']; + } + }); + + this.returnRequestFacade + .getReturnReasons$() + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(reasons => { + if (reasons?.length) { + this.returnReasons = reasons; + } + }); + + this.checkAll.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(isChecked => { + this.returnableItems.forEach(({ sku, maxReturnQty }) => { + this.getControl(sku, 'checked').setValue(isChecked); + this.updateStatusAndValidity(`${sku}.qty`, isChecked, maxReturnQty); + if (this.isB2C) { + this.updateStatusAndValidity(`${sku}.reason`, isChecked); + } + }); + }); + + this.itemsForm.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((items: FormItems) => { + if (this.isItemformHasValidRow()) { + this.updatePositions(items); + } + if (this.itemsCheckState().every(item => !item)) { + this.updatePositions(items); + } + }); + } + + generateFormGroup() { + this.returnableItems.forEach(d => { + this.itemsForm.addControl( + d.sku, + new FormGroup({ + checked: new FormControl(false), + qty: new FormControl({ value: '1', disabled: true }), + reason: new FormControl({ value: undefined, disabled: true }), + }) + ); + }); + } + + updatePositions(items: FormItems) { + this.totalQuantity = 0; + const positionItems = Object.keys(items) + .filter(itemKey => this.itemsForm.value[itemKey].checked) + .map(itemKey => { + this.totalQuantity += parseInt(this.itemsForm.value[itemKey].qty ?? 0, 10); + let reason; + if (this.isB2C) { + reason = this.itemsForm.value[itemKey].reason; + } + return { + positionNumber: this.returnableItems.find(pos => pos.sku === itemKey).positionNumber, + productNumber: itemKey, + quantity: this.itemsForm.value[itemKey].qty, + reason, + }; + }); + + this.itemsUpdated.emit(positionItems); + this.quantityUpdated.emit(this.totalQuantity); + } + + get itemsForm() { + return this.form.get('items') as FormGroup; + } + + get checkAll() { + return this.form.get('checkAll') as FormControl; + } + + getControl(sku: string, control: string) { + return this.itemsForm.get(`${sku}.${control}`); + } + + updateStatusAndValidity(controlName: string, isChecked: boolean, maxQuantiy?: number) { + this.itemsForm.get(controlName)[isChecked ? 'enable' : 'disable'](); + const validators = maxQuantiy ? [Validators.required, Validators.max(maxQuantiy)] : [Validators.required]; + this.itemsForm.get(controlName)[isChecked ? 'setValidators' : 'removeValidators'](validators); + this.itemsForm.get(controlName).updateValueAndValidity(); + } + + itemsCheckState(): boolean[] { + return Object.keys(this.itemsForm.value).map(key => this.itemsForm.value[key].checked); + } + + isItemformHasValidRow(): boolean { + return !!Object.keys(this.itemsForm.value).filter( + key => + this.itemsForm.value[key].checked && + this.itemsForm.value[key].qty && + (!this.isB2C || (this.isB2C && this.itemsForm.value[key].reason)) + ).length; + } + + toggleFields(event: Event, sku: string, maxQuantiy: number) { + const isChecked = (event.target as HTMLInputElement).checked; + + // uncheck checkAll checkbox if any row is unselected + if (this.itemsCheckState().some(check => !check)) { + this.checkAll.setValue(false, { emitEvent: false }); + } + + // check checkAll checkbox if all rows are selected + if (this.itemsCheckState().every(check => check)) { + this.checkAll.setValue(true, { emitEvent: false }); + } + + this.updateStatusAndValidity(`${sku}.qty`, isChecked, maxQuantiy); + if (this.isB2C) { + this.updateStatusAndValidity(`${sku}.reason`, isChecked); + } + } +} diff --git a/src/app/extensions/return-request/exports/.gitignore b/src/app/extensions/return-request/exports/.gitignore new file mode 100644 index 0000000000..c9c09b831c --- /dev/null +++ b/src/app/extensions/return-request/exports/.gitignore @@ -0,0 +1 @@ +**/lazy* diff --git a/src/app/extensions/return-request/exports/return-request-exports.module.ts b/src/app/extensions/return-request/exports/return-request-exports.module.ts new file mode 100644 index 0000000000..e2aee36b8c --- /dev/null +++ b/src/app/extensions/return-request/exports/return-request-exports.module.ts @@ -0,0 +1,23 @@ +import { NgModule } from '@angular/core'; + +import { FeatureToggleModule } from 'ish-core/feature-toggle.module'; +import { LAZY_FEATURE_MODULE } from 'ish-core/utils/module-loader/module-loader.service'; + +import { LazyReturnRequestReturnButtonComponent } from './lazy-return-request-return-button/lazy-return-request-return-button.component'; + +@NgModule({ + imports: [FeatureToggleModule], + providers: [ + { + provide: LAZY_FEATURE_MODULE, + useValue: { + feature: 'returnRequest', + location: () => import('../store/return-request-store.module').then(m => m.ReturnRequestStoreModule), + }, + multi: true, + }, + ], + declarations: [LazyReturnRequestReturnButtonComponent], + exports: [LazyReturnRequestReturnButtonComponent], +}) +export class ReturnRequestExportsModule {} diff --git a/src/app/extensions/return-request/facades/return-request.facade.ts b/src/app/extensions/return-request/facades/return-request.facade.ts new file mode 100644 index 0000000000..1ce6f0cbc2 --- /dev/null +++ b/src/app/extensions/return-request/facades/return-request.facade.ts @@ -0,0 +1,49 @@ +import { Injectable } from '@angular/core'; +import { Store, select } from '@ngrx/store'; + +import { CreateReturnRequestPayload } from '../models/return-request/return-request.model'; +import { + createReturnRequest, + getGuestOrders, + getReasons, + getReturnRequestError, + getReturnRequestLoading, + getReturnRequests, + getReturnableItems, + loadOrderByDocumentNoAndEmail, + loadOrderReturnReasons, + loadOrderReturnRequests, + loadOrderReturnableItems, +} from '../store/return-request'; + +@Injectable({ providedIn: 'root' }) +export class ReturnRequestFacade { + constructor(private store: Store) {} + + returnRequestLoading$ = this.store.pipe(select(getReturnRequestLoading)); + returnRequestError$ = this.store.pipe(select(getReturnRequestError)); + + getReturnReasons$() { + this.store.dispatch(loadOrderReturnReasons()); + return this.store.pipe(select(getReasons)); + } + + getOrderReturnableItems$(request: { orderId?: string; documentNo?: string; email?: string; isGuest: boolean }) { + this.store.dispatch(loadOrderReturnableItems({ ...request })); + return this.store.pipe(select(getReturnableItems)); + } + + getOrderReturnRequests$(orderIds: string[]) { + this.store.dispatch(loadOrderReturnRequests({ orderIds })); + return this.store.pipe(select(getReturnRequests)); + } + + createRequest(request: CreateReturnRequestPayload) { + this.store.dispatch(createReturnRequest({ request })); + } + + getGuestUserOrders$(documentNo: string, email: string) { + this.store.dispatch(loadOrderByDocumentNoAndEmail({ documentNo, email })); + return this.store.pipe(select(getGuestOrders)); + } +} diff --git a/src/app/extensions/return-request/models/return-request/return-request.interface.ts b/src/app/extensions/return-request/models/return-request/return-request.interface.ts new file mode 100644 index 0000000000..9e036d365e --- /dev/null +++ b/src/app/extensions/return-request/models/return-request/return-request.interface.ts @@ -0,0 +1,44 @@ +import { ReturnRequestStatus, ReturnRequestType } from './return-request.model'; + +export interface ReturnRequestData { + type: ReturnRequestType; + rmaNumber: string; + comment: string; + id: number; + creationDate: number; + shopOrderNumber: string; + shopName: string; + supplierOrderNumber: string; + supplierName: string; + status: ReturnRequestStatus; + businessStatus: string; +} + +export interface ReturnRequestPositionData { + id: number; + positionNumber: number; + productNumber: string; + reason: string; + quantity: number; + productName: string; + supplierProductNumber: string; + customAttributes: unknown[]; +} + +export interface ReturnableOrdersData { + positions: { + positionNumber: number; + quantity: number; + items: { productSerialNumber: string }[]; + product: { + number: string; + name: string; + }; + }[]; +} + +export interface ReturnReasonData { + name: string; + description: string; + type?: string; +} diff --git a/src/app/extensions/return-request/models/return-request/return-request.mapper.ts b/src/app/extensions/return-request/models/return-request/return-request.mapper.ts new file mode 100644 index 0000000000..9b111a58c8 --- /dev/null +++ b/src/app/extensions/return-request/models/return-request/return-request.mapper.ts @@ -0,0 +1,53 @@ +import { SelectOption } from 'ish-core/models/select-option/select-option.model'; + +import { + ReturnReasonData, + ReturnRequestData, + ReturnRequestPositionData, + ReturnableOrdersData, +} from './return-request.interface'; +import { ReturnRequest, ReturnRequestPosition, ReturnablePosition } from './return-request.model'; + +export class ReturnRequestMapper { + static fromReturnPosition(returnables: ReturnableOrdersData): ReturnablePosition[] { + return returnables.positions.map(pos => ({ + maxReturnQty: pos.quantity, + positionNumber: pos.positionNumber, + sku: pos.product.number, + productSerialNumbers: pos.items, + productName: pos.product.name, + })); + } + + static fromReturnRequest(returnRequest: ReturnRequestData[], orderId: string): ReturnRequest[] { + return returnRequest.map(r => ({ + type: r.type, + id: r.id, + orderId, + businessStatus: r.businessStatus, + creationDate: r.creationDate, + rmaNumber: r.rmaNumber, + status: r.status, + })); + } + + static fromReturnRequestPosition(returnRequestPosition: ReturnRequestPositionData[]): ReturnRequestPosition[] { + return returnRequestPosition.map(r => ({ + id: r.id, + positionNumber: r.positionNumber, + productNumber: r.productNumber, + reason: r.reason, + quantity: r.quantity, + productName: r.productName, + supplierProductNumber: r.supplierProductNumber, + customAttributes: r.customAttributes, + })); + } + + static fromReturnReason(data: ReturnReasonData[]): SelectOption[] { + return data.map(d => ({ + value: d.name, + label: d.description, + })); + } +} diff --git a/src/app/extensions/return-request/models/return-request/return-request.model.ts b/src/app/extensions/return-request/models/return-request/return-request.model.ts new file mode 100644 index 0000000000..d8c505e5e5 --- /dev/null +++ b/src/app/extensions/return-request/models/return-request/return-request.model.ts @@ -0,0 +1,49 @@ +export type ReturnRequestType = 'RETURN' | 'PICKUP'; + +export type ReturnRequestStatus = 'ACCEPTED' | 'CLOSED' | 'DO_APPROVE' | 'DO_CLOSE' | 'INITIAL' | 'REJECTED'; + +export interface ReturnablePosition { + positionNumber: number; + maxReturnQty: number; + productSerialNumbers: { productSerialNumber: string }[]; + sku: string; + productName: string; +} + +export interface ReturnRequestPosition { + id: number; + positionNumber: number; + productNumber: string; + reason: string; + quantity: number; + productName: string; + supplierProductNumber: string; + customAttributes: unknown[]; +} + +export interface ReturnRequest extends Partial { + type: ReturnRequestType; + rmaNumber: string; + id: number; + orderId: string; + creationDate: number; + status: ReturnRequestStatus; + businessStatus: string; +} + +export interface CreateReturnRequestPosition { + positionNumber: number; + productNumber: string; + reason: string; + quantity: number; +} + +export interface CreateReturnRequestPayload { + type: ReturnRequestType; + positions: CreateReturnRequestPosition[]; + customAttributes?: { [key: string]: string }[]; + isGuest: boolean; + orderId?: string; + email?: string; + documentNo?: string; +} diff --git a/src/app/extensions/return-request/pages/return-overview/return-overview-page.component.html b/src/app/extensions/return-request/pages/return-overview/return-overview-page.component.html new file mode 100644 index 0000000000..47c481da1c --- /dev/null +++ b/src/app/extensions/return-request/pages/return-overview/return-overview-page.component.html @@ -0,0 +1,42 @@ + + +
+

{{ 'toolineo.account.return_overview.heading' | translate }}

+

{{ 'toolineo.account.return_overview.subtitle' | translate }}

+ +
+ + + + +
+
+ diff --git a/src/app/extensions/return-request/pages/return-overview/return-overview-page.component.spec.ts b/src/app/extensions/return-request/pages/return-overview/return-overview-page.component.spec.ts new file mode 100644 index 0000000000..8ec527a09d --- /dev/null +++ b/src/app/extensions/return-request/pages/return-overview/return-overview-page.component.spec.ts @@ -0,0 +1,49 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { TranslateModule } from '@ngx-translate/core'; +import { MockComponent } from 'ng-mocks'; +import { of } from 'rxjs'; +import { instance, mock, when } from 'ts-mockito'; + +import { AccountFacade } from 'ish-core/facades/account.facade'; +import { ErrorMessageComponent } from 'ish-shared/components/common/error-message/error-message.component'; +import { LoadingComponent } from 'ish-shared/components/common/loading/loading.component'; + +import { ReturnRequestFacade } from '../../facades/return-request.facade'; + +import { ReturnOverviewPageComponent } from './return-overview-page.component'; + +describe('Return Overview Page Component', () => { + let component: ReturnOverviewPageComponent; + let fixture: ComponentFixture; + let element: HTMLElement; + let accountFacade: AccountFacade; + + beforeEach(async () => { + accountFacade = mock(AccountFacade); + when(accountFacade.orders$).thenReturn(of([])); + await TestBed.configureTestingModule({ + imports: [TranslateModule.forRoot()], + declarations: [ + MockComponent(ErrorMessageComponent), + MockComponent(LoadingComponent), + ReturnOverviewPageComponent, + ], + providers: [ + { provide: AccountFacade, useFactory: () => instance(accountFacade) }, + { provide: ReturnRequestFacade, useFactory: () => instance(mock(ReturnRequestFacade)) }, + ], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ReturnOverviewPageComponent); + component = fixture.componentInstance; + element = fixture.nativeElement; + }); + + it('should be created', () => { + expect(component).toBeTruthy(); + expect(element).toBeTruthy(); + expect(() => fixture.detectChanges()).not.toThrow(); + }); +}); diff --git a/src/app/extensions/return-request/pages/return-overview/return-overview-page.component.ts b/src/app/extensions/return-request/pages/return-overview/return-overview-page.component.ts new file mode 100644 index 0000000000..2cead68abf --- /dev/null +++ b/src/app/extensions/return-request/pages/return-overview/return-overview-page.component.ts @@ -0,0 +1,62 @@ +import { ChangeDetectionStrategy, Component, DestroyRef, OnInit, inject } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { Observable, map, of, switchMap } from 'rxjs'; + +import { AccountFacade } from 'ish-core/facades/account.facade'; +import { HttpError } from 'ish-core/models/http-error/http-error.model'; + +import { ReturnRequestFacade } from '../../facades/return-request.facade'; +import { ReturnRequest, ReturnRequestStatus } from '../../models/return-request/return-request.model'; +import { allowedStatus } from '../../util'; + +type TabName = 'all' | 'requested' | 'confirmed' | 'completed'; + +@Component({ + selector: 'ish-return-overview-page', + templateUrl: './return-overview-page.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ReturnOverviewPageComponent implements OnInit { + currentYear = new Date().getFullYear(); + active: TabName = 'all'; + ordersId$: Observable; + returnRequests: ReturnRequest[]; + loading$: Observable; + error$: Observable; + + private destroyRef = inject(DestroyRef); + + constructor(private accountFacade: AccountFacade, private returnRequestFacade: ReturnRequestFacade) {} + + ngOnInit() { + this.loading$ = this.returnRequestFacade.returnRequestLoading$; + this.error$ = this.returnRequestFacade.returnRequestError$; + + this.accountFacade.orders$ + .pipe( + map(orders => orders.filter(order => allowedStatus(order.statusCode)).map(order => order.id)), + switchMap(ids => (ids.length ? this.returnRequestFacade.getOrderReturnRequests$(ids) : of(undefined))), + takeUntilDestroyed(this.destroyRef) + ) + .subscribe(data => { + if (data) { + this.returnRequests = data; + } + }); + } + + getLast3Year(): number[] { + const currentYear = new Date().getFullYear(); + return Array(3) + .fill('') + .map((_, index) => currentYear - (index + 1)); + } + + getOrders(status?: ReturnRequestStatus) { + return this.returnRequests.filter(request => !status || request.status === status); + } + + hasStatusCode(status: string) { + return allowedStatus(status); + } +} diff --git a/src/app/extensions/return-request/pages/return-overview/return-overview-page.module.ts b/src/app/extensions/return-request/pages/return-overview/return-overview-page.module.ts new file mode 100644 index 0000000000..9cc45e4018 --- /dev/null +++ b/src/app/extensions/return-request/pages/return-overview/return-overview-page.module.ts @@ -0,0 +1,18 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; + +import { ReturnRequestModule } from '../../return-request.module'; + +import { ReturnOverviewPageComponent } from './return-overview-page.component'; + +const returnRequestOverviewPageRoutes: Routes = [ + { + path: '', + component: ReturnOverviewPageComponent, + }, +]; + +@NgModule({ + imports: [RouterModule.forChild(returnRequestOverviewPageRoutes), ReturnRequestModule], +}) +export class ReturnOverviewPageModule {} diff --git a/src/app/extensions/return-request/pages/return-request-detail/return-request-detail-page.component.html b/src/app/extensions/return-request/pages/return-request-detail/return-request-detail-page.component.html new file mode 100644 index 0000000000..209afd31d2 --- /dev/null +++ b/src/app/extensions/return-request/pages/return-request-detail/return-request-detail-page.component.html @@ -0,0 +1,70 @@ +
+
+
+

{{ 'toolineo.account.return_detail.heading' | translate }} {{ returnId }}

+ {{ 'toolineo.account.return_detail.created_on' | translate }} June 12, 2023
+ {{ 'toolineo.account.return_detail.dealer_link_label' | translate }} 12323213
+ {{ 'toolineo.account.return_detail.order' | translate }} 000000000000 +
+ + Lorem, ipsum dolor sit amet consectetur adipisicing elit. + +
+
+
+

+ {{ 'toolineo.account.return_detail.status' | translate }} +
+ waiting for confiration from the Dealer +

+ + {{ 'toolineo.account.return_detail.before_date' | translate }} + + +

22.06.2023

+
+

+ + {{ 'toolineo.account.return_detail.credit_return_note' | translate }} +

+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + +
{{ 'toolineo.account.return_detail.table.header.article' | translate }}{{ 'toolineo.account.return_detail.table.header.quantity_reported' | translate }}{{ 'toolineo.account.return_detail.table.header.reason' | translate }}{{ 'toolineo.account.return_detail.table.header.confirmed' | translate }}{{ 'toolineo.account.return_detail.table.header.returns_required' | translate }}{{ 'toolineo.account.return_detail.table.header.graduation_date' | translate }}{{ 'toolineo.account.return_detail.table.header.solution' | translate }}
+ + 4I do not like it2YesOctober 12, 2023xy
+
+ +
+

{{ 'toolineo.account.return_detail.dealer.title' | translate }}

+ Lorem ipsum dolor, sit amet consectetur adipisicing elit. +
diff --git a/src/app/extensions/return-request/pages/return-request-detail/return-request-detail-page.component.spec.ts b/src/app/extensions/return-request/pages/return-request-detail/return-request-detail-page.component.spec.ts new file mode 100644 index 0000000000..3cf79ae8fb --- /dev/null +++ b/src/app/extensions/return-request/pages/return-request-detail/return-request-detail-page.component.spec.ts @@ -0,0 +1,27 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ReturnRequestDetailPageComponent } from './return-request-detail-page.component'; + +describe('Return Request Detail Page Component', () => { + let component: ReturnRequestDetailPageComponent; + let fixture: ComponentFixture; + let element: HTMLElement; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ReturnRequestDetailPageComponent], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ReturnRequestDetailPageComponent); + component = fixture.componentInstance; + element = fixture.nativeElement; + }); + + it('should be created', () => { + expect(component).toBeTruthy(); + expect(element).toBeTruthy(); + expect(() => fixture.detectChanges()).not.toThrow(); + }); +}); diff --git a/src/app/extensions/return-request/pages/return-request-detail/return-request-detail-page.component.ts b/src/app/extensions/return-request/pages/return-request-detail/return-request-detail-page.component.ts new file mode 100644 index 0000000000..8d6c811be9 --- /dev/null +++ b/src/app/extensions/return-request/pages/return-request-detail/return-request-detail-page.component.ts @@ -0,0 +1,16 @@ +import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; + +@Component({ + selector: 'ish-return-request-detail-page', + templateUrl: './return-request-detail-page.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ReturnRequestDetailPageComponent implements OnInit { + returnId: string; + constructor(private activateRoute: ActivatedRoute) {} + + ngOnInit() { + this.returnId = this.activateRoute.snapshot.params.id; + } +} diff --git a/src/app/extensions/return-request/pages/return-request-detail/return-request-detail-page.module.ts b/src/app/extensions/return-request/pages/return-request-detail/return-request-detail-page.module.ts new file mode 100644 index 0000000000..f83977c10b --- /dev/null +++ b/src/app/extensions/return-request/pages/return-request-detail/return-request-detail-page.module.ts @@ -0,0 +1,18 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; + +import { ReturnRequestModule } from '../../return-request.module'; + +import { ReturnRequestDetailPageComponent } from './return-request-detail-page.component'; + +const returnRequestDetailPageRoutes: Routes = [ + { + path: '', + component: ReturnRequestDetailPageComponent, + }, +]; + +@NgModule({ + imports: [RouterModule.forChild(returnRequestDetailPageRoutes), ReturnRequestModule], +}) +export class ReturnRequestDetailPageModule {} diff --git a/src/app/extensions/return-request/pages/return-request-guest/return-request-guest-page.component.html b/src/app/extensions/return-request/pages/return-request-guest/return-request-guest-page.component.html new file mode 100644 index 0000000000..b7f03eb9ec --- /dev/null +++ b/src/app/extensions/return-request/pages/return-request-guest/return-request-guest-page.component.html @@ -0,0 +1,27 @@ +
+
+

{{ 'toolineo.return_request_guest.heading' | translate }}

+ {{ 'toolineo.return_request_guest.heading_info' | translate }} +
+
+

{{ 'toolineo.return_request_guest.sub_heading' | translate : { '0': documentNo } }}

+ + + {{ 'toolineo.return_request_guest.sub_heading_info' | translate }} +
+
+ + + + + + + +

{{ 'toolineo.return_request_guest.no_order_found' | translate }}

+
+ + diff --git a/src/app/extensions/return-request/pages/return-request-guest/return-request-guest-page.component.spec.ts b/src/app/extensions/return-request/pages/return-request-guest/return-request-guest-page.component.spec.ts new file mode 100644 index 0000000000..a61da96a3e --- /dev/null +++ b/src/app/extensions/return-request/pages/return-request-guest/return-request-guest-page.component.spec.ts @@ -0,0 +1,36 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { FaIconComponent } from '@fortawesome/angular-fontawesome'; +import { TranslateModule } from '@ngx-translate/core'; +import { MockComponent } from 'ng-mocks'; +import { instance, mock } from 'ts-mockito'; + +import { ReturnRequestFacade } from '../../facades/return-request.facade'; + +import { ReturnRequestGuestPageComponent } from './return-request-guest-page.component'; + +describe('Return Request Guest Page Component', () => { + let component: ReturnRequestGuestPageComponent; + let fixture: ComponentFixture; + let element: HTMLElement; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [RouterTestingModule, TranslateModule.forRoot()], + declarations: [MockComponent(FaIconComponent), ReturnRequestGuestPageComponent], + providers: [{ provide: ReturnRequestFacade, useFactory: () => instance(mock(ReturnRequestFacade)) }], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ReturnRequestGuestPageComponent); + component = fixture.componentInstance; + element = fixture.nativeElement; + }); + + it('should be created', () => { + expect(component).toBeTruthy(); + expect(element).toBeTruthy(); + expect(() => fixture.detectChanges()).not.toThrow(); + }); +}); diff --git a/src/app/extensions/return-request/pages/return-request-guest/return-request-guest-page.component.ts b/src/app/extensions/return-request/pages/return-request-guest/return-request-guest-page.component.ts new file mode 100644 index 0000000000..b4b7f2e571 --- /dev/null +++ b/src/app/extensions/return-request/pages/return-request-guest/return-request-guest-page.component.ts @@ -0,0 +1,44 @@ +import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { Observable, delay, of, switchMap, tap } from 'rxjs'; + +import { Order } from 'ish-core/models/order/order.model'; + +import { ReturnRequestFacade } from '../../facades/return-request.facade'; + +@Component({ + selector: 'ish-return-request-guest-page', + templateUrl: './return-request-guest-page.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ReturnRequestGuestPageComponent implements OnInit { + order$: Observable; + showLoader = false; + documentNo: string; + + constructor( + private activatedRoute: ActivatedRoute, + private router: Router, + private returnRequestFacade: ReturnRequestFacade + ) {} + + ngOnInit(): void { + const paramsMap = this.activatedRoute.snapshot.queryParams; + const email = paramsMap?.email; + this.documentNo = paramsMap?.documentNo; + if (this.documentNo && email) { + this.showLoader = true; + this.order$ = of(1).pipe( + delay(1000), + switchMap(() => this.returnRequestFacade.getGuestUserOrders$(this.documentNo, email)), + tap(order => { + if (order?.id) { + this.showLoader = false; + } + }) + ); + } else { + this.router.navigate(['/home']); + } + } +} diff --git a/src/app/extensions/return-request/pages/return-request-guest/return-request-guest-page.module.ts b/src/app/extensions/return-request/pages/return-request-guest/return-request-guest-page.module.ts new file mode 100644 index 0000000000..bd8dad9d38 --- /dev/null +++ b/src/app/extensions/return-request/pages/return-request-guest/return-request-guest-page.module.ts @@ -0,0 +1,18 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; + +import { ReturnRequestModule } from '../../return-request.module'; + +import { ReturnRequestGuestPageComponent } from './return-request-guest-page.component'; + +const returnRequestGuestPageRoute: Routes = [ + { + path: '', + component: ReturnRequestGuestPageComponent, + }, +]; + +@NgModule({ + imports: [RouterModule.forChild(returnRequestGuestPageRoute), ReturnRequestModule], +}) +export class ReturnRequestGuestPageModule {} diff --git a/src/app/extensions/return-request/pages/return-request-routing.module.ts b/src/app/extensions/return-request/pages/return-request-routing.module.ts new file mode 100644 index 0000000000..f0920753b4 --- /dev/null +++ b/src/app/extensions/return-request/pages/return-request-routing.module.ts @@ -0,0 +1,21 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; + +const routes: Routes = [ + { + path: '', + loadChildren: () => import('./return-overview/return-overview-page.module').then(m => m.ReturnOverviewPageModule), + }, + { + path: ':id', + loadChildren: () => + import('./return-request-detail/return-request-detail-page.module').then(m => m.ReturnRequestDetailPageModule), + data: { breadcrumbData: [{ key: 'Returns details' }] }, + }, +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], +}) +export class ReturnRequestRoutingModule {} diff --git a/src/app/extensions/return-request/return-request.module.ts b/src/app/extensions/return-request/return-request.module.ts new file mode 100644 index 0000000000..a32e68acb2 --- /dev/null +++ b/src/app/extensions/return-request/return-request.module.ts @@ -0,0 +1,29 @@ +import { NgModule } from '@angular/core'; +import { NgbNavModule } from '@ng-bootstrap/ng-bootstrap'; + +import { SharedModule } from 'ish-shared/shared.module'; + +import { ReturnRequestItemsComponent } from './components/return-request-items/return-request-items.component'; +import { ReturnRequestModalComponent } from './components/return-request-modal/return-request-modal.component'; +import { ReturnRequestProductInfoComponent } from './components/return-request-product-info/return-request-product-info.component'; +import { ReturnableItemsComponent } from './components/returnable-items/returnable-items.component'; +import { ReturnOverviewPageComponent } from './pages/return-overview/return-overview-page.component'; +import { ReturnRequestDetailPageComponent } from './pages/return-request-detail/return-request-detail-page.component'; +import { ReturnRequestGuestPageComponent } from './pages/return-request-guest/return-request-guest-page.component'; +import { ReturnRequestReturnButtonComponent } from './shared/return-request-return-button/return-request-return-button.component'; + +@NgModule({ + imports: [NgbNavModule, SharedModule], + declarations: [ + ReturnableItemsComponent, + ReturnOverviewPageComponent, + ReturnRequestDetailPageComponent, + ReturnRequestGuestPageComponent, + ReturnRequestItemsComponent, + ReturnRequestModalComponent, + ReturnRequestProductInfoComponent, + ReturnRequestReturnButtonComponent, + ], + exports: [NgbNavModule, SharedModule], +}) +export class ReturnRequestModule {} diff --git a/src/app/extensions/return-request/services/return-request/return-request.service.ts b/src/app/extensions/return-request/services/return-request/return-request.service.ts new file mode 100644 index 0000000000..aab5adeb11 --- /dev/null +++ b/src/app/extensions/return-request/services/return-request/return-request.service.ts @@ -0,0 +1,168 @@ +import { HttpHeaders, HttpParams } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Observable, concatMap, forkJoin, map, throwError } from 'rxjs'; + +import { OrderData } from 'ish-core/models/order/order.interface'; +import { OrderMapper } from 'ish-core/models/order/order.mapper'; +import { Order } from 'ish-core/models/order/order.model'; +import { SelectOption } from 'ish-core/models/select-option/select-option.model'; +import { ApiService } from 'ish-core/services/api/api.service'; + +import { + ReturnReasonData, + ReturnRequestData, + ReturnRequestPositionData, + ReturnableOrdersData, +} from '../../models/return-request/return-request.interface'; +import { ReturnRequestMapper } from '../../models/return-request/return-request.mapper'; +import { + CreateReturnRequestPayload, + ReturnRequest, + ReturnRequestPosition, + ReturnablePosition, +} from '../../models/return-request/return-request.model'; + +@Injectable({ providedIn: 'root' }) +export class ReturnRequestService { + /** + * http header for IOM API v1 + */ + private returnRequestHeaders = new HttpHeaders({ + 'content-type': 'application/json', + Accept: 'application/vnd.intershop.order-iom-ext.v1+json', + }); + + constructor(private apiService: ApiService) {} + + getOrderReturnReasons(): Observable { + return this.apiService + .get(`orders/return-reasons`, { + headers: this.returnRequestHeaders, + }) + .pipe(map(ReturnRequestMapper.fromReturnReason)); + } + + getOrderReturnableItems(orderId: string): Observable { + if (!orderId) { + return throwError(() => new Error('getOrderReturnRequest() called without orderId')); + } + + return this.apiService + .get(`orders/${orderId}/return-requests/returnables`, { + headers: this.returnRequestHeaders, + }) + .pipe(map(data => ReturnRequestMapper.fromReturnPosition(data))); + } + + getOrderReturnRequestPosition(orderId: string, requestId: number): Observable { + return this.apiService + .get(`orders/${orderId}/return-requests/${requestId}/positions`, { + headers: this.returnRequestHeaders, + }) + .pipe(map(data => ReturnRequestMapper.fromReturnRequestPosition(data))); + } + + getOrderReturnRequest(orderId: string): Observable { + if (!orderId) { + return throwError(() => new Error('getOrderReturnRequest() called without orderId')); + } + + return this.apiService + .get(`orders/${orderId}/return-requests`, { + headers: this.returnRequestHeaders, + }) + .pipe(map(data => ReturnRequestMapper.fromReturnRequest(data, orderId))); + } + + getOrderReturnRequests(orderIds: string[]): Observable { + if (!orderIds.length) { + return throwError(() => new Error('getOrderReturnRequests() called without orderId')); + } + + return forkJoin(orderIds.map(id => this.getOrderReturnRequest(id))).pipe( + map(d => d.flat(1)), + concatMap(requestData => + forkJoin(requestData.map(d => this.getOrderReturnRequestPosition(d.orderId, d.id))).pipe( + map(d => d.flat(1)), + map(requestRequestPosition => + requestData.map(request => { + const position = requestRequestPosition.find(pos => pos.id === request.id); + return { ...request, ...position }; + }) + ) + ) + ) + ); + } + + createReturnRequest(request: CreateReturnRequestPayload): Observable { + const body = { + type: request.type, + positions: request.positions, + customAttributes: request.customAttributes, + }; + return this.apiService.post(`orders/${request.orderId}/return-requests`, body, { + headers: this.returnRequestHeaders, + }); + } + + /** + * Gets an anonymous line items with the given id and email. + * + * @param documentNo The (uuid) of the order. + * @param email email used while placing an order. + * @returns The order + */ + getOrderByDocumentNoAndEmail(documentNo: string, email: string): Observable { + if (!documentNo) { + return throwError(() => new Error('getOrderByDocumentNoAndEmail() called without documentNo')); + } + + if (!email) { + return throwError(() => new Error('getOrderByDocumentNoAndEmail() called without email')); + } + + return this.apiService + .get(`return/${documentNo}`, { + params: new HttpParams().append('email', email), + headers: this.returnRequestHeaders, + }) + .pipe(map(data => OrderMapper.fromData(data))); + } + + getOrderReturnableItemsByDocumentNoAndEmail(documentNo: string, email: string): Observable { + if (!email) { + return throwError(() => new Error('getOrderReturnableItemsByEmail() called without email')); + } + + if (!documentNo) { + return throwError(() => new Error('getOrderReturnableItemsByEmail() called without documentNo')); + } + + return this.apiService + .get<{ + returnableData: { + entity: ReturnableOrdersData; + }; + }>(`return/${documentNo}/return-requests/returnables`, { + params: new HttpParams().append('email', email), + headers: this.returnRequestHeaders, + }) + .pipe( + map(data => data.returnableData.entity), + map(data => ReturnRequestMapper.fromReturnPosition(data)) + ); + } + + createReturnRequestByDocumentNoAndEmail(request: CreateReturnRequestPayload): Observable { + const body = { + type: request.type, + positions: request.positions, + customAttributes: request.customAttributes, + }; + return this.apiService.post(`return/${request.documentNo}/return-requests`, body, { + params: new HttpParams().append('email', request.email), + headers: this.returnRequestHeaders, + }); + } +} diff --git a/src/app/extensions/return-request/shared/return-request-return-button/return-request-return-button.component.html b/src/app/extensions/return-request/shared/return-request-return-button/return-request-return-button.component.html new file mode 100644 index 0000000000..55571fcafb --- /dev/null +++ b/src/app/extensions/return-request/shared/return-request-return-button/return-request-return-button.component.html @@ -0,0 +1,12 @@ +
+ + + +
diff --git a/src/app/extensions/return-request/shared/return-request-return-button/return-request-return-button.component.spec.ts b/src/app/extensions/return-request/shared/return-request-return-button/return-request-return-button.component.spec.ts new file mode 100644 index 0000000000..7d74c92f5b --- /dev/null +++ b/src/app/extensions/return-request/shared/return-request-return-button/return-request-return-button.component.spec.ts @@ -0,0 +1,31 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { instance, mock } from 'ts-mockito'; + +import { ReturnRequestFacade } from '../../facades/return-request.facade'; + +import { ReturnRequestReturnButtonComponent } from './return-request-return-button.component'; + +describe('Return Request Return Button Component', () => { + let component: ReturnRequestReturnButtonComponent; + let fixture: ComponentFixture; + let element: HTMLElement; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ReturnRequestReturnButtonComponent], + providers: [{ provide: ReturnRequestFacade, useFactory: () => instance(mock(ReturnRequestFacade)) }], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ReturnRequestReturnButtonComponent); + component = fixture.componentInstance; + element = fixture.nativeElement; + }); + + it('should be created', () => { + expect(component).toBeTruthy(); + expect(element).toBeTruthy(); + expect(() => fixture.detectChanges()).not.toThrow(); + }); +}); diff --git a/src/app/extensions/return-request/shared/return-request-return-button/return-request-return-button.component.ts b/src/app/extensions/return-request/shared/return-request-return-button/return-request-return-button.component.ts new file mode 100644 index 0000000000..a2bb82d150 --- /dev/null +++ b/src/app/extensions/return-request/shared/return-request-return-button/return-request-return-button.component.ts @@ -0,0 +1,31 @@ +import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; + +import { Order } from 'ish-core/models/order/order.model'; +import { GenerateLazyComponent } from 'ish-core/utils/module-loader/generate-lazy-component.decorator'; + +import { allowedStatus } from '../../util'; + +@Component({ + selector: 'ish-return-request-return-button', + templateUrl: './return-request-return-button.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +@GenerateLazyComponent() +export class ReturnRequestReturnButtonComponent { + @Input() order: Order; + @Input() isGuest: boolean; + + isModalOpen = false; + + showButton() { + return allowedStatus(this.order?.statusCode); + } + + openModal() { + this.isModalOpen = true; + } + + onModalClose() { + this.isModalOpen = false; + } +} diff --git a/src/app/extensions/return-request/store/return-request-store.module.ts b/src/app/extensions/return-request/store/return-request-store.module.ts new file mode 100644 index 0000000000..fae13a127c --- /dev/null +++ b/src/app/extensions/return-request/store/return-request-store.module.ts @@ -0,0 +1,38 @@ +import { Injectable, InjectionToken, NgModule } from '@angular/core'; +import { EffectsModule } from '@ngrx/effects'; +import { ActionReducerMap, StoreConfig, StoreModule } from '@ngrx/store'; +import { pick } from 'lodash-es'; + +import { resetOnLogoutMeta } from 'ish-core/utils/meta-reducers'; + +import { ReturnRequestsState } from './return-request-store'; +import { ReturnRequestEffects } from './return-request/return-request.effects'; +import { returnRequestReducer } from './return-request/return-request.reducer'; + +const returnRequestReducers: ActionReducerMap = { + returnRequest: returnRequestReducer, +}; + +const returnRequestEffects = [ReturnRequestEffects]; + +@Injectable() +export class DefaultReturnRequestStoreConfig implements StoreConfig { + metaReducers = [resetOnLogoutMeta]; +} + +export const RETURN_REQUEST_STORE_CONFIG = new InjectionToken>( + 'returnRequestStoreConfig' +); + +@NgModule({ + imports: [ + EffectsModule.forFeature(returnRequestEffects), + StoreModule.forFeature('returnRequest', returnRequestReducers, RETURN_REQUEST_STORE_CONFIG), + ], + providers: [{ provide: RETURN_REQUEST_STORE_CONFIG, useClass: DefaultReturnRequestStoreConfig }], +}) +export class ReturnRequestStoreModule { + static forTesting(...reducers: (keyof ActionReducerMap)[]) { + return StoreModule.forFeature('returnRequest', pick(returnRequestReducers, reducers)); + } +} diff --git a/src/app/extensions/return-request/store/return-request-store.ts b/src/app/extensions/return-request/store/return-request-store.ts new file mode 100644 index 0000000000..b2e59769f9 --- /dev/null +++ b/src/app/extensions/return-request/store/return-request-store.ts @@ -0,0 +1,9 @@ +import { createFeatureSelector } from '@ngrx/store'; + +import { ReturnRequestState } from './return-request/return-request.reducer'; + +export interface ReturnRequestsState { + returnRequest: ReturnRequestState; +} + +export const getReturnRequestsState = createFeatureSelector('returnRequest'); diff --git a/src/app/extensions/return-request/store/return-request/index.ts b/src/app/extensions/return-request/store/return-request/index.ts new file mode 100644 index 0000000000..205ca9c3d3 --- /dev/null +++ b/src/app/extensions/return-request/store/return-request/index.ts @@ -0,0 +1,3 @@ +// API to access ngrx return request state +export * from './return-request.actions'; +export * from './return-request.selectors'; diff --git a/src/app/extensions/return-request/store/return-request/return-request.actions.ts b/src/app/extensions/return-request/store/return-request/return-request.actions.ts new file mode 100644 index 0000000000..28bfb6cbac --- /dev/null +++ b/src/app/extensions/return-request/store/return-request/return-request.actions.ts @@ -0,0 +1,77 @@ +import { createAction } from '@ngrx/store'; + +import { Order } from 'ish-core/models/order/order.model'; +import { SelectOption } from 'ish-core/models/select-option/select-option.model'; +import { httpError, payload } from 'ish-core/utils/ngrx-creators'; + +import { + CreateReturnRequestPayload, + ReturnRequest, + ReturnablePosition, +} from '../../models/return-request/return-request.model'; + +export const loadOrderReturnReasons = createAction('[Order Return Reason] Load Order Return Reasons'); + +export const loadOrderReturnReasonsSuccess = createAction( + '[Order Return Request] Load Order Return Reasons Success', + payload<{ reasons: SelectOption[] }>() +); + +export const loadOrderReturnReasonsFail = createAction( + '[Order Return Request] Load Order Return Reasons Fail', + httpError() +); + +export const loadOrderReturnableItems = createAction( + '[Order Return Request] Load Order Returnable Items', + payload<{ isGuest: boolean; orderId?: string; documentNo?: string; email?: string }>() +); + +export const loadOrderReturnableItemsSuccess = createAction( + '[Order Return Request] Load Order Returnable Items Success', + payload<{ orderReturnableItems: ReturnablePosition[] }>() +); + +export const loadOrderReturnableItemsFail = createAction( + '[Order Return Request] Load Order Returnable Items Fail', + httpError() +); + +export const loadOrderReturnRequests = createAction( + '[Order Return Request] Load Order Return Requests', + payload<{ orderIds: string[] }>() +); + +export const loadOrderReturnRequestsSuccess = createAction( + '[Order Return Request] Load Order Return Requests Success', + payload<{ orderReturnRequests: ReturnRequest[] }>() +); + +export const loadOrderReturnRequestsFail = createAction( + '[Order Return Request] Load Order Return Requests Fail', + httpError() +); + +export const createReturnRequest = createAction( + '[Order Return Request] Create Return Request', + payload<{ request: CreateReturnRequestPayload }>() +); + +export const createReturnRequestSuccess = createAction('[Order Return Request] Create Return Request Success'); + +export const createReturnRequestFail = createAction('[Order Return Request] Create Return Request Fail', httpError()); + +export const loadOrderByDocumentNoAndEmail = createAction( + '[Order Return Request] Load Order By Document Number and Email', + payload<{ documentNo: string; email: string }>() +); + +export const loadOrderByDocumentNoAndEmailSuccess = createAction( + '[Order Return Request] Load Order By Document Number and Email Success', + payload<{ order: Order }>() +); + +export const loadOrderByDocumentNoAndEmailFail = createAction( + '[Order Return Request] Load Order By Document Number and Email Fail', + httpError() +); diff --git a/src/app/extensions/return-request/store/return-request/return-request.effects.ts b/src/app/extensions/return-request/store/return-request/return-request.effects.ts new file mode 100644 index 0000000000..afbdf0e51e --- /dev/null +++ b/src/app/extensions/return-request/store/return-request/return-request.effects.ts @@ -0,0 +1,114 @@ +import { Injectable } from '@angular/core'; +import { Actions, createEffect, ofType } from '@ngrx/effects'; +import { concatMap, iif, map, switchMap } from 'rxjs'; + +import { displayErrorMessage, displaySuccessMessage } from 'ish-core/store/core/messages'; +import { mapErrorToAction, mapToPayload, mapToPayloadProperty } from 'ish-core/utils/operators'; + +import { ReturnRequestService } from '../../services/return-request/return-request.service'; + +import { + createReturnRequest, + createReturnRequestFail, + loadOrderByDocumentNoAndEmail, + loadOrderByDocumentNoAndEmailFail, + loadOrderByDocumentNoAndEmailSuccess, + loadOrderReturnReasons, + loadOrderReturnReasonsFail, + loadOrderReturnReasonsSuccess, + loadOrderReturnRequests, + loadOrderReturnRequestsFail, + loadOrderReturnRequestsSuccess, + loadOrderReturnableItems, + loadOrderReturnableItemsFail, + loadOrderReturnableItemsSuccess, +} from './return-request.actions'; + +@Injectable() +export class ReturnRequestEffects { + constructor(private actions$: Actions, private returnRequestService: ReturnRequestService) {} + + loadOrderReturnReasons$ = createEffect(() => + this.actions$.pipe( + ofType(loadOrderReturnReasons), + switchMap(() => + this.returnRequestService.getOrderReturnReasons().pipe( + map(reasons => loadOrderReturnReasonsSuccess({ reasons })), + mapErrorToAction(loadOrderReturnReasonsFail) + ) + ) + ) + ); + + loadOrderReturnableItems$ = createEffect(() => + this.actions$.pipe( + ofType(loadOrderReturnableItems), + mapToPayload(), + switchMap(payload => + iif( + () => payload.isGuest, + this.returnRequestService.getOrderReturnableItemsByDocumentNoAndEmail(payload.documentNo, payload.email), + this.returnRequestService.getOrderReturnableItems(payload.orderId) + ).pipe( + map(orderReturnableItems => loadOrderReturnableItemsSuccess({ orderReturnableItems })), + mapErrorToAction(loadOrderReturnableItemsFail) + ) + ) + ) + ); + + getOrderReturnRequests$ = createEffect(() => + this.actions$.pipe( + ofType(loadOrderReturnRequests), + mapToPayload(), + concatMap(({ orderIds }) => + this.returnRequestService.getOrderReturnRequests(orderIds).pipe( + map(orderReturnRequests => loadOrderReturnRequestsSuccess({ orderReturnRequests })), + mapErrorToAction(loadOrderReturnRequestsFail) + ) + ) + ) + ); + + createReturnRequest$ = createEffect(() => + this.actions$.pipe( + ofType(createReturnRequest), + mapToPayload(), + concatMap(({ request }) => + iif( + () => request.isGuest, + this.returnRequestService.createReturnRequestByDocumentNoAndEmail(request), + this.returnRequestService.createReturnRequest(request) + ).pipe( + map(() => displaySuccessMessage({ message: 'toolineo.account.return_request.modal.submit_success' })), + mapErrorToAction(createReturnRequestFail) + ) + ) + ) + ); + + displayCreateReturnRequestErrorMessage$ = createEffect(() => + this.actions$.pipe( + ofType(createReturnRequestFail), + mapToPayloadProperty('error'), + map(error => + displayErrorMessage({ + message: error.message, + }) + ) + ) + ); + + loadOrderByDocumentNoAndEmail$ = createEffect(() => + this.actions$.pipe( + ofType(loadOrderByDocumentNoAndEmail), + mapToPayload(), + switchMap(({ documentNo, email }) => + this.returnRequestService.getOrderByDocumentNoAndEmail(documentNo, email).pipe( + map(order => loadOrderByDocumentNoAndEmailSuccess({ order })), + mapErrorToAction(loadOrderByDocumentNoAndEmailFail) + ) + ) + ) + ); +} diff --git a/src/app/extensions/return-request/store/return-request/return-request.reducer.ts b/src/app/extensions/return-request/store/return-request/return-request.reducer.ts new file mode 100644 index 0000000000..9da0f34e73 --- /dev/null +++ b/src/app/extensions/return-request/store/return-request/return-request.reducer.ts @@ -0,0 +1,87 @@ +import { EntityState, createEntityAdapter } from '@ngrx/entity'; +import { createReducer, on } from '@ngrx/store'; + +import { HttpError } from 'ish-core/models/http-error/http-error.model'; +import { Order } from 'ish-core/models/order/order.model'; +import { SelectOption } from 'ish-core/models/select-option/select-option.model'; +import { setErrorOn, setLoadingOn, unsetLoadingAndErrorOn } from 'ish-core/utils/ngrx-creators'; + +import { ReturnRequest, ReturnablePosition } from '../../models/return-request/return-request.model'; + +import { + createReturnRequest, + createReturnRequestFail, + createReturnRequestSuccess, + loadOrderByDocumentNoAndEmail, + loadOrderByDocumentNoAndEmailFail, + loadOrderByDocumentNoAndEmailSuccess, + loadOrderReturnReasons, + loadOrderReturnReasonsFail, + loadOrderReturnReasonsSuccess, + loadOrderReturnRequests, + loadOrderReturnRequestsFail, + loadOrderReturnRequestsSuccess, + loadOrderReturnableItems, + loadOrderReturnableItemsFail, + loadOrderReturnableItemsSuccess, +} from './return-request.actions'; + +export const returnRequestAdapter = createEntityAdapter({ + selectId: request => `${request.orderId}_${request.id}`, +}); + +export interface ReturnRequestState extends EntityState { + loading: boolean; + error: HttpError; + reasons: SelectOption[]; + orderReturnableItems: ReturnablePosition[]; + guestUserOrder: Order; +} + +export const initialState: ReturnRequestState = returnRequestAdapter.getInitialState({ + loading: false, + error: undefined, + reasons: undefined, + orderReturnableItems: undefined, + guestUserOrder: undefined, +}); + +export const returnRequestReducer = createReducer( + initialState, + setLoadingOn( + loadOrderReturnReasons, + loadOrderReturnRequests, + createReturnRequest, + loadOrderReturnableItems, + loadOrderByDocumentNoAndEmail + ), + setErrorOn( + loadOrderReturnReasonsFail, + loadOrderReturnRequestsFail, + createReturnRequestFail, + loadOrderReturnableItemsFail, + loadOrderByDocumentNoAndEmailFail + ), + unsetLoadingAndErrorOn( + loadOrderReturnReasonsSuccess, + loadOrderReturnRequestsSuccess, + createReturnRequestSuccess, + loadOrderReturnableItemsSuccess + ), + on( + loadOrderReturnableItemsSuccess, + (state, action): ReturnRequestState => ({ ...state, orderReturnableItems: action.payload.orderReturnableItems }) + ), + on( + loadOrderReturnRequestsSuccess, + (state, action): ReturnRequestState => returnRequestAdapter.addMany(action.payload.orderReturnRequests, state) + ), + on( + loadOrderReturnReasonsSuccess, + (state, action): ReturnRequestState => ({ ...state, reasons: action.payload.reasons }) + ), + on( + loadOrderByDocumentNoAndEmailSuccess, + (state, action): ReturnRequestState => ({ ...state, guestUserOrder: action.payload.order }) + ) +); diff --git a/src/app/extensions/return-request/store/return-request/return-request.selectors.ts b/src/app/extensions/return-request/store/return-request/return-request.selectors.ts new file mode 100644 index 0000000000..3d1254fc71 --- /dev/null +++ b/src/app/extensions/return-request/store/return-request/return-request.selectors.ts @@ -0,0 +1,21 @@ +import { createSelector } from '@ngrx/store'; + +import { getReturnRequestsState } from '../return-request-store'; + +import { initialState, returnRequestAdapter } from './return-request.reducer'; + +const getReturnRequestState = createSelector(getReturnRequestsState, state => state?.returnRequest ?? initialState); + +const { selectAll } = returnRequestAdapter.getSelectors(getReturnRequestState); + +export const getReturnRequests = selectAll; + +export const getReturnRequestLoading = createSelector(getReturnRequestState, state => state.loading); + +export const getReturnRequestError = createSelector(getReturnRequestState, state => state.error); + +export const getReasons = createSelector(getReturnRequestState, state => state.reasons); + +export const getReturnableItems = createSelector(getReturnRequestState, state => state.orderReturnableItems); + +export const getGuestOrders = createSelector(getReturnRequestState, state => state.guestUserOrder); diff --git a/src/app/extensions/return-request/util/index.ts b/src/app/extensions/return-request/util/index.ts new file mode 100644 index 0000000000..b0b0cf3f42 --- /dev/null +++ b/src/app/extensions/return-request/util/index.ts @@ -0,0 +1,9 @@ +export function allowedStatus(status: string) { + return [ + 'STATE_COMMISSIONED_PARTLY_DISPATCHED', + 'STATE_DISPATCHED', + 'STATE_DISPATCHED_PARTLY_RETURNED', + 'EXPORTED', + 'STATE_COMMISSIONED_PARTLY_DISPATCHED_PARTLY_RETURNED', + ].includes(status); +} diff --git a/src/app/pages/account-order/account-order/account-order.component.html b/src/app/pages/account-order/account-order/account-order.component.html index af9337c2a2..c78e28f5d9 100644 --- a/src/app/pages/account-order/account-order/account-order.component.html +++ b/src/app/pages/account-order/account-order/account-order.component.html @@ -94,6 +94,7 @@

{{ 'account.orderdetails.heading.default' | translate }}

+ m.AccountOrderHistoryPageModule ), }, + { + path: 'returns-overview', + data: { breadcrumbData: [{ key: 'toolineo.account.return_overview.link' }] }, + loadChildren: () => + import('../../extensions/return-request/pages/return-request-routing.module').then( + m => m.ReturnRequestRoutingModule + ), + }, { path: 'payment', data: { breadcrumbData: [{ key: 'account.payment.link' }] }, diff --git a/src/app/pages/app-routing.module.ts b/src/app/pages/app-routing.module.ts index f388cd52eb..185a73a3d4 100644 --- a/src/app/pages/app-routing.module.ts +++ b/src/app/pages/app-routing.module.ts @@ -127,6 +127,19 @@ const routes: Routes = [ }, { path: 'cookies', loadChildren: () => import('./cookies/cookies-page.module').then(m => m.CookiesPageModule) }, { path: 'cobrowse', loadChildren: () => import('./co-browse/co-browse-page.module').then(m => m.CoBrowsePageModule) }, + { + path: 'return-request', + loadChildren: () => + import('../extensions/return-request/pages/return-request-guest/return-request-guest-page.module').then( + m => m.ReturnRequestGuestPageModule + ), + data: { + meta: { + title: 'toolineo.return_request.guest_title', + robots: 'noindex, nofollow', + }, + }, + }, ]; @NgModule({ diff --git a/src/app/shared/components/order/order-list/order-list.component.ts b/src/app/shared/components/order/order-list/order-list.component.ts index e099df7835..f2dbc38ce2 100644 --- a/src/app/shared/components/order/order-list/order-list.component.ts +++ b/src/app/shared/components/order/order-list/order-list.component.ts @@ -10,7 +10,6 @@ export type OrderColumnsType = | 'lineItems' | 'status' | 'destination' - | 'lineItems' | 'orderTotal'; /** diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index 45820d2c42..376ab7c8c4 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -34,6 +34,7 @@ import { QuickorderExportsModule } from '../extensions/quickorder/exports/quicko import { QuotingExportsModule } from '../extensions/quoting/exports/quoting-exports.module'; import { RatingExportsModule } from '../extensions/rating/exports/rating-exports.module'; import { RecentlyExportsModule } from '../extensions/recently/exports/recently-exports.module'; +import { ReturnRequestExportsModule } from '../extensions/return-request/exports/return-request-exports.module'; import { StoreLocatorExportsModule } from '../extensions/store-locator/exports/store-locator-exports.module'; import { TactonExportsModule } from '../extensions/tacton/exports/tacton-exports.module'; import { WishlistsExportsModule } from '../extensions/wishlists/exports/wishlists-exports.module'; @@ -187,6 +188,7 @@ const importExportModules = [ RatingExportsModule, ReactiveFormsModule, RecentlyExportsModule, + ReturnRequestExportsModule, RoleToggleModule, RouterModule, StoreLocatorExportsModule, diff --git a/src/assets/i18n/de_DE.json b/src/assets/i18n/de_DE.json index 11a74acde8..da4f1048e8 100644 --- a/src/assets/i18n/de_DE.json +++ b/src/assets/i18n/de_DE.json @@ -1278,5 +1278,70 @@ "tacton.submit.error.message": "Beim Verarbeiten Ihrer Anfrage ist ein technischer Fehler aufgetreten. Bitte versuchen Sie es später erneut.", "tacton.submit.success.message": "Ihre Anfrage wurde erfolgreich versendet. Wir werden {{0}} per E-Mail über den Status Ihrer Anfrage informieren.", "tacton.undo.tooltip": "Wiederherstellen", - "textarea.max_limit": "Ihre Nachricht: {{0}} Zeichen verbleibend." + "textarea.max_limit": "Ihre Nachricht: {{0}} Zeichen verbleibend.", + "toolineo.account.return_detail.before_date": "Vor. Abschlussdatum", + "toolineo.account.return_detail.button.download_return": "RETOURENLABEL DOWNLOADEN", + "toolineo.account.return_detail.created_on": "Erstellt am", + "toolineo.account.return_detail.credit_return_note": "Sollte Ihre Retoure eine Gutschrift beinhalten, erhalten Sie diese nach dem Abschluss der gesamten Retoure.", + "toolineo.account.return_detail.dealer.message_from": "NACHRICHT VOM {{1}}", + "toolineo.account.return_detail.dealer.title": "Nachrichten des Händlers", + "toolineo.account.return_detail.dealer_link_label": "Händler:", + "toolineo.account.return_detail.heading": "Retoure", + "toolineo.account.return_detail.message_to_dealer": "IHRE MITTEILUNG AN DEN HÄNDLER", + "toolineo.account.return_detail.order": "BESTELLUNG:", + "toolineo.account.return_detail.status": "Status:", + "toolineo.account.return_detail.table.header.article": "ARTIKEL", + "toolineo.account.return_detail.table.header.confirmed": "BESTATIGT", + "toolineo.account.return_detail.table.header.graduation_date": "ABSCHLUSSDATUM", + "toolineo.account.return_detail.table.header.quantity_reported": "GEMELDETE MENGE", + "toolineo.account.return_detail.table.header.reason": "GRUND", + "toolineo.account.return_detail.table.header.returns_required": "RUCKSENDUNG ERFORDERLICH", + "toolineo.account.return_detail.table.header.solution": "LÖSUNG", + "toolineo.account.return_overview.dropdown.options.label1": "Letzte 3 Monate", + "toolineo.account.return_overview.dropdown.options.label2": "Letzte 6 Monate", + "toolineo.account.return_overview.dropdown.options.label3": "Letzte 12 Monate", + "toolineo.account.return_overview.heading": "Retourenübersicht", + "toolineo.account.return_overview.link": "Retourenübersicht", + "toolineo.account.return_overview.no_result_found": "Es wurden keine Retourendatensätze gefunden", + "toolineo.account.return_overview.subtitle": "Sollte Ihre Rücksendung eine Gutschrift enthalten, erhalten Sie diese nach Abschluss der gesamten Rücksendung.", + "toolineo.account.return_overview.tab.all": "Alle", + "toolineo.account.return_overview.tab.completed": "Vollendet", + "toolineo.account.return_overview.tab.confirmed": "Bestätigt", + "toolineo.account.return_overview.tab.content.column.article": "Artikel", + "toolineo.account.return_overview.tab.content.column.created_on": "Erstellt am", + "toolineo.account.return_overview.tab.content.column.dealer": "Händler", + "toolineo.account.return_overview.tab.content.column.return": "Zurückkehren", + "toolineo.account.return_overview.tab.content.column.status": "Status", + "toolineo.account.return_overview.tab.content.download_return_label": "Laden Sie das Rücksendedokument herunter", + "toolineo.account.return_overview.tab.content.order_from": "Befehl {{0}} aus", + "toolineo.account.return_overview.tab.rejected": "Abgelehnt", + "toolineo.account.return_overview.tab.requested": "Angefordert", + "toolineo.account.return_request.btn.label": "Artikel Zuruckgeben", + "toolineo.account.return_request.modal.error.qty.max": "Ungültige Rückgabemenge", + "toolineo.account.return_request.modal.error.qty.required": "Menge ist erforderlich", + "toolineo.account.return_request.modal.error.reason.required": "Rückgabegrund ist erforderlich", + "toolineo.account.return_request.modal.form.btn.label": "ANFRAGE ABSCHICKEN", + "toolineo.account.return_request.modal.form.check_all_label": "Alle Positionen und Mengen auswählen", + "toolineo.account.return_request.modal.form.comment_label": "Ihr Kommentar", + "toolineo.account.return_request.modal.form.comment_sub_label": "Dieses Kommentar wird dem Händler mitgeteilt. Sollten Sie detaillierte Informationen zu Ihrer Anfrage haben, tragen Sie diese bitte hier mit ein.", + "toolineo.account.return_request.modal.form.select_reason_label": "Wählen Sie Grund aus", + "toolineo.account.return_request.modal.orde_date": "Bestelldatum:", + "toolineo.account.return_request.modal.orde_number": "Bestellnummer Händler:", + "toolineo.account.return_request.modal.submit_success": "Die Anfrage wurde erfolgreich übermittelt", + "toolineo.account.return_request.modal.summary.items": "Artikel", + "toolineo.account.return_request.modal.summary.title": "Sie fragen eine Retoure an für", + "toolineo.account.return_request.modal.table.heading.product_label": "Produktbezeichnung", + "toolineo.account.return_request.modal.table.heading.reason": "Grund", + "toolineo.account.return_request.modal.table.heading.return_qty": "Menge der Retoure", + "toolineo.account.return_request.modal.table.heading.returnable_qty": "Retournierbare Menge", + "toolineo.account.return_request.modal.table.product.EAN": "EAN:", + "toolineo.account.return_request.modal.table.product.item_number": "Art.Nr.:", + "toolineo.account.return_request.modal.table.product.manufacturer_number": "Hersteller-Nr.:", + "toolineo.account.return_request.modal.title": "Welche Artikel möchten Sie zurückgeben?", + "toolineo.return_request_guest.heading": "Ihre Bestellung", + "toolineo.return_request_guest.heading_info": "Bestelleingang. nach Stunde ersten der innerhalbMoglich", + "toolineo.return_request_guest.no_order_found": "Keine Bestellung gefunden", + "toolineo.return_request_guest.seo.title": "Anfrage zur Auftragsrückgabe", + "toolineo.return_request_guest.sub_heading": "Bestellung: {{0}}", + "toolineo.return_request_guest.sub_heading_info": "Die Zeit der Express-Stornierung ist abgelaufen. Für eine Stornierung nutzen Sie bitte das Kontaktformular." } diff --git a/src/assets/i18n/en_US.json b/src/assets/i18n/en_US.json index cea4d91179..f19def0f57 100644 --- a/src/assets/i18n/en_US.json +++ b/src/assets/i18n/en_US.json @@ -1278,5 +1278,70 @@ "tacton.submit.error.message": "A technical error occurred while processing your request. Please try again later.", "tacton.submit.success.message": "Your request has been successfully submitted. We will e-mail {{0}} to keep you updated on the status of your request.", "tacton.undo.tooltip": "Undo", - "textarea.max_limit": "Your message: {{0}} characters remaining." + "textarea.max_limit": "Your message: {{0}} characters remaining.", + "toolineo.account.return_detail.before_date": "Before. Graduation Date", + "toolineo.account.return_detail.button.download_return": "DOWNLOAD RETURN LABEL", + "toolineo.account.return_detail.created_on": "Created on", + "toolineo.account.return_detail.credit_return_note": "If your return includes a credit, you will receive this after the entire return has been completed.", + "toolineo.account.return_detail.dealer.message_from": "MESSAGE FROM", + "toolineo.account.return_detail.dealer.title": "Dealer messages", + "toolineo.account.return_detail.dealer_link_label": "Dealer:", + "toolineo.account.return_detail.heading": "Return", + "toolineo.account.return_detail.message_to_dealer": "YOUR MESSAGE TO THE DEALER", + "toolineo.account.return_detail.order": "ORDER:", + "toolineo.account.return_detail.status": "status:", + "toolineo.account.return_detail.table.header.article": "ARTICLE", + "toolineo.account.return_detail.table.header.confirmed": "CONFIRMED", + "toolineo.account.return_detail.table.header.graduation_date": "GRADUATION DATE", + "toolineo.account.return_detail.table.header.quantity_reported": "QUANTITY REPORTED", + "toolineo.account.return_detail.table.header.reason": "REASON", + "toolineo.account.return_detail.table.header.returns_required": "RETURN REQUIRED", + "toolineo.account.return_detail.table.header.solution": "SOLUTION", + "toolineo.account.return_overview.dropdown.options.label1": "Last 3 months", + "toolineo.account.return_overview.dropdown.options.label2": "Last 6 months", + "toolineo.account.return_overview.dropdown.options.label3": "Last 12 months", + "toolineo.account.return_overview.heading": "Returns overview", + "toolineo.account.return_overview.link": "Returns overview", + "toolineo.account.return_overview.no_result_found": "No return records were found", + "toolineo.account.return_overview.subtitle": "If your return contains a credit, you will receive it after the entire return has been completed.", + "toolineo.account.return_overview.tab.all": "All", + "toolineo.account.return_overview.tab.completed": "Completed", + "toolineo.account.return_overview.tab.confirmed": "Confirmed", + "toolineo.account.return_overview.tab.content.column.article": "Article", + "toolineo.account.return_overview.tab.content.column.created_on": "Created On", + "toolineo.account.return_overview.tab.content.column.dealer": "Dealer", + "toolineo.account.return_overview.tab.content.column.return": "Return", + "toolineo.account.return_overview.tab.content.column.status": "Status", + "toolineo.account.return_overview.tab.content.download_return_label": "Download Return document", + "toolineo.account.return_overview.tab.content.order_from": "Order {{0}} from", + "toolineo.account.return_overview.tab.rejected": "Rejected", + "toolineo.account.return_overview.tab.requested": "Requested", + "toolineo.account.return_request.btn.label": "Return item", + "toolineo.account.return_request.modal.error.qty.max": "Invalid return quantity", + "toolineo.account.return_request.modal.error.qty.required": "Quantity is required", + "toolineo.account.return_request.modal.error.reason.required": "Return reason is required", + "toolineo.account.return_request.modal.form.btn.label": "Submit Request", + "toolineo.account.return_request.modal.form.check_all_label": "Select all positions and quantities", + "toolineo.account.return_request.modal.form.comment_label": "Your comment", + "toolineo.account.return_request.modal.form.comment_sub_label": "This comment will be communicated to the dealer. If you have detailed information about your request, please enter it here.", + "toolineo.account.return_request.modal.form.select_reason_label": "Select Reason", + "toolineo.account.return_request.modal.orde_date": "Order Date:", + "toolineo.account.return_request.modal.orde_number": "Order number dealer:", + "toolineo.account.return_request.modal.submit_success": "Request has been submitted Successfylly", + "toolineo.account.return_request.modal.summary.items": "items", + "toolineo.account.return_request.modal.summary.title": "You request a return for", + "toolineo.account.return_request.modal.table.heading.product_label": "Product Name", + "toolineo.account.return_request.modal.table.heading.reason": "Reason", + "toolineo.account.return_request.modal.table.heading.return_qty": "Quantity of returns", + "toolineo.account.return_request.modal.table.heading.returnable_qty": "Returnable Quantity", + "toolineo.account.return_request.modal.table.product.EAN": "EAN:", + "toolineo.account.return_request.modal.table.product.item_number": "Item No.:", + "toolineo.account.return_request.modal.table.product.manufacturer_number": "Manufacturer No.:", + "toolineo.account.return_request.modal.title": "Which items would you like to return?", + "toolineo.return_request_guest.heading": "Your Order", + "toolineo.return_request_guest.heading_info": "Order received. after hour first of within Possible", + "toolineo.return_request_guest.no_order_found": "No order found", + "toolineo.return_request_guest.seo.title": "Request for order return", + "toolineo.return_request_guest.sub_heading": "Order: {{0}}", + "toolineo.return_request_guest.sub_heading_info": "The express cancellation time has expired. To cancel, please use the contact form." } diff --git a/src/assets/i18n/fr_FR.json b/src/assets/i18n/fr_FR.json index bc0c4bcbff..c58e20e041 100644 --- a/src/assets/i18n/fr_FR.json +++ b/src/assets/i18n/fr_FR.json @@ -1278,5 +1278,70 @@ "tacton.submit.error.message": "Une erreur technique s’est produite lors du traitement de votre demande. Veuillez réessayer plus tard.", "tacton.submit.success.message": "Votre demande de devis a été soumise avec succès. Nous vous enverrons un courriel à {{0}} pour vous tenir au courant du statut de votre demande.", "tacton.undo.tooltip": "Annuler", - "textarea.max_limit": "Votre message : {{0}} caractères restants." + "textarea.max_limit": "Votre message : {{0}} caractères restants.", + "toolineo.account.return_detail.before_date": "Avant. Date d'obtention du diplôme", + "toolineo.account.return_detail.button.download_return": "TÉLÉCHARGER L'ÉTIQUETTE DE RETOUR", + "toolineo.account.return_detail.created_on": "Créé sur", + "toolineo.account.return_detail.credit_return_note": "Si votre retour comprend un crédit, vous le recevrez une fois la totalité du retour terminée.", + "toolineo.account.return_detail.dealer.message_from": "MESSAGE DE {{1}}", + "toolineo.account.return_detail.dealer.title": "Messages du concessionnaire", + "toolineo.account.return_detail.dealer_link_label": "Marchand:", + "toolineo.account.return_detail.heading": "Return", + "toolineo.account.return_detail.message_to_dealer": "VOTRE MESSAGE AU CONCESSIONNAIRE", + "toolineo.account.return_detail.order": "UNE COMMANDE:", + "toolineo.account.return_detail.status": "statut:", + "toolineo.account.return_detail.table.header.article": "ARTICLE", + "toolineo.account.return_detail.table.header.confirmed": "CONFIRMÉ", + "toolineo.account.return_detail.table.header.graduation_date": "DATE D'OBTENTION DU DIPLÔME", + "toolineo.account.return_detail.table.header.quantity_reported": "QUANTITÉ DÉCLARÉE", + "toolineo.account.return_detail.table.header.reason": "RAISON", + "toolineo.account.return_detail.table.header.returns_required": "RETOUR OBLIGATOIRE", + "toolineo.account.return_detail.table.header.solution": "SOLUTION", + "toolineo.account.return_overview.dropdown.options.label1": "3 derniers mois", + "toolineo.account.return_overview.dropdown.options.label2": "6 derniers mois", + "toolineo.account.return_overview.dropdown.options.label3": "12 derniers mois", + "toolineo.account.return_overview.heading": "Aperçu des retours", + "toolineo.account.return_overview.link": "Aperçu des retours", + "toolineo.account.return_overview.no_result_found": "Aucun enregistrement de retour n'a été trouvé", + "toolineo.account.return_overview.subtitle": "Si votre retour contient un crédit, vous le recevrez une fois la totalité du retour terminée.", + "toolineo.account.return_overview.tab.all": "Tous", + "toolineo.account.return_overview.tab.completed": "Complété", + "toolineo.account.return_overview.tab.confirmed": "Confirmé", + "toolineo.account.return_overview.tab.content.column.article": "Article", + "toolineo.account.return_overview.tab.content.column.created_on": "Créé sur", + "toolineo.account.return_overview.tab.content.column.dealer": "Marchand", + "toolineo.account.return_overview.tab.content.column.return": "Rendre", + "toolineo.account.return_overview.tab.content.column.status": "statut", + "toolineo.account.return_overview.tab.content.download_return_label": "Téléchargez le document de retour", + "toolineo.account.return_overview.tab.content.order_from": "commande {{0}} hors de", + "toolineo.account.return_overview.tab.rejected": "Rejeté", + "toolineo.account.return_overview.tab.requested": "Demandé", + "toolineo.account.return_request.btn.label": "Retourner l'objet", + "toolineo.account.return_request.modal.error.qty.max": "Quantité retournée invalide", + "toolineo.account.return_request.modal.error.qty.required": "La quantité est requise", + "toolineo.account.return_request.modal.error.reason.required": "Le motif du retour est requis", + "toolineo.account.return_request.modal.form.btn.label": "Envoyer la demande", + "toolineo.account.return_request.modal.form.check_all_label": "Sélectionnez toutes les positions et quantités", + "toolineo.account.return_request.modal.form.comment_label": "votre commentaire", + "toolineo.account.return_request.modal.form.comment_sub_label": "Ce commentaire sera communiqué au concessionnaire. Si vous disposez d'informations détaillées sur votre demande, veuillez les saisir ici.", + "toolineo.account.return_request.modal.form.select_reason_label": "Sélectionnez la raison", + "toolineo.account.return_request.modal.orde_date": "Date de commande:", + "toolineo.account.return_request.modal.orde_number": "Numéro de commande revendeur:", + "toolineo.account.return_request.modal.submit_success": "La demande a été soumise avec succès", + "toolineo.account.return_request.modal.summary.items": "des articles", + "toolineo.account.return_request.modal.summary.title": "Vous demandez un retour pour", + "toolineo.account.return_request.modal.table.heading.product_label": "Nom du produit", + "toolineo.account.return_request.modal.table.heading.reason": "Raison", + "toolineo.account.return_request.modal.table.heading.return_qty": "Quantité de retours", + "toolineo.account.return_request.modal.table.heading.returnable_qty": "Quantité consignée", + "toolineo.account.return_request.modal.table.product.EAN": "EAN:", + "toolineo.account.return_request.modal.table.product.item_number": "Numéro d'article.:", + "toolineo.account.return_request.modal.table.product.manufacturer_number": "Numéro du fabricant.:", + "toolineo.account.return_request.modal.title": "Quels articles souhaiteriez-vous retourner?", + "toolineo.return_request_guest.heading": "Votre commande", + "toolineo.return_request_guest.heading_info": "Commande reçue. après l'heure, dans les 1 à 2 jours possibles", + "toolineo.return_request_guest.no_order_found": "Aucune commande trouvée", + "toolineo.return_request_guest.seo.title": "Demande de retour de commande", + "toolineo.return_request_guest.sub_heading": "Commande: {{0}}", + "toolineo.return_request_guest.sub_heading_info": "Le délai d'annulation express est expiré. Pour annuler, veuillez utiliser le Formulaire de contact." } diff --git a/src/environments/environment.model.ts b/src/environments/environment.model.ts index 9bf3f0a723..220c682a85 100644 --- a/src/environments/environment.model.ts +++ b/src/environments/environment.model.ts @@ -51,6 +51,7 @@ export interface Environment { | 'tracking' | 'tacton' | 'maps' + | 'returnRequest' )[]; /* ADDITIONAL FEATURE CONFIGURATIONS */