Skip to content

Commit

Permalink
Merge pull request #362 from AllianceBioversityCIAT/staging
Browse files Browse the repository at this point in the history
Tracking of outcomes achievements against targets
  • Loading branch information
Cristian45 authored Dec 3, 2024
2 parents e969590 + 3f181ae commit 21b2dee
Show file tree
Hide file tree
Showing 91 changed files with 4,061 additions and 148 deletions.
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
<div [style]="inlineStylesContainer">
<app-pr-field-header [label]="this.label" [description]="this.description" [required]="this.required"
[showDescriptionLabel]="this.showDescriptionLabel" [descInlineStyles]="this.descInlineStyles"
[labelDescInlineStyles]="this.labelDescInlineStyles"
Expand All @@ -12,7 +13,7 @@
'Not provided' :
'Not applicable'))"></div>
<div class="custom_select" *ngSwitchCase="false">
<a class="field" tabindex="0" [id]="this.optionValue+indexReference??''"
<a class="field" tabindex="0" [id]="this.optionValue + (indexReference || '')"
[ngClass]="{'select-disable': this.disabled}" [ngClass]="{'globalDisabled': fieldDisabled}">
<div class="text"
[ngClass]="{'select_placeholder': !(this.optionsIntance | labelName:this.value:this.optionValue:this.optionLabel), 'truncate': this.truncateSelectionText}"
Expand Down Expand Up @@ -57,3 +58,4 @@
description="If you don't find the partner you are looking for, <a class='open_route pSelectP'>request</a> to have it added to the list. Please note that once your partner request is approved, it could take up to an hour to be available in the CLARISA institutions list.">
</app-alert-status>
</div>
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@
}
}

.pr-field {
width: 100%;
}

.pr-field .read-only {
padding: 8px 10px;
box-shadow: 0px 0px 14px 0px #dcdcdc;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Component, forwardRef, Input, Output, EventEmitter, SimpleChanges } from '@angular/core';
import { Component, forwardRef, Input, Output, EventEmitter } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { RolesService } from '../../shared/services/global/roles.service';
import { DataControlService } from '../../shared/services/data-control.service';
Expand Down Expand Up @@ -43,6 +43,7 @@ export class PrSelectComponent implements ControlValueAccessor {
@Input() optionsInlineStyles?: string = '';
@Input() showDescriptionLabel?: boolean = false;
@Input() truncateSelectionText?: boolean = false;
@Input() inlineStylesContainer?: string = '';
@Input() _value: string;

@Output() selectOptionEvent = new EventEmitter();
Expand Down Expand Up @@ -82,7 +83,7 @@ export class PrSelectComponent implements ControlValueAccessor {

removeFocus(option?) {
if (option?.disabled) return;
const element: any = document.getElementById(this.optionValue + this.indexReference ?? '');
const element: any = document.getElementById(this.optionValue + (this.indexReference || ''));
element.blur();
}
get optionsIntance() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { NgModule } from '@angular/core';
import { OutcomeIndicatorComponent } from './outcome-indicator.component';
import { OutcomeIndicatorRouting } from '../../shared/routing/routing-data';
import { RouterModule, Routes } from '@angular/router';

const routes: Routes = [{ path: '', component: OutcomeIndicatorComponent, children: OutcomeIndicatorRouting }];

@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class OutcomeIndicatorRoutingModule {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<div class="local_container section_container">
@if (!router.url.includes('/outcome-indicator-module/indicator-details')) {
<app-pr-select
[options]="this.api.rolesSE.isAdmin ? this.allInitiatives : this.api.dataControlSE.myInitiativesList"
label="Initiative"
[required]="false"
[isStatic]="true"
optionLabel="full_name"
optionValue="official_code"
placeholder="Select Initiative"
labelDescInlineStyles="color: #5569dd"
[(ngModel)]="this.outcomeIService.initiativeIdFilter"
inlineStylesContainer="display: flex; flex-direction: row; gap: 10px; align-items: center; width: 100%; margin-top: 20px;"
labelDescInlineStyles="color: #5569dd; margin: 0;"
(ngModelChange)="handleInitiativeChange()">
</app-pr-select>

<div class="separator_line"></div>
}

<router-outlet></router-outlet>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
.local_container {
margin-top: 20px !important;
margin-bottom: 20px !important;
padding: 0px 40px;
padding-top: 15px !important;
padding-bottom: 50px;
}

p {
margin: 0;
}

.separator_line {
margin-top: 20px;
margin-bottom: 20px;
border-top: 2px solid #5569dd;
box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.2);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { OutcomeIndicatorComponent } from './outcome-indicator.component';
import { RouterTestingModule } from '@angular/router/testing';
import { CustomFieldsModule } from '../../custom-fields/custom-fields.module';
import { of, throwError } from 'rxjs';
import { HttpClientTestingModule } from '@angular/common/http/testing';

describe('OutcomeIndicatorComponent', () => {
let component: OutcomeIndicatorComponent;
let fixture: ComponentFixture<OutcomeIndicatorComponent>;

beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [OutcomeIndicatorComponent],
imports: [RouterTestingModule, CustomFieldsModule, HttpClientTestingModule]
}).compileComponents();
});

beforeEach(() => {
fixture = TestBed.createComponent(OutcomeIndicatorComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});

it('should initialize component for admin user', async () => {
component.api.rolesSE.isAdmin = true;
jest.spyOn(component, 'loadAllInitiatives');
jest.spyOn(component.api.dataControlSE, 'getCurrentPhases');

await component.initializeComponent();

expect(component.api.dataControlSE.getCurrentPhases).toHaveBeenCalled();
expect(component.loadAllInitiatives).toHaveBeenCalled();
});

it('should initialize component for non-admin user', async () => {
component.api.rolesSE.isAdmin = false;
jest.spyOn(component.api, 'updateUserData');
jest.spyOn(component.api.dataControlSE, 'getCurrentPhases');

await component.initializeComponent();

expect(component.api.dataControlSE.getCurrentPhases).toHaveBeenCalled();
expect(component.api.updateUserData).toHaveBeenCalled();
});

it('should set default initiative for non-admin user', () => {
const mockInitiative = { official_code: 'TEST1' };
component.api.dataControlSE.myInitiativesList = [mockInitiative];
jest.spyOn(component, 'updateQueryParams');
jest.spyOn(component.outcomeIService, 'getEOIsData');
jest.spyOn(component.outcomeIService, 'getWorkPackagesData');

component.setDefaultInitiativeForNonAdmin();

expect(component.outcomeIService.initiativeIdFilter).toBe('TEST1');
expect(component.updateQueryParams).toHaveBeenCalled();
expect(component.outcomeIService.getEOIsData).toHaveBeenCalled();
expect(component.outcomeIService.getWorkPackagesData).toHaveBeenCalled();
});

it('should set default initiative for non-admin user when query param exists', () => {
component.api.rolesSE.isAdmin = false;
component.api.dataControlSE.myInitiativesList = [{ official_code: 'TEST1' }, { official_code: 'TEST2' }];
component.activatedRoute.snapshot.queryParams = { init: 'TEST2' };
jest.spyOn(component, 'updateQueryParams');
jest.spyOn(component.outcomeIService, 'getEOIsData');
jest.spyOn(component.outcomeIService, 'getWorkPackagesData');

component.setDefaultInitiativeForNonAdmin();

expect(component.outcomeIService.initiativeIdFilter).toBe('TEST2');
expect(component.updateQueryParams).toHaveBeenCalled();
expect(component.outcomeIService.getEOIsData).toHaveBeenCalled();
expect(component.outcomeIService.getWorkPackagesData).toHaveBeenCalled();
});

it('should load all initiatives', async () => {
const mockResponse = { response: [{ official_code: 'TEST1' }] };
jest.spyOn(component.api.resultsSE, 'GET_AllInitiatives').mockReturnValue(of(mockResponse));
jest.spyOn(component, 'handleInitiativeQueryParam');

await component.loadAllInitiatives();

expect(component.allInitiatives).toEqual(mockResponse.response);
expect(component.handleInitiativeQueryParam).toHaveBeenCalled();
});

it('should handle error when loading initiatives', () => {
jest.spyOn(console, 'error');
jest.spyOn(component.api.resultsSE, 'GET_AllInitiatives').mockReturnValue(throwError(() => 'Test error'));

component.loadAllInitiatives();

expect(console.error).toHaveBeenCalledWith('Error loading initiatives:', 'Test error');
});

it('should handle initiative query param when param exists', () => {
const mockQueryParams = { init: 'test1' };
component.activatedRoute.snapshot.queryParams = mockQueryParams;
jest.spyOn(component.outcomeIService, 'getEOIsData');
jest.spyOn(component.outcomeIService, 'getWorkPackagesData');

component.handleInitiativeQueryParam();

expect(component.outcomeIService.initiativeIdFilter).toBe('TEST1');
expect(component.outcomeIService.getEOIsData).toHaveBeenCalled();
expect(component.outcomeIService.getWorkPackagesData).toHaveBeenCalled();
});

it('should update query params', () => {
component.outcomeIService.initiativeIdFilter = 'TEST1';
jest.spyOn(component.router, 'navigate');

component.updateQueryParams();

expect(component.router.navigate).toHaveBeenCalledWith([], {
relativeTo: component.activatedRoute,
queryParams: { init: 'TEST1' },
queryParamsHandling: 'merge'
});
});

it('should handle initiative change', () => {
jest.spyOn(component, 'updateQueryParams');
jest.spyOn(component.outcomeIService, 'getEOIsData');
jest.spyOn(component.outcomeIService, 'getWorkPackagesData');
jest.spyOn(component.outcomeIService.searchText, 'set');

component.handleInitiativeChange();

expect(component.updateQueryParams).toHaveBeenCalled();
expect(component.outcomeIService.getEOIsData).toHaveBeenCalled();
expect(component.outcomeIService.getWorkPackagesData).toHaveBeenCalled();
expect(component.outcomeIService.searchText.set).toHaveBeenCalledWith('');
});

it('should update query params when not in indicator details route', () => {
component.outcomeIService.initiativeIdFilter = 'TEST1';
jest.spyOn(component.router, 'navigate');
jest.spyOn(component.router, 'url', 'get').mockReturnValue('/outcome-indicator-module');

component.updateQueryParams();

expect(component.router.navigate).toHaveBeenCalledWith([], {
relativeTo: component.activatedRoute,
queryParams: { init: 'TEST1' },
queryParamsHandling: 'merge'
});
});

it('should not update query params when in indicator details route', () => {
component.outcomeIService.initiativeIdFilter = 'TEST1';
jest.spyOn(component.router, 'navigate');
jest.spyOn(component.router, 'url', 'get').mockReturnValue('/outcome-indicator-module/indicator-details');

component.updateQueryParams();

expect(component.router.navigate).not.toHaveBeenCalled();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { Component, OnInit } from '@angular/core';
import { ApiService } from '../../shared/services/api/api.service';
import { OutcomeIndicatorService } from './services/outcome-indicator.service';
import { ActivatedRoute, Router } from '@angular/router';

@Component({
selector: 'app-outcome-indicator-module',
templateUrl: './outcome-indicator.component.html',
styleUrls: ['./outcome-indicator.component.scss']
})
export class OutcomeIndicatorComponent implements OnInit {
readonly QUERY_PARAM_INITIATIVE = 'init';
allInitiatives: any[] = [];

constructor(
public api: ApiService,
public outcomeIService: OutcomeIndicatorService,
public activatedRoute: ActivatedRoute,
public router: Router
) {}

ngOnInit(): void {
this.initializeComponent();
}

async initializeComponent(): Promise<void> {
this.api.dataControlSE.getCurrentPhases();

if (this.api.rolesSE.isAdmin) {
await this.loadAllInitiatives();
} else {
this.api.updateUserData(() => this.setDefaultInitiativeForNonAdmin());
}
}

handleInitiativeChange() {
this.updateQueryParams();
this.outcomeIService.getEOIsData();
this.outcomeIService.getWorkPackagesData();
this.outcomeIService.searchText.set('');
}

setDefaultInitiativeForNonAdmin(): void {
const defaultInitiative = this.api.dataControlSE.myInitiativesList[0]?.official_code;
const initParam = this.activatedRoute.snapshot.queryParams[this.QUERY_PARAM_INITIATIVE];

this.outcomeIService.initiativeIdFilter = this.api.dataControlSE.myInitiativesList.some(init => init.official_code === initParam?.toUpperCase())
? initParam
: defaultInitiative;

this.updateQueryParams();
this.outcomeIService.getEOIsData();
this.outcomeIService.getWorkPackagesData();
}

async loadAllInitiatives(): Promise<void> {
this.api.resultsSE.GET_AllInitiatives().subscribe({
next: ({ response }) => {
this.allInitiatives = response;
this.handleInitiativeQueryParam();
},
error: error => console.error('Error loading initiatives:', error)
});
}

handleInitiativeQueryParam(): void {
const initParam = this.activatedRoute.snapshot.queryParams[this.QUERY_PARAM_INITIATIVE];

if (initParam) {
this.outcomeIService.initiativeIdFilter = initParam.toUpperCase();
} else if (this.allInitiatives.length > 0) {
this.outcomeIService.initiativeIdFilter = this.allInitiatives[0].official_code;
this.updateQueryParams();
}
this.outcomeIService.getEOIsData();
this.outcomeIService.getWorkPackagesData();
}

updateQueryParams(): void {
if (this.router.url.includes('/outcome-indicator-module/indicator-details')) {
return;
}

this.router.navigate([], {
relativeTo: this.activatedRoute,
queryParams: { [this.QUERY_PARAM_INITIATIVE]: this.outcomeIService.initiativeIdFilter },
queryParamsHandling: 'merge'
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';

import { OutcomeIndicatorComponent } from './outcome-indicator.component';
import { OutcomeIndicatorRoutingModule } from './outcome-indicator-routing.module';
import { CustomFieldsModule } from '../../custom-fields/custom-fields.module';

@NgModule({
declarations: [OutcomeIndicatorComponent],
imports: [CommonModule, OutcomeIndicatorRoutingModule, CustomFieldsModule]
})
export class OutcomeIndicatorModule {}
Loading

0 comments on commit 21b2dee

Please sign in to comment.