Skip to content

Commit

Permalink
feat: approve/reject a requisition
Browse files Browse the repository at this point in the history
  • Loading branch information
SGrueber committed Aug 14, 2020
1 parent 9ff5324 commit 9ce9991
Show file tree
Hide file tree
Showing 19 changed files with 475 additions and 47 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<ng-template #modal let-rejectModal>
<div class="modal-content">
<div class="modal-header">
<h2 class="modal-title">{{ 'approval.rejectform.reject_order.heading' | translate }}</h2>
<button class="close" (click)="hide()" [title]="'application.overlay.close.text' | translate" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>

<form [formGroup]="rejectForm" (ngSubmit)="submitForm()">
<div class="modal-body">
<div class="form-group clearfix">
<ish-textarea
[form]="rejectForm"
controlName="comment"
label="approval.rejectform.add_a_comment.label"
labelClass="col-12"
inputClass="col-12"
[errorMessages]="{ required: 'approval.rejectform.invalid_comment.error' }"
markRequiredLabel="off"
maxlength="1000"
rows="4"
></ish-textarea>
</div>
</div>

<div class="modal-footer">
<button class="btn btn-primary" type="submit" (click)="submitForm()" [disabled]="formDisabled">
{{ 'approval.rejectform.button.reject.label' | translate }}
</button>
<button class="btn btn-secondary" type="button" (click)="rejectModal.close('')">
{{ 'approval.rejectform.button.cancel.label' | translate }}
</button>
</div>
</form>
</div>
</ng-template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { ComponentFixture, TestBed, async } from '@angular/core/testing';
import { ReactiveFormsModule } from '@angular/forms';
import { NgbModalModule } from '@ng-bootstrap/ng-bootstrap';
import { TranslateModule } from '@ngx-translate/core';
import { MockComponent } from 'ng-mocks';
import { spy, verify } from 'ts-mockito';

import { TextareaComponent } from 'ish-shared/forms/components/textarea/textarea.component';

import { RequisitionRejectDialogComponent } from './requisition-reject-dialog.component';

describe('Requisition Reject Dialog Component', () => {
let component: RequisitionRejectDialogComponent;
let fixture: ComponentFixture<RequisitionRejectDialogComponent>;
let element: HTMLElement;

beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [MockComponent(TextareaComponent), RequisitionRejectDialogComponent],
imports: [NgbModalModule, ReactiveFormsModule, TranslateModule.forRoot()],
}).compileComponents();
}));

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

it('should be created', () => {
expect(component).toBeTruthy();
expect(element).toBeTruthy();
expect(() => fixture.detectChanges()).not.toThrow();
});

it('should emit approval comment when submit form was called and the form was valid', done => {
fixture.detectChanges();
component.rejectForm.setValue({
comment: 'test comment',
});

component.submit.subscribe(emit => {
expect(emit).toEqual('test comment');
done();
});

component.submitForm();
});

it('should not emit new approval comment when submit form was called and the form was invalid', () => {
fixture.detectChanges();
const emitter = spy(component.submit);
component.submitForm();

verify(emitter.emit()).never();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { ChangeDetectionStrategy, Component, EventEmitter, Output, TemplateRef, ViewChild } from '@angular/core';
import { FormControl, FormGroup, Validators } from '@angular/forms';
import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap';

import { markAsDirtyRecursive } from 'ish-shared/forms/utils/form-utils';

/**
* The Wishlist Reject Approval Dialog shows the modal to reject a requisition.
*
* @example
* <ish-requisition-reject-dialog
(submit)="rejectRequisition($event)">
</ish-requisition-reject-dialog>
*/
@Component({
selector: 'ish-requisition-reject-dialog',
templateUrl: './requisition-reject-dialog.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class RequisitionRejectDialogComponent {
/**
* Emits the reject event with the reject comment.
*/
@Output() submit = new EventEmitter<string>();

rejectForm: FormGroup;
submitted = false;

/**
* A reference to the current modal.
*/
modal: NgbModalRef;

@ViewChild('modal') modalTemplate: TemplateRef<unknown>;

constructor(private ngbModal: NgbModal) {
this.initForm();
}

initForm() {
this.rejectForm = new FormGroup({
comment: new FormControl('', Validators.required),
});
}

/** Emits the reject comment data, when the form was valid. */
submitForm() {
if (this.rejectForm.valid) {
this.submit.emit(this.rejectForm.get('comment').value);

this.hide();
} else {
this.submitted = true;
markAsDirtyRecursive(this.rejectForm);
}
}

/** Opens the modal. */
show() {
this.modal = this.ngbModal.open(this.modalTemplate);
}

/** Close the modal. */
hide() {
this.rejectForm.reset({
comment: '',
});
this.submitted = false;
if (this.modal) {
this.modal.close();
}
}

get formDisabled() {
return this.rejectForm.invalid && this.submitted;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
getRequisitionsLoading,
loadRequisition,
loadRequisitions,
updateRequisitionStatus,
} from '../store/requisitions';

// tslint:disable:member-ordering
Expand Down Expand Up @@ -55,4 +56,14 @@ export class RequisitionManagementFacade {
this.store.dispatch(loadRequisition({ requisitionId }));
return this.store.pipe(select(getRequisition));
}

approveRequisition$(requisitionId: string) {
this.store.dispatch(updateRequisitionStatus({ requisitionId, status: 'approved' }));
return this.store.pipe(select(getRequisition));
}

rejectRequisition$(requisitionId: string, comment?: string) {
this.store.dispatch(updateRequisitionStatus({ requisitionId, status: 'rejected', approvalComment: comment }));
return this.store.pipe(select(getRequisition));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,21 +56,26 @@ export class RequisitionMapper {

fromListData(payload: RequisitionData): Requisition[] {
if (Array.isArray(payload.data)) {
return payload.data.map(data => ({
...this.fromData({ ...payload, data }),
totals: {
itemTotal: data.totals ? PriceItemMapper.fromPriceItem(data.totals.itemTotal) : undefined,
total: data.totals
? PriceItemMapper.fromPriceItem(data.totals.grandTotal)
: {
type: 'PriceItem',
gross: data.totalGross.value,
net: data.totalNet.value,
currency: data.totalGross.currency,
},
isEstimated: false,
},
}));
return (
payload.data
/* filter requisitions that didn't need an approval */
.filter(data => data.requisitionNo)
.map(data => ({
...this.fromData({ ...payload, data }),
totals: {
itemTotal: data.totals ? PriceItemMapper.fromPriceItem(data.totals.itemTotal) : undefined,
total: data.totals
? PriceItemMapper.fromPriceItem(data.totals.grandTotal)
: {
type: 'PriceItem',
gross: data.totalGross.value,
net: data.totalNet.value,
currency: data.totalGross.currency,
},
isEstimated: false,
},
}))
);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
<div class="float-right">
<ul class="share-tools">
<li>
<a href="javascript:window.print();" class="link-print pull-right" rel="nofollow">
<a href="javascript:window.print();" class="link-print" rel="nofollow">
<fa-icon [icon]="['fas', 'print']"></fa-icon>
<span class="share-label">{{ 'account.orderdetails.print_link.text' | translate }}</span>
</a>
</li>
</ul>
</div>

<h1>{{ 'approval.detailspage.heading' | translate }}</h1>
<ish-error-message [error]="error$ | async"></ish-error-message>

Expand All @@ -16,6 +17,8 @@ <h1>{{ 'approval.detailspage.heading' | translate }}</h1>

<ish-requisition-buyer-approval [requisition]="requisition"></ish-requisition-buyer-approval>

<ng-container *ngTemplateOutlet="approvalButtonBar; context: { requisition: requisition }"></ng-container>

<h2>{{ 'approval.detailspage.order_details.heading' | translate }}</h2>

<div class="row d-flex">
Expand Down Expand Up @@ -65,6 +68,23 @@ <h3>{{ 'checkout.order_summary.heading' | translate }}</h3>
</div>
</div>

<ng-container *ngTemplateOutlet="approvalButtonBar; context: { requisition: requisition }"></ng-container>

<ng-template #approvalButtonBar let-requisition="requisition">
<div *ngIf="requisition.approval?.statusCode === 'pending' && view === 'approver'" class="section text-right">
<button (click)="rejectDialog.show()" type="button" class="btn btn-secondary">
{{ 'approval.detailspage.reject.button.label' | translate }}
</button>
<button (click)="approveRequisition(requisition.id)" type="button" class="btn btn-primary">
{{ 'approval.detailspage.approve.button.label' | translate }}
</button>
</div>
</ng-template>
<ish-requisition-reject-dialog
#rejectDialog
(submit)="rejectRequisition(requisition.id, $event)"
></ish-requisition-reject-dialog>

<div class="section d-flex d-flex justify-content-between">
<a [routerLink]="['../', { requisitionStatus: requisition.approval.statusCode }]">
<ng-container *ngIf="view === 'buyer'; else backToApprovals">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ 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 { of } from 'rxjs';
import { anyString, instance, mock, when } from 'ts-mockito';

import { AddressComponent } from 'ish-shared/components/address/address/address.component';
import { BasketCostSummaryComponent } from 'ish-shared/components/basket/basket-cost-summary/basket-cost-summary.component';
Expand All @@ -13,8 +14,10 @@ import { InfoBoxComponent } from 'ish-shared/components/common/info-box/info-box
import { LoadingComponent } from 'ish-shared/components/common/loading/loading.component';

import { RequisitionBuyerApprovalComponent } from '../../components/requisition/requisition-buyer-approval/requisition-buyer-approval.component';
import { RequisitionRejectDialogComponent } from '../../components/requisition/requisition-reject-dialog/requisition-reject-dialog.component';
import { RequisitionSummaryComponent } from '../../components/requisition/requisition-summary/requisition-summary.component';
import { RequisitionManagementFacade } from '../../facades/requisition-management.facade';
import { RequisitionView } from '../../models/requisition/requisition.model';

import { RequisitionDetailPageComponent } from './requisition-detail-page.component';

Expand All @@ -37,6 +40,7 @@ describe('Requisition Detail Page Component', () => {
MockComponent(LineItemListComponent),
MockComponent(LoadingComponent),
MockComponent(RequisitionBuyerApprovalComponent),
MockComponent(RequisitionRejectDialogComponent),
MockComponent(RequisitionSummaryComponent),
RequisitionDetailPageComponent,
],
Expand All @@ -48,11 +52,51 @@ describe('Requisition Detail Page Component', () => {
fixture = TestBed.createComponent(RequisitionDetailPageComponent);
component = fixture.componentInstance;
element = fixture.nativeElement;

const requisition = {
id: '4711',
requisitionNo: '4712',
approval: {
status: 'Approval Pending',
statusCode: 'pending',
customerApprovers: [
{ firstName: 'Jack', lastName: 'Link' },
{ firstName: 'Bernhhard', lastName: 'Boldner' },
],
},
user: { firstName: 'Patricia', lastName: 'Miller' },
totals: undefined,
creationDate: 24324321,
lineItemCount: 2,
lineItems: undefined,
} as RequisitionView;

when(reqFacade.requisition$(anyString())).thenReturn(of(requisition));
});

it('should be created', () => {
expect(component).toBeTruthy();
expect(element).toBeTruthy();
expect(() => fixture.detectChanges()).not.toThrow();
});

it('should display only the title if there is no requisition given', () => {
when(reqFacade.requisition$).thenReturn();

fixture.detectChanges();
expect(element).toMatchInlineSnapshot(`
<div class="float-right">
<ul class="share-tools">
<li>
<a class="link-print" href="javascript:window.print();" rel="nofollow"
><fa-icon ng-reflect-icon="fas,print"></fa-icon
><span class="share-label">account.orderdetails.print_link.text</span></a
>
</li>
</ul>
</div>
<h1>approval.detailspage.heading</h1>
<ish-error-message></ish-error-message>
`);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,15 @@ export class RequisitionDetailPageComponent implements OnInit, OnDestroy {
this.loading$ = this.requisitionManagementFacade.requisitionsLoading$;
}

approveRequisition(requisitionId: string) {
this.requisitionManagementFacade.approveRequisition$(requisitionId);
}

rejectRequisition(requisitionId: string, comment: string) {
this.requisitionManagementFacade.rejectRequisition$(requisitionId, comment);
return false;
}

ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
Expand Down
Loading

1 comment on commit 9ce9991

@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.