Skip to content

Commit

Permalink
feat: extend requisition model to show requisition list
Browse files Browse the repository at this point in the history
  • Loading branch information
Silke Grueber authored and Silke Grueber committed Jul 31, 2020
1 parent 88bd4d6 commit e7f9ae5
Show file tree
Hide file tree
Showing 25 changed files with 307 additions and 52 deletions.
Original file line number Diff line number Diff line change
@@ -1,8 +1,84 @@
<div class="list-body" data-testing-id="requisition-list">
<ng-container *ngFor="let requisition of requisitions">
<div>
<!-- TODO: routing must work for standalone usage, needs to be more relative without the 'account' part -->
<a [routerLink]="['/account/requisitions', requisition.id]">{{ requisition | json }}</a>
</div>
</ng-container>
</div>
<ng-container *ngIf="requisitions?.length > 0; else emptyList">
<div class="list-body" data-testing-id="requisition-list">

<div class="list-header d-md-flex">
<div class="list-header-item col-sm-2 list-header-item-descr">
{{ 'account.approvallist.table.id_of_order' | translate }}
</div>
<div *ngIf="requisitionStatus === 'approved'" class="list-header-item col-sm-2 list-header-item-descr">
{{ 'account.approvallist.table.no_of_order' | translate }}
</div>
<div class="list-header-item col-sm-2 list-header-item-descr">
{{ 'account.approvallist.table.date_of_order' | translate }}
</div>
<div *ngIf="view === 'buyer' && requisitionStatus !== 'pending'" class="list-header-item col-sm-2 list-header-item-descr">
{{ 'account.approvallist.table.approver' | translate }}
</div>
<div *ngIf="view === 'approver'" class="list-header-item col-sm-2 list-header-item-descr">
{{ 'account.approvallist.table.purchaser' | translate }}
</div>
<div *ngIf="requisitionStatus !== 'pending'" class="list-header-item col-sm-2 list-header-item-descr">
<ng-container *ngIf="requisitionStatus === 'approved'; else tableHeaderItemRejected">
{{ 'account.approvallist.table.approval_date' | translate }}
</ng-container>
<ng-template #tableHeaderItemRejected> {{ 'account.approvallist.table.reject_date' | translate }}</ng-template>
</div>
<div *ngIf="requisitionStatus !== 'approved'" class="list-header-item col-sm-2 list-header-item-descr">
{{ 'account.approvallist.table.line_items' | translate }}
</div>
<div class="list-header-item col-sm-2 list-header-item-descr text-right">
{{ 'account.approvallist.table.line_item_total' | translate }}
</div>
</div>
<div class="list-body">
<ng-container *ngFor="let requisition of requisitions">
<div class="list-item-row-big list-item-row d-flex flex-row flex-wrap">
<!--Requisition No -->
<div class="col-12 col-md-2 list-item">
<label class="d-xl-none d-lg-none d-md-none control-label">
{{ 'account.approvallist.table.id_of_order' | translate }}</label
>
<!-- TODO: routing must work for standalone usage, needs to be more relative without the 'account' part -->
<a [routerLink]="['/account/requisitions', requisition.id]">{{ requisition.requisitionNo }}</a>
</div>

<!--Order No -->
<div *ngIf="requisitionStatus === 'approved'" class="col-12 col-md-2 list-item">
{{ requisition.orderNo }}
</div>

<!-- Creation Date -->
<div class="col-7 col-md-2 list-item">{{ requisition.creationDate | ishDate }}</div>

<!-- Approver -->
<div *ngIf="view === 'buyer' && requisitionStatus !== 'pending'" class="col-7 col-md-2 list-item">
{{ requisition.approval?.approver?.firstName }} {{ requisition.approval?.approver?.lastName }}
</div>

<!-- Buyer -->
<div *ngIf="view === 'approver'" class="col-7 col-md-2 list-item">
{{ requisition.user.firstName }} {{ requisition.user.lastName }}
</div>

<!-- Approval/Rejection Date -->
<div *ngIf="requisitionStatus !== 'pending'" class="col-7 col-md-2 list-item">
{{ requisition.approval.approvalDate | ishDate }}
</div>

<!-- Items count -->
<div *ngIf="requisitionStatus !== 'approved'" class="col-7 col-md-2 list-item">
{{ requisition.lineItemCount }}
</div>

<!-- Total -->
<div class="col-7 col-md-2 list-item text-right">{{ requisition.totals.total | ishPrice: 'gross' }}</div>
</div>
</ng-container>
</div>

</div>
</ng-container>
<ng-template #emptyList>
<p data-testing-id="emptyList">{{ 'account.approvallist.no_items_message' | translate }}</p>
</ng-template>

Original file line number Diff line number Diff line change
@@ -1,26 +1,30 @@
import { ComponentFixture, TestBed, async } from '@angular/core/testing';
import { MockComponent } from 'ng-mocks';
import { instance, mock } from 'ts-mockito';
import { RouterTestingModule } from '@angular/router/testing';
import { TranslateModule } from '@ngx-translate/core';
import { MockComponent, MockPipe } from 'ng-mocks';

import { PricePipe } from 'ish-core/models/price/price.pipe';
import { DatePipe } from 'ish-core/pipes/date.pipe';
import { ErrorMessageComponent } from 'ish-shared/components/common/error-message/error-message.component';
import { LoadingComponent } from 'ish-shared/components/common/loading/loading.component';

import { RequisitionManagementFacade } from '../../facades/requisition-management.facade';

import { RequisitionsListComponent } from './requisitions-list.component';

describe('Requisitions List Component', () => {
let component: RequisitionsListComponent;
let fixture: ComponentFixture<RequisitionsListComponent>;
let element: HTMLElement;
let requisitionManagementFacade: RequisitionManagementFacade;

beforeEach(async(() => {
requisitionManagementFacade = mock(RequisitionManagementFacade);

TestBed.configureTestingModule({
declarations: [MockComponent(ErrorMessageComponent), MockComponent(LoadingComponent), RequisitionsListComponent],
providers: [{ provide: RequisitionManagementFacade, useFactory: () => instance(requisitionManagementFacade) }],
imports: [RouterTestingModule, TranslateModule.forRoot()],
declarations: [
MockComponent(ErrorMessageComponent),
MockComponent(LoadingComponent),
MockPipe(DatePipe),
MockPipe(PricePipe),
RequisitionsListComponent,
],
}).compileComponents();
}));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,6 @@ export class RequisitionsListComponent {
* The requisitions to be listed
*/
@Input() requisitions: Requisition[];
@Input() requisitionStatus = 'pending';
@Input() view: 'buyer' | 'approver' = 'buyer';
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { Injectable } from '@angular/core';
import { Store, select } from '@ngrx/store';
import { combineLatest } from 'rxjs';
import { map, switchMapTo, tap } from 'rxjs/operators';
import { distinctUntilChanged, map, switchMapTo, tap, withLatestFrom } from 'rxjs/operators';

import { selectRouteParam, selectUrl } from 'ish-core/store/core/router';
import { log } from 'ish-core/utils/dev/operators';
Expand All @@ -11,6 +10,7 @@ import {
getRequisitions,
getRequisitionsError,
getRequisitionsLoading,
getRequisitionsStatus,
loadRequisition,
loadRequisitions,
} from '../store/requisitions';
Expand All @@ -22,14 +22,17 @@ export class RequisitionManagementFacade {

requisitionsError$ = this.store.pipe(select(getRequisitionsError));
requisitionsLoading$ = this.store.pipe(select(getRequisitionsLoading));
requisitionsStatus$ = this.store.pipe(select(getRequisitionsStatus));

requisitions$ = combineLatest([
this.store.pipe(select(selectRouteParam('status'))),
this.store.pipe(
select(selectUrl),
map(url => (url.includes('/buyer/') ? 'buyer' : 'approver'))
requisitions$ = this.store.pipe(
select(selectRouteParam('status')),
withLatestFrom(
this.store.pipe(
select(selectUrl),
map(url => (url.includes('/buyer/') ? 'buyer' : 'approver'))
)
),
]).pipe(
distinctUntilChanged(),
log('facade requisitions'),
tap(([status, view]) => this.store.dispatch(loadRequisitions({ view, status }))),
switchMapTo(this.store.pipe(select(getRequisitions)))
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,21 @@
import { User } from '@sentry/browser';

import { BasketTotalData } from 'ish-core/models/basket-total/basket-total.interface';
import { PriceData } from 'ish-core/models/price/price.interface';

import { RequisitionApproval } from './requisition.model';

export interface RequisitionData {
id: string;
requisitionNo: string;
user: string;
approvalStatus: { status: string };
orderNo?: string;
creationDate: number;
lineItemCount: number;
totals?: BasketTotalData;
totalGross: PriceData;
totalNet: PriceData;

userInformation: User;

approvalStatus: RequisitionApproval;
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,41 @@ describe('Requisition Mapper', () => {
const data: RequisitionData = {
id: 'testUUDI',
requisitionNo: '0001',
user: 'test@user.com',
creationDate: 12345678,
lineItemCount: 2,
approvalStatus: { status: 'pending' },
userInformation: { firstName: 'Patricia', lastName: 'Miller', email: 'pmiller@test.intershop.de' },
totalGross: { currency: 'USD', value: 2000 },
totalNet: { currency: 'USD', value: 1890 },
};
const mapped = requisitionMapper.fromData(data);
expect(mapped).toHaveProperty('id', 'testUUDI');
expect(mapped).toHaveProperty('requisitionNo', '0001');
expect(mapped).toHaveProperty('user', 'test@user.com');
expect(mapped).toHaveProperty('approvalStatus', 'pending');
expect(mapped).toMatchInlineSnapshot(`
Object {
"approval": Object {
"status": "pending",
},
"creationDate": 12345678,
"id": "testUUDI",
"lineItemCount": 2,
"orderNo": undefined,
"requisitionNo": "0001",
"totals": Object {
"isEstimated": false,
"itemTotal": undefined,
"total": Object {
"currency": "USD",
"gross": 2000,
"net": 1890,
"type": "PriceItem",
},
},
"user": Object {
"email": "pmiller@test.intershop.de",
"firstName": "Patricia",
"lastName": "Miller",
},
}
`);
});
});
});
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { Injectable } from '@angular/core';

import { PriceItemMapper } from 'ish-core/models/price-item/price-item.mapper';

import { RequisitionData } from './requisition.interface';
import { Requisition } from './requisition.model';

Expand All @@ -14,8 +16,25 @@ export class RequisitionMapper {
return {
id: requisitionData.id,
requisitionNo: requisitionData.requisitionNo,
user: requisitionData.user,
approvalStatus: requisitionData.approvalStatus.status,
orderNo: requisitionData.orderNo,
creationDate: requisitionData.creationDate,
lineItemCount: requisitionData.lineItemCount,
totals: {
itemTotal: requisitionData.totals
? PriceItemMapper.fromPriceItem(requisitionData.totals.itemTotal)
: undefined,
total: requisitionData.totals
? PriceItemMapper.fromPriceItem(requisitionData.totals.grandTotal)
: {
type: 'PriceItem',
gross: requisitionData.totalGross.value,
net: requisitionData.totalNet.value,
currency: requisitionData.totalGross.currency,
},
isEstimated: false,
},
user: requisitionData.userInformation,
approval: requisitionData.approvalStatus,
};
} else {
throw new Error(`requisitionData is required`);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,21 @@
import { User } from '@sentry/browser';

import { BasketTotal } from 'ish-core/models/basket-total/basket-total.model';

export interface RequisitionApproval {
status: string;
approvalDate?: number;
approver?: { firstName: string; lastName: string };
}

export interface Requisition {
id: string;
requisitionNo: string;
user: string;
approvalStatus: string;
orderNo?: string;
creationDate: number;
lineItemCount: number;
totals: BasketTotal;

user: User;
approval: RequisitionApproval;
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@ <h1>{{ 'account.requisitions.approvals' | translate }}</h1>

<p>{{ 'account.requisitions.approvals.text' | translate }}</p>

<a routerLink="../pending" routerLinkActive="btn-default" class="btn">Waiting for my Approval</a>
<a routerLink="../approved" routerLinkActive="btn-default" class="btn">Approved by me</a>
<a routerLink="../rejected" routerLinkActive="btn-default" class="btn">Rejected by me</a>
<a routerLink="../pending" routerLinkActive="btn-default" class="btn">Waiting for your Approval</a>
<a routerLink="../approved" routerLinkActive="btn-default" class="btn">Approved by you</a>
<a routerLink="../rejected" routerLinkActive="btn-default" class="btn">Rejected by you</a>

<div class="section loading-container">
<ish-error-message [error]="error$ | async"></ish-error-message>

<ish-requisitions-list [requisitions]="requisitions$ | async"></ish-requisitions-list>
<ish-requisitions-list [requisitions]="requisitions$ | async" [requisitionStatus]="status$ | async" view="approver"></ish-requisitions-list>

<ish-loading *ngIf="loading$ | async"></ish-loading>
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,33 @@ import { ComponentFixture, TestBed, async } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { TranslateModule } from '@ngx-translate/core';
import { MockComponent } from 'ng-mocks';
import { instance, mock } from 'ts-mockito';

import { ErrorMessageComponent } from 'ish-shared/components/common/error-message/error-message.component';
import { LoadingComponent } from 'ish-shared/components/common/loading/loading.component';

import { RequisitionsListComponent } from '../../components/requisitions-list/requisitions-list.component';
import { RequisitionManagementFacade } from '../../facades/requisition-management.facade';

import { ApproverPageComponent } from './approver-page.component';

describe('Approver Page Component', () => {
let component: ApproverPageComponent;
let fixture: ComponentFixture<ApproverPageComponent>;
let element: HTMLElement;
let reqFacade: RequisitionManagementFacade;

beforeEach(async(() => {
reqFacade = mock(RequisitionManagementFacade);
TestBed.configureTestingModule({
imports: [RouterTestingModule, TranslateModule.forRoot()],
declarations: [ApproverPageComponent, MockComponent(RequisitionsListComponent)],
declarations: [
ApproverPageComponent,
MockComponent(ErrorMessageComponent),
MockComponent(LoadingComponent),
MockComponent(RequisitionsListComponent),
],
providers: [{ provide: RequisitionManagementFacade, useFactory: () => instance(reqFacade) }],
}).compileComponents();
}));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,14 @@ export class ApproverPageComponent implements OnInit {
requisitions$: Observable<Requisition[]>;
error$: Observable<HttpError>;
loading$: Observable<boolean>;
status$: Observable<string>;

constructor(private requisitionManagementFacade: RequisitionManagementFacade) {}

ngOnInit() {
this.requisitions$ = this.requisitionManagementFacade.requisitions$;
this.error$ = this.requisitionManagementFacade.requisitionsError$;
this.loading$ = this.requisitionManagementFacade.requisitionsLoading$;
this.status$ = this.requisitionManagementFacade.requisitionsStatus$;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ <h1>{{ 'account.requisitions.requisitions' | translate }}</h1>
<div class="section loading-container">
<ish-error-message [error]="error$ | async"></ish-error-message>

<ish-requisitions-list [requisitions]="requisitions$ | async"></ish-requisitions-list>
<ish-requisitions-list [requisitions]="requisitions$ | async" [requisitionStatus]="status$ | async" view="buyer"></ish-requisitions-list>

<ish-loading *ngIf="loading$ | async"></ish-loading>
</div>
Loading

1 comment on commit e7f9ae5

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Azure Demo Servers are available:

Please sign in to comment.