diff --git a/dmp-backend/web/src/main/java/eu/eudat/models/rda/mapper/DmpRDAMapper.java b/dmp-backend/web/src/main/java/eu/eudat/models/rda/mapper/DmpRDAMapper.java index a64649c86..bda50f6ba 100644 --- a/dmp-backend/web/src/main/java/eu/eudat/models/rda/mapper/DmpRDAMapper.java +++ b/dmp-backend/web/src/main/java/eu/eudat/models/rda/mapper/DmpRDAMapper.java @@ -3,6 +3,7 @@ package eu.eudat.models.rda.mapper; import eu.eudat.data.entities.*; import eu.eudat.logic.services.ApiContext; import eu.eudat.logic.utilities.helpers.StreamDistinctBy; +import eu.eudat.models.rda.Cost; import eu.eudat.models.rda.Dmp; import eu.eudat.models.rda.DmpId; import net.minidev.json.JSONObject; @@ -43,16 +44,34 @@ public class DmpRDAMapper { Map extraProperties = new org.json.JSONObject(dmp.getExtraProperties()).toMap(); if (!extraProperties.isEmpty()) { - rda.setLanguage(LanguageRDAMapper.mapLanguageIsoToRDAIso(extraProperties.get("language").toString())); + if (extraProperties.get("language") != null) { + rda.setLanguage(LanguageRDAMapper.mapLanguageIsoToRDAIso(extraProperties.get("language").toString())); + } + if (extraProperties.get("costs") != null) { + rda.setCost(new ArrayList<>()); + ((List) extraProperties.get("costs")).forEach(costl -> { + Cost cost = new Cost(); + Map code = new org.json.JSONObject((String) ((Map) costl).get("code")).toMap(); + cost.setCurrencyCode(Cost.CurrencyCode.fromValue((String) code.get("value"))); + cost.setDescription((String) ((Map) costl).get("description")); + cost.setTitle((String) ((Map) costl).get("title")); + cost.setValue(((Integer) ((Map) costl).get("value")).doubleValue()); + rda.getCost().add(cost); + }); + } + if (extraProperties.get("contact") != null) { + UserInfo contact = apiContext.getOperationsContext().getDatabaseRepository().getUserInfoDao().find(UUID.fromString((String)extraProperties.get("contact"))); + rda.setContact(ContactRDAMapper.toRDA(contact)); + } } - UserInfo creator; + /*UserInfo creator; if (dmp.getCreator() != null) { creator = dmp.getCreator(); } else { creator = dmp.getUsers().stream().filter(userDMP -> userDMP.getRole().equals(UserDMP.UserDMPRoles.OWNER.getValue())).map(UserDMP::getUser).findFirst().orElse(new UserInfo()); } - rda.setContact(ContactRDAMapper.toRDA(creator)); + rda.setContact(ContactRDAMapper.toRDA(creator));*/ rda.setContributor(new ArrayList<>()); if (dmp.getResearchers() != null && !dmp.getResearchers().isEmpty()) { rda.getContributor().addAll(dmp.getResearchers().stream().map(ContributorRDAMapper::toRDA).collect(Collectors.toList())); diff --git a/dmp-frontend/src/app/core/model/dmp/cost.ts b/dmp-frontend/src/app/core/model/dmp/cost.ts new file mode 100644 index 000000000..ab432f02b --- /dev/null +++ b/dmp-frontend/src/app/core/model/dmp/cost.ts @@ -0,0 +1,9 @@ +import { FormGroup, FormBuilder, Validators } from '@angular/forms'; +import { ValidationContext } from '@common/forms/validation/validation-context'; + +export interface CostModel { + code: string; + description: string; + title: string; + value: number; +} diff --git a/dmp-frontend/src/app/ui/dmp/dmp.module.ts b/dmp-frontend/src/app/ui/dmp/dmp.module.ts index d331894da..67a9a3f76 100644 --- a/dmp-frontend/src/app/ui/dmp/dmp.module.ts +++ b/dmp-frontend/src/app/ui/dmp/dmp.module.ts @@ -32,6 +32,8 @@ import { FormValidationErrorsDialogModule } from '@common/forms/form-validation- import { CommonUiModule } from '@common/ui/common-ui.module'; import { MultipleChoiceDialogModule } from '@common/modules/multiple-choice-dialog/multiple-choice-dialog.module'; import { AddOrganizationComponent } from './editor/add-organization/add-organization.component'; +import { AddCostComponent } from './editor/cost-editor/add-cost/add-cost.component'; +import { CostListingComponent } from './editor/cost-editor/cost-listing/cost-listing.component'; @NgModule({ imports: [ @@ -69,7 +71,9 @@ import { AddOrganizationComponent } from './editor/add-organization/add-organiza GrantTabComponent, DatasetsTabComponent, DmpCloneComponent, - AddOrganizationComponent + AddOrganizationComponent, + AddCostComponent, + CostListingComponent ], entryComponents: [ DmpInvitationDialogComponent, @@ -77,7 +81,8 @@ import { AddOrganizationComponent } from './editor/add-organization/add-organiza AvailableProfilesComponent, DmpFinalizeDialogComponent, DmpUploadDialogue, - AddOrganizationComponent + AddOrganizationComponent, + AddCostComponent ] }) export class DmpModule { } diff --git a/dmp-frontend/src/app/ui/dmp/editor/cost-editor/add-cost/add-cost.component.html b/dmp-frontend/src/app/ui/dmp/editor/cost-editor/add-cost/add-cost.component.html new file mode 100644 index 000000000..fca4f3c07 --- /dev/null +++ b/dmp-frontend/src/app/ui/dmp/editor/cost-editor/add-cost/add-cost.component.html @@ -0,0 +1,28 @@ +
+

{{'ADDEDITCOST-EDITOR.ADD-TITLE' | translate}}

+
+ + + + {{'GENERAL.VALIDATION.REQUIRED' | translate}} + + + + {{'GENERAL.VALIDATION.REQUIRED' | translate}} + + + + {{'GENERAL.VALIDATION.REQUIRED' | translate}} + + + + {{'GENERAL.VALIDATION.REQUIRED' | translate}} + +
+
+
+
+
+
+
+
diff --git a/dmp-frontend/src/app/ui/dmp/editor/cost-editor/add-cost/add-cost.component.ts b/dmp-frontend/src/app/ui/dmp/editor/cost-editor/add-cost/add-cost.component.ts new file mode 100644 index 000000000..7a4da0f75 --- /dev/null +++ b/dmp-frontend/src/app/ui/dmp/editor/cost-editor/add-cost/add-cost.component.ts @@ -0,0 +1,60 @@ +import { Component, Inject, OnInit } from '@angular/core'; +import { FormGroup } from '@angular/forms'; +import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; +import { ExternalResearcherService } from '@app/core/services/external-sources/researcher/external-researcher.service'; +import { BaseComponent } from '@common/base/base.component'; +import { takeUntil } from 'rxjs/operators'; +import { CostEditorModel } from './add-cost.model'; +import { SingleAutoCompleteConfiguration } from '@app/library/auto-complete/single/single-auto-complete-configuration'; +import { Observable } from 'rxjs'; +import { LocalFetchModel } from '@app/core/model/local-fetch/local-fetch.model'; +import { CurrencyService } from '@app/core/services/currency/currency.service'; + +@Component({ + selector: 'app-add-cost-component', + templateUrl: 'add-cost.component.html', +}) +export class AddCostComponent extends BaseComponent implements OnInit { + + public formGroup: FormGroup; + + currencyAutoCompleteConfiguration: SingleAutoCompleteConfiguration = { + filterFn: this.searchCurrency.bind(this), + initialItems: () => this.searchCurrency(''), + displayFn: (item) => typeof (item) == 'string' ? JSON.parse(item)['name'] : item.name, + titleFn: (item) => typeof (item) == 'string' ? JSON.parse(item)['name'] : item.name, + valueAssign: (item) => JSON.stringify(item) + }; + + constructor( + private externalResearcherService: ExternalResearcherService, + public dialogRef: MatDialogRef, + private currencyService: CurrencyService, + @Inject(MAT_DIALOG_DATA) public data: any + ) { super(); } + + ngOnInit(): void { + const cost = new CostEditorModel(); + this.formGroup = cost.buildForm(); + } + + send(value: any) { + this.externalResearcherService.createResearcher(this.formGroup.value) + .pipe(takeUntil(this._destroyed)) + .subscribe( + null, null, () => this.dialogRef.close() + ); + } + + addCost() { + this.dialogRef.close(this.formGroup.value); + } + + isFormValid() { + return this.formGroup.valid; + } + + searchCurrency(like: string): Observable { + return this.currencyService.get(like); + } +} diff --git a/dmp-frontend/src/app/ui/dmp/editor/cost-editor/add-cost/add-cost.model.ts b/dmp-frontend/src/app/ui/dmp/editor/cost-editor/add-cost/add-cost.model.ts new file mode 100644 index 000000000..252e4950d --- /dev/null +++ b/dmp-frontend/src/app/ui/dmp/editor/cost-editor/add-cost/add-cost.model.ts @@ -0,0 +1,44 @@ +import { FormBuilder, FormGroup } from '@angular/forms'; +import { ResearcherModel } from '@app/core/model/researcher/researcher'; +import { BackendErrorValidator } from '@common/forms/validation/custom-validator'; +import { ValidationErrorModel } from '@common/forms/validation/error-model/validation-error-model'; +import { ValidationContext } from '@common/forms/validation/validation-context'; +import { OrganizationModel } from '@app/core/model/organisation/organization'; +import { CostModel } from '@app/core/model/dmp/cost'; + +export class CostEditorModel implements CostModel{ + public code: string; + public description: string; + public title: string; + public value: number; + public validationErrorModel: ValidationErrorModel = new ValidationErrorModel(); + + fromModel(item: CostModel): CostEditorModel { + this.code = item.code; + this.description = item.description; + this.title = item.title; + this.value = item.value; + return this; + } + + buildForm(context: ValidationContext = null, disabled: boolean = false): FormGroup { + if (context == null) { context = this.createValidationContext(); } + const formGroup = new FormBuilder().group({ + code: [{ value: this.code, disabled: disabled }, context.getValidation('code').validators], + description: [{ value: this.description, disabled: disabled }, context.getValidation('description').validators], + title: [{ value: this.title, disabled: disabled }, context.getValidation('title').validators], + value: [{ value: this.value, disabled: disabled }, context.getValidation('value').validators] + }); + + return formGroup; + } + + createValidationContext(): ValidationContext { + const baseContext: ValidationContext = new ValidationContext(); + baseContext.validation.push({ key: 'code', validators: [] }); + baseContext.validation.push({ key: 'description', validators: [] }); + baseContext.validation.push({ key: 'title', validators: [BackendErrorValidator(this.validationErrorModel, 'title')] }); + baseContext.validation.push({ key: 'value', validators: [] }); + return baseContext; + } +} diff --git a/dmp-frontend/src/app/ui/dmp/editor/cost-editor/cost-listing/cost-listing.component.html b/dmp-frontend/src/app/ui/dmp/editor/cost-editor/cost-listing/cost-listing.component.html new file mode 100644 index 000000000..8ed92f09d --- /dev/null +++ b/dmp-frontend/src/app/ui/dmp/editor/cost-editor/cost-listing/cost-listing.component.html @@ -0,0 +1,42 @@ + + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+
+
+
+
+
+
+
+
+
+
+
+ + + + + +
diff --git a/dmp-frontend/src/app/ui/dmp/editor/cost-editor/cost-listing/cost-listing.component.scss b/dmp-frontend/src/app/ui/dmp/editor/cost-editor/cost-listing/cost-listing.component.scss new file mode 100644 index 000000000..52f45cc20 --- /dev/null +++ b/dmp-frontend/src/app/ui/dmp/editor/cost-editor/cost-listing/cost-listing.component.scss @@ -0,0 +1,10 @@ +.listing-container { + height: fit-content; + width: fit-content; + background-color: whitesmoke; +} + + +.cost-element { + margin-bottom: 1em; +} diff --git a/dmp-frontend/src/app/ui/dmp/editor/cost-editor/cost-listing/cost-listing.component.ts b/dmp-frontend/src/app/ui/dmp/editor/cost-editor/cost-listing/cost-listing.component.ts new file mode 100644 index 000000000..16bbc9a89 --- /dev/null +++ b/dmp-frontend/src/app/ui/dmp/editor/cost-editor/cost-listing/cost-listing.component.ts @@ -0,0 +1,63 @@ +import { Component, OnInit, Input } from '@angular/core'; +import { BaseComponent } from '@common/base/base.component'; +import { FormArray, FormControl } from '@angular/forms'; +import { takeUntil } from 'rxjs/operators'; +import { CostModel } from '@app/core/model/dmp/cost'; +import { SingleAutoCompleteConfiguration } from '@app/library/auto-complete/single/single-auto-complete-configuration'; +import { CurrencyService } from '@app/core/services/currency/currency.service'; +import { Observable } from 'rxjs'; +import { LocalFetchModel } from '@app/core/model/local-fetch/local-fetch.model'; +import { CostEditorModel } from '../add-cost/add-cost.model'; + +@Component({ + selector: 'app-cost-listing', + templateUrl: './cost-listing.component.html', + styleUrls: ['./cost-listing.component.scss'] +}) +export class CostListingComponent extends BaseComponent implements OnInit { + + @Input() form: FormArray; + + private cost: CostEditorModel[] = []; + + currencyAutoCompleteConfiguration: SingleAutoCompleteConfiguration = { + filterFn: this.searchCurrency.bind(this), + initialItems: () => this.searchCurrency(''), + displayFn: (item) => typeof (item) == 'string' ? JSON.parse(item)['name'] : item.name, + titleFn: (item) => typeof (item) == 'string' ? JSON.parse(item)['name'] : item.name, + valueAssign: (item) => JSON.stringify(item) + }; + + constructor( + private currencyService: CurrencyService, + ) { + super(); + } + + ngOnInit() { + } + + searchCurrency(like: string): Observable { + return this.currencyService.get(like); +} + +switchEditMode(event: number) { + const control = this.form.at(event); + if (control.disabled) { + this.cost[event] = control.value; + control.enable(); + } else { + control.disable(); + } +} + +removeCost(event: number) { + this.form.removeAt(event); +} + +revertEdits(event: number) { + this.form.at(event).setValue(this.cost[event]); + this.form.at(event).disable(); +} + +} diff --git a/dmp-frontend/src/app/ui/dmp/editor/general-tab/extra-properties-form.model.ts b/dmp-frontend/src/app/ui/dmp/editor/general-tab/extra-properties-form.model.ts index 89d32c5ce..c2dc45d91 100644 --- a/dmp-frontend/src/app/ui/dmp/editor/general-tab/extra-properties-form.model.ts +++ b/dmp-frontend/src/app/ui/dmp/editor/general-tab/extra-properties-form.model.ts @@ -1,30 +1,53 @@ import { ValidationContext } from '@common/forms/validation/validation-context'; import { FormGroup, FormBuilder, Validators } from '@angular/forms'; import { BackendErrorValidator } from '@common/forms/validation/custom-validator'; +import { CostModel } from '@app/core/model/dmp/cost'; +import { isNullOrUndefined } from 'util'; +import { CostEditorModel } from '../cost-editor/add-cost/add-cost.model'; export class ExtraPropertiesFormModel { public language: string; public license: string; public visible: boolean; public publicDate: Date; + public contact: string; + public costs: CostEditorModel[] = []; fromModel(item: any): ExtraPropertiesFormModel { this.language = item.language; this.license = item.license; this.visible = item.visible; this.publicDate = item.publicDate; + this.contact = item.contact; + if (!isNullOrUndefined(item.costs)) { + (item.costs).forEach(element => { + this.costs.push(new CostEditorModel().fromModel(element)); + }); + } return this; } buildForm(context: ValidationContext = null, disabled: boolean = false): FormGroup { if (context == null) { context = this.createValidationContext(); } - + const formBuilder = new FormBuilder(); const formGroup = new FormBuilder().group({ language: [{ value: this.language, disabled: disabled }, context.getValidation('language').validators], license: [{ value: this.license, disabled: disabled }, context.getValidation('license').validators], visible: [{ value: this.visible, disabled: disabled }, context.getValidation('visible').validators], - publicDate: [{ value: this.publicDate, disabled: disabled }, context.getValidation('publicDate').validators] + publicDate: [{ value: this.publicDate, disabled: disabled }, context.getValidation('publicDate').validators], + contact: [{ value: this.contact, disabled: disabled }, context.getValidation('contact').validators], + // costs: [{ value: this.costs, disabled: disabled }, context.getValidation('costs').validators] }); + + const costArray = new Array(); + //if (this.externalDatasets && this.externalDatasets.length > 0) { + this.costs.forEach(item => { + costArray.push(item.buildForm(context.getValidation('costs').descendantValidations, true)); + }); + // } else { + // //externalDatasetsFormArray.push(new ExternalDatasetModel().buildForm(context.getValidation('externalDatasets').descendantValidations, disabled)); + // } + formGroup.addControl('costs', formBuilder.array(costArray)); return formGroup; } @@ -34,6 +57,8 @@ export class ExtraPropertiesFormModel { baseContext.validation.push({ key: 'license', validators: [] }); baseContext.validation.push({ key: 'visible', validators: [] }); baseContext.validation.push({ key: 'publicDate', validators: [] }); + baseContext.validation.push({ key: 'contact', validators: [] }); + baseContext.validation.push({ key: 'costs', validators: [] }); return baseContext; } diff --git a/dmp-frontend/src/app/ui/dmp/editor/general-tab/general-tab.component.html b/dmp-frontend/src/app/ui/dmp/editor/general-tab/general-tab.component.html index ce433d868..78852a3a6 100644 --- a/dmp-frontend/src/app/ui/dmp/editor/general-tab/general-tab.component.html +++ b/dmp-frontend/src/app/ui/dmp/editor/general-tab/general-tab.component.html @@ -88,7 +88,7 @@ - {{formGroup.get('extraProperties').get('language').getError('backendError').message}} + {{formGroup.get('extraProperties').get('license').getError('backendError').message}} {{'GENERAL.VALIDATION.REQUIRED' | translate}} @@ -117,13 +117,36 @@ - - {{formGroup.get('extraProperties').get('visible').getError('backendError').message}} - + + {{formGroup.get('extraProperties').get('publicDate').getError('backendError').message}} + {{'GENERAL.VALIDATION.REQUIRED' | translate}} +
+ + + + + {{vis.name | translate}} + + + + {{formGroup.get('extraProperties').get('contact').getError('backendError').message}} + + {{'GENERAL.VALIDATION.REQUIRED' | translate}} + + +
+
+ Costs + + +
diff --git a/dmp-frontend/src/app/ui/dmp/editor/general-tab/general-tab.component.scss b/dmp-frontend/src/app/ui/dmp/editor/general-tab/general-tab.component.scss index 05d8ae793..322c63d34 100644 --- a/dmp-frontend/src/app/ui/dmp/editor/general-tab/general-tab.component.scss +++ b/dmp-frontend/src/app/ui/dmp/editor/general-tab/general-tab.component.scss @@ -37,3 +37,11 @@ ::ng-deep .mat-form-field-appearance-legacy .mat-form-field-wrapper { padding-bottom: 1.25em; } + +.cost-placeholder { + text-decoration: underline; +} + +.cost-add { + margin-top: 1em; +} diff --git a/dmp-frontend/src/app/ui/dmp/editor/general-tab/general-tab.component.ts b/dmp-frontend/src/app/ui/dmp/editor/general-tab/general-tab.component.ts index 0dee0573f..f699dad91 100644 --- a/dmp-frontend/src/app/ui/dmp/editor/general-tab/general-tab.component.ts +++ b/dmp-frontend/src/app/ui/dmp/editor/general-tab/general-tab.component.ts @@ -1,6 +1,6 @@ import { Component, Input, OnInit } from '@angular/core'; -import { FormGroup } from '@angular/forms'; +import { FormGroup, FormArray } from '@angular/forms'; import { MatDialog } from '@angular/material/dialog'; import { DataTableRequest } from '@app/core/model/data-table/data-table-request'; import { DatasetProfileModel } from '@app/core/model/dataset/dataset-profile'; @@ -26,6 +26,8 @@ import { ConfigurationService } from '@app/core/services/configuration/configura import { LanguageInfoService } from '@app/core/services/culture/language-info-service'; import { LanguageInfo } from '@app/core/model/language-info'; import { LicenseCriteria } from '@app/core/query/license/license-criteria'; +import { AddCostComponent } from '../cost-editor/add-cost/add-cost.component'; +import { CostEditorModel } from '../cost-editor/add-cost/add-cost.model'; interface Visible { value: boolean; @@ -253,4 +255,27 @@ export class GeneralTabComponent extends BaseComponent implements OnInit { getLanguageInfos(): LanguageInfo[] { return this.languageInfoService.getLanguageInfoValues(); } + + getAssociates(): any[] { + let associates: any[] = []; + //associates = (this.formGroup.get('researchers').value as any[]); + associates = associates.concat(this.formGroup.get('associatedUsers').value); + return associates; + } + + addCost(event: MouseEvent) { + event.stopPropagation(); + const dialogRef = this.dialog.open(AddCostComponent, { + data: this.formGroup.get('extraProperties').get('costs') + }); + dialogRef.afterClosed().pipe(takeUntil(this._destroyed)).subscribe(result => { + if (result) { + const costsArray = this.formGroup.get('extraProperties').get('costs').value || []; + costsArray.push(result); + let costeditModel: CostEditorModel = new CostEditorModel(); + costeditModel = costeditModel.fromModel(result); + (this.formGroup.get('extraProperties').get('costs')).push(costeditModel.buildForm(null, true)); + } + }); + } } diff --git a/dmp-frontend/src/assets/i18n/en.json b/dmp-frontend/src/assets/i18n/en.json index 9c5ff334a..31a07273d 100644 --- a/dmp-frontend/src/assets/i18n/en.json +++ b/dmp-frontend/src/assets/i18n/en.json @@ -711,7 +711,8 @@ "LANGUAGE": "Language", "LICENSE": "License", "VISIBILITY": "Visibility", - "PUBLICATION": "Publication Date" + "PUBLICATION": "Publication Date", + "CONTACT": "Contact" }, "ACTIONS": { "GO-TO-GRANT": "Go To DMP Grant", @@ -986,6 +987,18 @@ "CANCEL": "Cancel" } }, + "ADDEDITCOST-EDITOR": { + "ADD-TITLE": "Add a Cost", + "EDIT-TITLE": "Edit the Cost", + "CODE": "Code", + "DESCRIPTION": "Description", + "TITLE": "Title", + "VALUE": "Value", + "ACTIONS": { + "SAVE": "Save", + "CANCEL": "Cancel" + } + }, "DMP-WIZARD": { "FIRST-STEP": { "DMP": "DMP Editor",