-
Notifications
You must be signed in to change notification settings - Fork 83
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add cost center approval widget (#887)
- Loading branch information
Showing
21 changed files
with
486 additions
and
12 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
96 changes: 96 additions & 0 deletions
96
...n-detail/requisition-cost-center-approval/requisition-cost-center-approval.component.html
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
<ng-container *ngIf="approverEmail === (userEmail$ | async)"> | ||
<div *ngIf="costCenter" class="row"> | ||
<ish-info-box heading="approval.detailspage.costcenter.approval.heading" class="infobox-wrapper col-12"> | ||
<div class="row"> | ||
<div class="col-md-6"> | ||
<dl class="row dl-horizontal dl-separator"> | ||
<dt class="col-6">{{ 'approval.detailspage.cost_center.label' | translate }}</dt> | ||
<dd class="col-6">{{ costCenter.costCenterId }} {{ costCenter.name }}</dd> | ||
</dl> | ||
</div> | ||
</div> | ||
|
||
<div class="row section"> | ||
<div class="col-md-6 pt-1"> | ||
<dl class="row dl-horizontal dl-separator"> | ||
<dt class="col-6"> | ||
{{ 'approval.detailspage.cost_center.label' | translate }} {{ ccVal.budgetLabel | translate }} | ||
</dt> | ||
<dd class="col-6"> | ||
<strong> | ||
<ng-container *ngIf="costCenter?.budget; else noLimit"> | ||
{{ costCenter.budget | ishPrice }} | ||
</ng-container> | ||
<ng-template #noLimit> | ||
{{ 'account.budget.unlimited' | translate }} | ||
</ng-template> | ||
</strong> | ||
</dd> | ||
</dl> | ||
<dl *ngIf="costCenter.spentBudget" class="row dl-horizontal dl-separator"> | ||
<dt class="col-6">{{ 'account.budget.already_spent.label' | translate }}</dt> | ||
<dd class="col-6" [ngClass]="{ 'text-danger': ccVal.spentPercentage > 1 }"> | ||
{{ costCenter.spentBudget | ishPrice }} ({{ ccVal.spentPercentage | percent }}) | ||
</dd> | ||
</dl> | ||
<dl *ngIf="requisition.approval.statusCode !== 'APPROVED'" class="row dl-horizontal dl-separator"> | ||
<dt class="col-6">{{ 'approval.detailspage.budget.including_order.label' | translate }}</dt> | ||
<dd class="col-6" [ngClass]="{ 'text-danger': ccVal.spentPercentageIncludingThisRequisition > 1 }"> | ||
{{ ccVal.spentBudgetIncludingThisRequisition | ishPrice }} ({{ | ||
ccVal.spentPercentageIncludingThisRequisition | percent | ||
}}) | ||
</dd> | ||
</dl> | ||
</div> | ||
<div *ngIf="costCenter.budget" class="col-md-6 mb-2"> | ||
<ish-budget-bar | ||
[budget]="costCenter.budget" | ||
[spentBudget]="costCenter.spentBudget" | ||
[additionalAmount]="orderTotal" | ||
></ish-budget-bar> | ||
</div> | ||
</div> | ||
|
||
<div *ngIf="buyer" class="row" data-testing-id="cost-center-buyer-budget"> | ||
<div class="col-md-6"> | ||
<dl class="row dl-horizontal dl-separator"> | ||
<dt class="col-6"> | ||
{{ 'approval.detailspage.buyer.label' | translate }} {{ bVal.budgetLabel | translate }} | ||
</dt> | ||
<dd class="col-6"> | ||
<strong> | ||
<ng-container *ngIf="buyer?.budget; else noLimit"> | ||
{{ buyer.budget | ishPrice }} | ||
</ng-container> | ||
<ng-template #noLimit> | ||
{{ 'account.budget.unlimited' | translate }} | ||
</ng-template> | ||
</strong> | ||
</dd> | ||
</dl> | ||
<dl *ngIf="buyer.spentBudget" class="row dl-horizontal dl-separator"> | ||
<dt class="col-6">{{ 'account.budget.already_spent.label' | translate }}</dt> | ||
<dd class="col-6" [ngClass]="{ 'text-danger': bVal.spentPercentage > 1 }"> | ||
{{ buyer.spentBudget | ishPrice }} ({{ bVal.spentPercentage | percent }}) | ||
</dd> | ||
</dl> | ||
<dl *ngIf="requisition.approval.statusCode !== 'APPROVED'" class="row dl-horizontal dl-separator"> | ||
<dt class="col-6">{{ 'approval.detailspage.budget.including_order.label' | translate }}</dt> | ||
<dd class="col-6" [ngClass]="{ 'text-danger': bVal.spentPercentageIncludingThisRequisition > 1 }"> | ||
{{ bVal.spentBudgetIncludingThisRequisition | ishPrice }} ({{ | ||
bVal.spentPercentageIncludingThisRequisition | percent | ||
}}) | ||
</dd> | ||
</dl> | ||
</div> | ||
<div *ngIf="buyer.budget" class="col-md-6 mb-2"> | ||
<ish-budget-bar | ||
[budget]="buyer.budget" | ||
[spentBudget]="buyer.spentBudget" | ||
[additionalAmount]="orderTotal" | ||
></ish-budget-bar> | ||
</div> | ||
</div> | ||
</ish-info-box> | ||
</div> | ||
</ng-container> |
144 changes: 144 additions & 0 deletions
144
...etail/requisition-cost-center-approval/requisition-cost-center-approval.component.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,144 @@ | ||
import { CompilerOptions } from '@angular/core'; | ||
import { ComponentFixture, TestBed } from '@angular/core/testing'; | ||
import { TranslateModule, TranslateService } 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 { BasketTotal } from 'ish-core/models/basket-total/basket-total.model'; | ||
import { CostCenter, CostCenterBuyer } from 'ish-core/models/cost-center/cost-center.model'; | ||
import { PricePipe } from 'ish-core/models/price/price.pipe'; | ||
import { InfoBoxComponent } from 'ish-shared/components/common/info-box/info-box.component'; | ||
|
||
import { Requisition } from '../../../models/requisition/requisition.model'; | ||
import { BudgetBarComponent } from '../budget-bar/budget-bar.component'; | ||
|
||
import { RequisitionCostCenterApprovalComponent } from './requisition-cost-center-approval.component'; | ||
|
||
describe('Requisition Cost Center Approval Component', () => { | ||
let component: RequisitionCostCenterApprovalComponent; | ||
let fixture: ComponentFixture<RequisitionCostCenterApprovalComponent>; | ||
let element: HTMLElement; | ||
let accountFacade: AccountFacade; | ||
|
||
beforeEach(async () => { | ||
accountFacade = mock(AccountFacade); | ||
when(accountFacade.userPriceDisplayType$).thenReturn(of('gross')); | ||
|
||
await TestBed.configureTestingModule({ | ||
imports: [TranslateModule.forRoot()], | ||
declarations: [ | ||
MockComponent(BudgetBarComponent), | ||
MockComponent(InfoBoxComponent), | ||
PricePipe, | ||
RequisitionCostCenterApprovalComponent, | ||
], | ||
providers: [{ provide: AccountFacade, useFactory: () => instance(accountFacade) }], | ||
}) | ||
.configureCompiler({ preserveWhitespaces: true } as CompilerOptions) | ||
.compileComponents(); | ||
|
||
when(accountFacade.userEmail$).thenReturn(of('jlink@test.intershop.de')); | ||
}); | ||
|
||
beforeEach(() => { | ||
const translate = TestBed.inject(TranslateService); | ||
translate.use('en'); | ||
|
||
fixture = TestBed.createComponent(RequisitionCostCenterApprovalComponent); | ||
component = fixture.componentInstance; | ||
element = fixture.nativeElement; | ||
|
||
component.requisition = { | ||
id: 'testUUDI', | ||
requisitionNo: '0001', | ||
orderNo: '10001', | ||
approval: { | ||
statusCode: 'APPROVED', | ||
approver: { firstName: 'Bernhard', lastName: 'Boldner' }, | ||
approvalDate: 76543627, | ||
costCenterApproval: { | ||
approvers: [{ email: 'jlink@test.intershop.de' }], | ||
costCenter: { | ||
costCenterId: '100450', | ||
name: 'Headquarter', | ||
budgetPeriod: 'weekly', | ||
budget: { currency: 'USD', value: 3000, type: 'Money' }, | ||
spentBudget: { currency: 'USD', value: 300, type: 'Money' }, | ||
} as CostCenter, | ||
}, | ||
}, | ||
user: { firstName: 'Patricia', lastName: 'Miller', email: 'pmiller@test.intershop.de' }, | ||
totals: { | ||
total: { gross: 2000, net: 1800, currency: 'USD' }, | ||
} as BasketTotal, | ||
} as Requisition; | ||
}); | ||
|
||
it('should be created', () => { | ||
expect(component).toBeTruthy(); | ||
expect(element).toBeTruthy(); | ||
expect(() => fixture.detectChanges()).not.toThrow(); | ||
}); | ||
|
||
it('should display infobox if the cost center data are given', () => { | ||
component.ngOnChanges(); | ||
fixture.detectChanges(); | ||
expect(element.querySelector('ish-info-box')).toBeTruthy(); | ||
}); | ||
|
||
it('should not display infobox if there are no cost center data', () => { | ||
component.requisition.approval.costCenterApproval.costCenter = undefined; | ||
component.ngOnChanges(); | ||
fixture.detectChanges(); | ||
expect(element.querySelector('ish-info-box')).toBeFalsy(); | ||
}); | ||
|
||
it('should not display infobox if the current user is not the approver of the cost center', () => { | ||
when(accountFacade.userEmail$).thenReturn(of('bboldner@test.intershop.de')); | ||
|
||
component.ngOnChanges(); | ||
fixture.detectChanges(); | ||
expect(element.querySelector('ish-info-box')).toBeFalsy(); | ||
}); | ||
|
||
it('should display cost center budget information if created', () => { | ||
component.ngOnChanges(); | ||
fixture.detectChanges(); | ||
|
||
expect(element.textContent.replace(/^\s*[\r\n]*/gm, '')).toMatchInlineSnapshot(` | ||
"approval.detailspage.cost_center.label | ||
100450 Headquarter | ||
approval.detailspage.cost_center.label account.budget.type.weekly.label | ||
$3,000.00 | ||
account.budget.already_spent.label | ||
$300.00 (10%) | ||
" | ||
`); | ||
}); | ||
|
||
it('should display cost center buyer budget information if there are buyer data', () => { | ||
component.requisition.approval.costCenterApproval.costCenter.buyers = [ | ||
{ | ||
email: 'pmiller@test.intershop.de', | ||
budgetPeriod: 'monthly', | ||
budget: { currency: 'USD', value: 4000, type: 'Money' }, | ||
spentBudget: { currency: 'USD', value: 200, type: 'Money' }, | ||
} as CostCenterBuyer, | ||
]; | ||
|
||
component.ngOnChanges(); | ||
fixture.detectChanges(); | ||
|
||
expect( | ||
element.querySelector('[data-testing-id="cost-center-buyer-budget"]').textContent.replace(/^\s*[\r\n]*/gm, '') | ||
).toMatchInlineSnapshot(` | ||
"approval.detailspage.buyer.label account.budget.type.monthly.label | ||
$4,000.00 | ||
account.budget.already_spent.label | ||
$200.00 (5%) | ||
" | ||
`); | ||
}); | ||
}); |
80 changes: 80 additions & 0 deletions
80
...ion-detail/requisition-cost-center-approval/requisition-cost-center-approval.component.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
import { ChangeDetectionStrategy, Component, Input, OnChanges, OnInit } from '@angular/core'; | ||
import { Observable } from 'rxjs'; | ||
|
||
import { AccountFacade } from 'ish-core/facades/account.facade'; | ||
import { CostCenter, CostCenterBuyer } from 'ish-core/models/cost-center/cost-center.model'; | ||
import { Price, PriceHelper } from 'ish-core/models/price/price.model'; | ||
|
||
import { Requisition } from '../../../models/requisition/requisition.model'; | ||
|
||
interface BudgetValues { | ||
budgetLabel: string; | ||
spentPercentage: number; | ||
spentBudgetIncludingThisRequisition: Price; | ||
spentPercentageIncludingThisRequisition: number; | ||
} | ||
|
||
@Component({ | ||
selector: 'ish-requisition-cost-center-approval', | ||
templateUrl: './requisition-cost-center-approval.component.html', | ||
changeDetection: ChangeDetectionStrategy.OnPush, | ||
}) | ||
export class RequisitionCostCenterApprovalComponent implements OnInit, OnChanges { | ||
@Input() requisition: Requisition; | ||
|
||
costCenter: CostCenter; | ||
approverEmail: string; | ||
orderTotal: Price; | ||
buyer: CostCenterBuyer; | ||
|
||
ccVal: BudgetValues; | ||
bVal: BudgetValues; // buyer budget values | ||
|
||
userEmail$: Observable<string>; | ||
|
||
constructor(private accountFacade: AccountFacade) {} | ||
|
||
ngOnInit() { | ||
this.userEmail$ = this.accountFacade.userEmail$; | ||
} | ||
|
||
ngOnChanges() { | ||
this.costCenter = this.requisition?.approval?.costCenterApproval?.costCenter; | ||
|
||
this.approverEmail = this.requisition?.approval?.costCenterApproval?.approvers?.length | ||
? this.requisition.approval.costCenterApproval.approvers[0].email | ||
: undefined; | ||
|
||
this.orderTotal = { | ||
type: 'Money', | ||
value: this.requisition?.totals?.total?.gross, | ||
currency: this.requisition?.totals?.total?.currency, | ||
}; | ||
|
||
if (this.costCenter) { | ||
this.ccVal = this.determineBudgetValues(this.costCenter); | ||
|
||
this.buyer = this.costCenter.buyers?.find(buyer => buyer.email === this.requisition?.user.email); | ||
this.bVal = this.determineBudgetValues(this.buyer); | ||
} | ||
} | ||
|
||
/** calculates all displayed prices and percentages for a budget related object */ | ||
private determineBudgetValues(data: CostCenter | CostCenterBuyer): BudgetValues { | ||
if (!data) { | ||
return; | ||
} | ||
|
||
const spentBudgetIncludingThisRequisition = PriceHelper.sum(data?.spentBudget, this.orderTotal); | ||
|
||
return { | ||
budgetLabel: `account.budget.type.${data.budgetPeriod}.label`, | ||
spentPercentage: data?.spentBudget?.value ? data.spentBudget?.value / data.budget.value : 0, | ||
spentBudgetIncludingThisRequisition, | ||
spentPercentageIncludingThisRequisition: | ||
spentBudgetIncludingThisRequisition?.value && data?.budget?.value | ||
? spentBudgetIncludingThisRequisition.value / data?.budget?.value | ||
: 0, | ||
}; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.