Skip to content

Commit

Permalink
feat: filter orders by date range, order number, sku and state
Browse files Browse the repository at this point in the history
  • Loading branch information
dhhyi committed Jan 11, 2024
1 parent 767ab5f commit 7f86757
Show file tree
Hide file tree
Showing 10 changed files with 317 additions and 3 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<form [formGroup]="form" (ngSubmit)="submitForm()">
<div class="row">
<div class="col-md-10">
<formly-form [form]="form" [fields]="fields" />
</div>

<div class="col-md-2 text-right d-flex flex-column justify-content-between pb-2">
<button type="reset" class="btn btn-secondary" (click)="resetForm()">
{{ 'account.order-history.filter.clear' | translate }}
</button>
<button type="submit" class="btn btn-primary">
{{ 'account.order-history.filter.apply' | translate }}
</button>
</div>
</div>
</form>
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ReactiveFormsModule } from '@angular/forms';
import { RouterTestingModule } from '@angular/router/testing';
import { FormlyForm } from '@ngx-formly/core';
import { TranslateModule } from '@ngx-translate/core';
import { MockComponent } from 'ng-mocks';

import { AccountOrderFiltersComponent } from './account-order-filters.component';

describe('Account Order Filters Component', () => {
let component: AccountOrderFiltersComponent;
let fixture: ComponentFixture<AccountOrderFiltersComponent>;
let element: HTMLElement;

beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [MockComponent(FormlyForm), ReactiveFormsModule, RouterTestingModule, TranslateModule.forRoot()],
declarations: [AccountOrderFiltersComponent],
}).compileComponents();
});

beforeEach(() => {
fixture = TestBed.createComponent(AccountOrderFiltersComponent);
component = fixture.componentInstance;
element = fixture.nativeElement;
});

it('should be created', () => {
expect(component).toBeTruthy();
expect(element).toBeTruthy();
expect(() => fixture.detectChanges()).not.toThrow();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
import {
AfterViewInit,
ChangeDetectionStrategy,
Component,
DestroyRef,
EventEmitter,
Injectable,
Input,
OnInit,
Output,
inject,
} from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { UntypedFormGroup } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { NgbDateAdapter, NgbDateStruct } from '@ng-bootstrap/ng-bootstrap';
import { FormlyFieldConfig } from '@ngx-formly/core';

import { OrderListQuery } from 'ish-core/models/order-list-query/order-list-query.model';

@Injectable()
export class OrderDateFilterAdapter extends NgbDateAdapter<string> {
fromModel(value: string): NgbDateStruct {
if (value) {
const dateParts = value.split('-');
return {
year: +dateParts[0],
month: +dateParts[1],
day: +dateParts[2],
};
}
}
toModel(date: NgbDateStruct): string {
if (date) {
return `${date.year}-${date.month.toString().padStart(2, '0')}-${date.day.toString().padStart(2, '0')}`;
}
}
}

interface FormModel extends Record<string, unknown> {
date?: {
fromDate: string;
toDate: string;
};
orderNo?: string;
sku?: string;
state?: string;
}

type UrlModel = Partial<Record<'from' | 'to' | 'orderNo' | 'sku' | 'state', string | string[]>>;

const filterableStates: OrderListQuery['statusCode'] = [
'NEW',
'INPROGRESS',
'CANCELLED',
'NOTDELIVERABLE',
'DELIVERED',
'RETURNED',
'PENDING',
'COMPLETED',
'MANUAL_INTERVENTION_NEEDED',
'EXPORTED',
'EXPORTFAILED',
'CANCELLEDANDEXPORTED',
];

function selectFirst(val: string | string[]): string {
return Array.isArray(val) ? val[0] : val;
}

function selectAll(val: string | string[]): string {
return Array.isArray(val) ? val.join(',') : val;
}

function selectArray(val: string | string[]): string[] {
if (!val) {
return;
}
return Array.isArray(val) ? val : [val];
}

function removeEmpty<T extends Record<string, unknown>>(obj: T): T {
return Object.keys(obj).reduce<Record<string, unknown>>((acc, key) => {
if (Array.isArray(obj[key])) {
if ((obj[key] as unknown[]).length > 0) {
acc[key] = obj[key];
}
} else if (obj[key]) {
acc[key] = obj[key];
}
return acc;
}, {}) as T;
}

function urlToModel(params: UrlModel): FormModel {
return removeEmpty<FormModel>({
date: {
fromDate: selectFirst(params.from),
toDate: selectFirst(params.to),
},
orderNo: selectAll(params.orderNo),
sku: selectAll(params.sku),
state: selectFirst(params.state),
});
}

function modelToUrl(model: FormModel): UrlModel {
return removeEmpty<UrlModel>({
from: model.date?.fromDate,
to: model.date?.toDate,
orderNo: model.orderNo?.split(',').map(s => s.trim()),
sku: model.sku?.split(',').map(s => s.trim()),
state: model.state
?.split(',')
.map(s => s.trim())
.filter(x => !!x) as OrderListQuery['statusCode'],
});
}

function urlToQuery(params: UrlModel): Partial<OrderListQuery> {
return removeEmpty<Partial<OrderListQuery>>({
creationDateFrom: selectFirst(params.from),
creationDateTo: selectFirst(params.to),
documentNumber: selectArray(params.orderNo),
lineItem_product: selectArray(params.sku),
statusCode: selectArray(params.state) as OrderListQuery['statusCode'],
});
}

@Component({
selector: 'ish-account-order-filters',
templateUrl: './account-order-filters.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [{ provide: NgbDateAdapter, useClass: OrderDateFilterAdapter }],
})
export class AccountOrderFiltersComponent implements OnInit, AfterViewInit {
@Input() fragmentOnRouting: string;

form = new UntypedFormGroup({});

fields: (FormlyFieldConfig & { key: keyof FormModel })[];

@Output() modelChange = new EventEmitter<Partial<OrderListQuery>>();

private destroyRef = inject(DestroyRef);

constructor(private route: ActivatedRoute, private router: Router) {}

ngOnInit() {
this.fields = [
{
key: 'orderNo',
type: 'ish-text-input-field',
props: {
placeholder: 'account.order-history.filter.label.order-no',
label: 'account.order-history.filter.label.order-no',
},
},
{
key: 'sku',
type: 'ish-text-input-field',
props: {
placeholder: 'account.order-history.filter.label.sku',
label: 'account.order-history.filter.label.sku',
},
},
{
key: 'state',
type: 'ish-select-field',
templateOptions: {
// keep-localization-pattern: ^account.order-history.filter.label.state.*
options: filterableStates.map(s => ({ label: `account.order-history.filter.label.state.${s}`, value: s })),
placeholder: 'account.order-history.filter.label.state',
label: 'account.order-history.filter.label.state',
},
},
{
key: 'date',
type: 'ish-date-range-picker-field',
props: {
placeholder: 'common.placeholder.shortdate-caps',
label: 'account.order-history.filter.label.date',
minDays: -365 * 2,
maxDays: 0,
startDate: -30,
},
},
];
}

ngAfterViewInit(): void {
this.route.queryParams.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(params => {
this.form.patchValue(urlToModel(params));
this.modelChange.emit(urlToQuery(params));
});
}

private navigate(queryParams: UrlModel) {
this.router.navigate([], {
relativeTo: this.route,
queryParams,
fragment: this.fragmentOnRouting,
});
}

submitForm() {
this.navigate(modelToUrl(this.form.value));
}

resetForm() {
this.navigate(undefined);
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
<a id="order-list-top" title="top"></a>
<h1>{{ 'account.order_history.heading' | translate }}</h1>
<ish-error-message [error]="ordersError$ | async" />
<p class="section">{{ 'account.order.subtitle' | translate }}</p>

<ish-account-order-filters fragmentOnRouting="order-list-top" (modelChange)="filter($event)" />

<ish-order-list
[orders]="orders$ | async"
[columnsToDisplay]="['creationDate', 'orderNoWithLink', 'lineItems', 'status', 'destination', 'orderTotal']"
noOrdersMessageKey="account.orderlist.no_placed_orders_message"
[noOrdersMessageKey]="!filtersActive ? 'account.orderlist.no_placed_orders_message' : undefined"
[loading]="ordersLoading$ | async"
/>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { AccountFacade } from 'ish-core/facades/account.facade';
import { ErrorMessageComponent } from 'ish-shared/components/common/error-message/error-message.component';
import { OrderListComponent } from 'ish-shared/components/order/order-list/order-list.component';

import { AccountOrderFiltersComponent } from './account-order-filters/account-order-filters.component';
import { AccountOrderHistoryPageComponent } from './account-order-history-page.component';

describe('Account Order History Page Component', () => {
Expand All @@ -19,6 +20,7 @@ describe('Account Order History Page Component', () => {
await TestBed.configureTestingModule({
declarations: [
AccountOrderHistoryPageComponent,
MockComponent(AccountOrderFiltersComponent),
MockComponent(ErrorMessageComponent),
MockComponent(OrderListComponent),
MockDirective(ServerHtmlDirective),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Observable } from 'rxjs';

import { AccountFacade } from 'ish-core/facades/account.facade';
import { HttpError } from 'ish-core/models/http-error/http-error.model';
import { OrderListQuery } from 'ish-core/models/order-list-query/order-list-query.model';
import { Order } from 'ish-core/models/order/order.model';

/**
Expand All @@ -18,17 +19,26 @@ export class AccountOrderHistoryPageComponent implements OnInit {
ordersLoading$: Observable<boolean>;
ordersError$: Observable<HttpError>;
moreOrdersAvailable$: Observable<boolean>;
filtersActive: boolean;

constructor(private accountFacade: AccountFacade) {}

ngOnInit(): void {
this.orders$ = this.accountFacade.orders$;
this.accountFacade.loadOrders({ limit: 30, include: ['commonShipToAddress'] });
this.ordersLoading$ = this.accountFacade.ordersLoading$;
this.ordersError$ = this.accountFacade.ordersError$;
this.moreOrdersAvailable$ = this.accountFacade.moreOrdersAvailable$;
}

filter(filters: Partial<OrderListQuery>) {
this.filtersActive = Object.keys(filters).length > 0;
this.accountFacade.loadOrders({
...filters,
limit: 30,
include: ['commonShipToAddress'],
});
}

loadMoreOrders(): void {
this.accountFacade.loadMoreOrders();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { RouterModule, Routes } from '@angular/router';

import { SharedModule } from 'ish-shared/shared.module';

import { AccountOrderFiltersComponent } from './account-order-filters/account-order-filters.component';
import { AccountOrderHistoryPageComponent } from './account-order-history-page.component';

const routes: Routes = [
Expand All @@ -19,6 +20,6 @@ const routes: Routes = [
@NgModule({
imports: [RouterModule.forChild(routes), SharedModule],
exports: [RouterModule],
declarations: [AccountOrderHistoryPageComponent],
declarations: [AccountOrderFiltersComponent, AccountOrderHistoryPageComponent],
})
export class AccountOrderHistoryPageModule {}
18 changes: 18 additions & 0 deletions src/assets/i18n/de_DE.json
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,24 @@
"account.notifications.table.notification": "Benachrichtigung",
"account.notifications.table.product": "Produkt",
"account.option.select.text": "Bitte auswählen",
"account.order-history.filter.apply": "Suchen",
"account.order-history.filter.clear": "Löschen",
"account.order-history.filter.label.date": "Zeitraum",
"account.order-history.filter.label.order-no": "Bestellnummer",
"account.order-history.filter.label.sku": "Artikelnummer",
"account.order-history.filter.label.state": "Bestellstatus",
"account.order-history.filter.label.state.CANCELLED": "Storniert",
"account.order-history.filter.label.state.CANCELLEDANDEXPORTED": "Storniert und exportiert",
"account.order-history.filter.label.state.COMPLETED": "Abgeschlossen",
"account.order-history.filter.label.state.DELIVERED": "Zugestellt",
"account.order-history.filter.label.state.EXPORTED": "Exportiert",
"account.order-history.filter.label.state.EXPORTFAILED": "Export fehlgeschlagen",
"account.order-history.filter.label.state.INPROGRESS": "In Bearbeitung",
"account.order-history.filter.label.state.MANUAL_INTERVENTION_NEEDED": "Manueller Eingriff erforderlich",
"account.order-history.filter.label.state.NEW": "Neu",
"account.order-history.filter.label.state.NOTDELIVERABLE": "Nicht zustellbar",
"account.order-history.filter.label.state.PENDING": "Ausstehend",
"account.order-history.filter.label.state.RETURNED": "Zurückgesendet",
"account.order.load_more": "Mehr laden",
"account.order.most_recent.heading": "Letzte Bestellungen",
"account.order.questions.note": "Besuchen Sie die <a href=\"{{0}}\">Hilfe</a> auf unserer Website für umfassende Bestell- und Versandinformationen oder <a href=\"{{1}}\">kontaktieren Sie uns</a> rund um die Uhr.",
Expand Down
18 changes: 18 additions & 0 deletions src/assets/i18n/en_US.json
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,24 @@
"account.notifications.table.notification": "Notification",
"account.notifications.table.product": "Product",
"account.option.select.text": "Please select",
"account.order-history.filter.apply": "Search",
"account.order-history.filter.clear": "Clear",
"account.order-history.filter.label.date": "Time frame",
"account.order-history.filter.label.order-no": "Order Number",
"account.order-history.filter.label.sku": "Product ID",
"account.order-history.filter.label.state": "Order Status",
"account.order-history.filter.label.state.CANCELLED": "Cancelled",
"account.order-history.filter.label.state.CANCELLEDANDEXPORTED": "Cancelled and Exported",
"account.order-history.filter.label.state.COMPLETED": "Completed",
"account.order-history.filter.label.state.DELIVERED": "Delivered",
"account.order-history.filter.label.state.EXPORTED": "Exported",
"account.order-history.filter.label.state.EXPORTFAILED": "Export Failed",
"account.order-history.filter.label.state.INPROGRESS": "In Progress",
"account.order-history.filter.label.state.MANUAL_INTERVENTION_NEEDED": "Manual Intervention Needed",
"account.order-history.filter.label.state.NEW": "New",
"account.order-history.filter.label.state.NOTDELIVERABLE": "Not Deliverable",
"account.order-history.filter.label.state.PENDING": "Pending",
"account.order-history.filter.label.state.RETURNED": "Returned",
"account.order.load_more": "Load More",
"account.order.most_recent.heading": "Most Recent Orders",
"account.order.questions.note": "Please visit the <a href=\"{{0}}\">Help</a> area of our website for comprehensive order and shipping information or <a href=\"{{1}}\">Contact Us</a> 24 hours a day.",
Expand Down

0 comments on commit 7f86757

Please sign in to comment.