From 8ed16b7b2066e5816c43d262219fb4f7f38b0cf7 Mon Sep 17 00:00:00 2001 From: Francisco de la Vega Date: Mon, 27 Apr 2026 12:30:13 +0200 Subject: [PATCH 1/3] Filter out offers without a picture from the dashboard site --- src/app/pages/dashboard/dashboard.component.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/app/pages/dashboard/dashboard.component.ts b/src/app/pages/dashboard/dashboard.component.ts index 5eeccd73..721c34c5 100644 --- a/src/app/pages/dashboard/dashboard.component.ts +++ b/src/app/pages/dashboard/dashboard.component.ts @@ -185,7 +185,11 @@ export class DashboardComponent implements OnInit, OnDestroy { ) .subscribe((picked) => { this.productService.getProductsDetails(picked).then((data) => { - this.productOfferings = data as ProductOffering[]; + this.productOfferings = (data as ProductOffering[]).filter((offering) => + offering.attachment?.some( + (a) => a.attachmentType === 'Picture' || a.name === 'Profile Picture' + ) + ); }) }); } From 75940c7a702987b893e3e51fca020a8eb5adde87 Mon Sep 17 00:00:00 2001 From: Francisco de la Vega Date: Mon, 27 Apr 2026 12:42:21 +0200 Subject: [PATCH 2/3] Move external billing feature to the procurement tab --- .../general-info/general-info.component.html | 17 ----- .../general-info/general-info.component.ts | 39 ---------- src/app/shared/forms/offer/offer.component.ts | 14 ++-- .../procurement-mode.component.html | 18 +++++ .../procurement-mode.component.ts | 71 ++++++++++++++----- 5 files changed, 77 insertions(+), 82 deletions(-) diff --git a/src/app/shared/forms/offer/general-info/general-info.component.html b/src/app/shared/forms/offer/general-info/general-info.component.html index 2d4d6850..7bfd3064 100644 --- a/src/app/shared/forms/offer/general-info/general-info.component.html +++ b/src/app/shared/forms/offer/general-info/general-info.component.html @@ -8,23 +8,6 @@ } -@if (extBillingEnabledControl) { -
- - -
-} -@if (extBillingEnabledControl?.value && plaSpecIdControl) { - - - @if (plaSpecIdControl.invalid && plaSpecIdControl.touched) { -

{{ 'CREATE_OFFER._pla_spec_id_required' | translate }}

- } -} @if (statusControl && formType === 'update') { diff --git a/src/app/shared/forms/offer/general-info/general-info.component.ts b/src/app/shared/forms/offer/general-info/general-info.component.ts index f617c3c9..1e752e99 100644 --- a/src/app/shared/forms/offer/general-info/general-info.component.ts +++ b/src/app/shared/forms/offer/general-info/general-info.component.ts @@ -16,8 +16,6 @@ interface GeneralInfo { status: string; description: string; version: string; - extBillingEnabled: boolean; - plaSpecId: string; } @Component({ @@ -71,39 +69,16 @@ export class GeneralInfoComponent implements OnInit, OnDestroy { return control instanceof FormControl ? control : null; } - get extBillingEnabledControl(): FormControl | null { - const control = this.formGroup.get('extBillingEnabled'); - return control instanceof FormControl ? control : null; - } - - get plaSpecIdControl(): FormControl | null { - const control = this.formGroup.get('plaSpecId'); - return control instanceof FormControl ? control : null; - } - ngOnInit() { console.log('πŸ“ Initializing form in', this.formType, 'mode'); this.isEditMode = this.formType === 'update'; if (this.isEditMode && this.data) { console.log('Initializing form in update mode with data:', this.data); - const existingPlaSpecId = this.data.pricingLogicAlgorithm?.[0]?.plaSpecId ?? ''; this.formGroup.addControl('name', new FormControl(this.data.name, [Validators.required, Validators.maxLength(100), noWhitespaceValidator])); this.formGroup.addControl('status', new FormControl(this.data.lifecycleStatus)); this.formGroup.addControl('description', new FormControl(this.data.description, Validators.maxLength(100000))); this.formGroup.addControl('version', new FormControl(this.data.version, [Validators.required,Validators.pattern('^-?[0-9]\\d*(\\.\\d*)?$'), noWhitespaceValidator])); - this.formGroup.addControl('extBillingEnabled', new FormControl(!!existingPlaSpecId)); - this.formGroup.addControl('plaSpecId', new FormControl(existingPlaSpecId, !!existingPlaSpecId ? [Validators.required] : [])); - - this.formGroup.get('extBillingEnabled')!.valueChanges.pipe(takeUntil(this.destroy$)).subscribe((enabled: boolean) => { - const plaControl = this.formGroup.get('plaSpecId')!; - if (enabled) { - plaControl.setValidators([Validators.required]); - } else { - plaControl.clearValidators(); - } - plaControl.updateValueAndValidity(); - }); // Store original value only in edit mode this.originalValue = { @@ -111,8 +86,6 @@ export class GeneralInfoComponent implements OnInit, OnDestroy { status: this.data.lifecycleStatus, description: this.data.description, version: this.data.version, - extBillingEnabled: !!existingPlaSpecId, - plaSpecId: existingPlaSpecId }; console.log('πŸ“ Original value stored:', this.originalValue); } else { @@ -121,18 +94,6 @@ export class GeneralInfoComponent implements OnInit, OnDestroy { this.formGroup.addControl('status', new FormControl('Active', [Validators.required])); this.formGroup.addControl('description', new FormControl('')); this.formGroup.addControl('version', new FormControl('0.1', [Validators.required,Validators.pattern('^-?[0-9]\\d*(\\.\\d*)?$'), noWhitespaceValidator])); - this.formGroup.addControl('extBillingEnabled', new FormControl(false)); - this.formGroup.addControl('plaSpecId', new FormControl('')); - - this.formGroup.get('extBillingEnabled')!.valueChanges.pipe(takeUntil(this.destroy$)).subscribe((enabled: boolean) => { - const plaControl = this.formGroup.get('plaSpecId')!; - if (enabled) { - plaControl.setValidators([Validators.required]); - } else { - plaControl.clearValidators(); - } - plaControl.updateValueAndValidity(); - }); } // Subscribe to form changes only in edit mode diff --git a/src/app/shared/forms/offer/offer.component.ts b/src/app/shared/forms/offer/offer.component.ts index f179dde9..91c72521 100644 --- a/src/app/shared/forms/offer/offer.component.ts +++ b/src/app/shared/forms/offer/offer.component.ts @@ -703,8 +703,8 @@ export class OfferComponent implements OnInit, OnDestroy{ bundledProductOffering: this.offersBundle, place: [], version: generalInfo.version, - ...(generalInfo.extBillingEnabled && generalInfo.plaSpecId ? { - pricingLogicAlgorithm: [{ name: 'external billing', plaSpecId: generalInfo.plaSpecId }] + ...(formValue.procurementMode.extBillingEnabled && formValue.procurementMode.plaSpecId ? { + pricingLogicAlgorithm: [{ name: 'external billing', plaSpecId: formValue.procurementMode.plaSpecId }] } : {}), category: categories, @@ -820,11 +820,6 @@ export class OfferComponent implements OnInit, OnDestroy{ basePayload.description = change.currentValue.description; basePayload.version = change.currentValue.version; basePayload.lifecycleStatus = change.currentValue.status; - if (change.currentValue.extBillingEnabled && change.currentValue.plaSpecId) { - basePayload.pricingLogicAlgorithm = [{ name: 'external billing', plaSpecId: change.currentValue.plaSpecId }]; - } else if (change.originalValue.extBillingEnabled && !change.currentValue.extBillingEnabled) { - basePayload.pricingLogicAlgorithm = []; - } break; case 'productSpecification': @@ -932,6 +927,11 @@ export class OfferComponent implements OnInit, OnDestroy{ description: change.currentValue.id }); } + if (change.currentValue.extBillingEnabled && change.currentValue.plaSpecId) { + basePayload.pricingLogicAlgorithm = [{ name: 'external billing', plaSpecId: change.currentValue.plaSpecId }]; + } else if (change.originalValue.extBillingEnabled && !change.currentValue.extBillingEnabled) { + basePayload.pricingLogicAlgorithm = []; + } break; case 'replication': diff --git a/src/app/shared/forms/offer/procurement-mode/procurement-mode.component.html b/src/app/shared/forms/offer/procurement-mode/procurement-mode.component.html index b241432d..736dfd3d 100644 --- a/src/app/shared/forms/offer/procurement-mode/procurement-mode.component.html +++ b/src/app/shared/forms/offer/procurement-mode/procurement-mode.component.html @@ -15,6 +15,24 @@ +@if (extBillingEnabledControl) { +
+ + +
+} +@if (extBillingEnabledControl?.value && plaSpecIdControl) { + + + @if (plaSpecIdControl.invalid && plaSpecIdControl.touched) { +

{{ 'CREATE_OFFER._pla_spec_id_required' | translate }}

+ } +} + @if(showProcurementError){
m.id === this.procurementMode)?.name || 'Manual' + name: this.procurementModes.find(m => m.id === this.procurementMode)?.name || 'Manual', + extBillingEnabled: this.formGroup.get('extBillingEnabled')?.value ?? false, + plaSpecId: this.formGroup.get('plaSpecId')?.value ?? '' }; // Solo emitir si el valor es diferente al original @@ -125,6 +129,16 @@ export class ProcurementModeComponent implements ControlValueAccessor, AfterView return control instanceof FormControl ? control : null; } + get extBillingEnabledControl(): FormControl | null { + const control = this.formGroup.get('extBillingEnabled'); + return control instanceof FormControl ? control : null; + } + + get plaSpecIdControl(): FormControl | null { + const control = this.formGroup.get('plaSpecId'); + return control instanceof FormControl ? control : null; + } + registerOnChange(fn: any): void { this.onChange = fn; } @@ -154,11 +168,27 @@ export class ProcurementModeComponent implements ControlValueAccessor, AfterView // Inicializar el control del formulario this.formGroup.addControl('mode', new FormControl(initialValue, [Validators.required])); + const existingPlaSpecId = this.data?.pricingLogicAlgorithm?.[0]?.plaSpecId ?? ''; + this.formGroup.addControl('extBillingEnabled', new FormControl(!!existingPlaSpecId)); + this.formGroup.addControl('plaSpecId', new FormControl(existingPlaSpecId, !!existingPlaSpecId ? [Validators.required] : [])); + + this.formGroup.get('extBillingEnabled')!.valueChanges.pipe(takeUntil(this.destroy$)).subscribe((enabled: boolean) => { + const plaControl = this.formGroup.get('plaSpecId')!; + if (enabled) { + plaControl.setValidators([Validators.required]); + } else { + plaControl.clearValidators(); + } + plaControl.updateValueAndValidity(); + }); + // Guardar el valor original solo en modo ediciΓ³n if (this.isEditMode) { this.originalValue = { id: initialValue, - name: this.procurementModes.find(m => m.id === initialValue)?.name || 'Manual' + name: this.procurementModes.find(m => m.id === initialValue)?.name || 'Manual', + extBillingEnabled: !!existingPlaSpecId, + plaSpecId: existingPlaSpecId }; console.log('πŸ“ Original value stored:', this.originalValue); } @@ -169,26 +199,29 @@ export class ProcurementModeComponent implements ControlValueAccessor, AfterView .subscribe(value => { console.log('πŸ“ Form value changed in subscription:', value); - if (value && value.mode) { - if (value.mode != 'manual' && this.gatewayCount == 0) { - this.errorMessage = "You can't select this procurement mode as you are not registered on the payment gateway."; - this.showProcurementError = true; - this.form.setErrors({ invalidProcurement: true }); - this.formGroup.patchValue({ - mode: 'manual' - }, { emitEvent: false }); - return; - } + if (value) { + if (value.mode) { + if (value.mode != 'manual' && this.gatewayCount == 0) { + this.errorMessage = "You can't select this procurement mode as you are not registered on the payment gateway."; + this.showProcurementError = true; + this.form.setErrors({ invalidProcurement: true }); + this.formGroup.patchValue({ + mode: 'manual' + }, { emitEvent: false }); + return; + } + + this.errorMessage = ""; + this.showProcurementError = false; + this.form.setErrors(null); - this.errorMessage = ""; - this.showProcurementError = false; - this.form.setErrors(null) + const mode = this.procurementModes.find(m => m.id === value.mode) || this.procurementModes[0]; + console.log('πŸ“ Found mode:', mode); - const mode = this.procurementModes.find(m => m.id === value.mode) || this.procurementModes[0]; - console.log('πŸ“ Found mode:', mode); + this.procurementMode = mode.id; + console.log('πŸ“ Current procurementMode:', this.procurementMode); + } - this.procurementMode = mode.id; - console.log('πŸ“ Current procurementMode:', this.procurementMode); this.hasBeenModified = true; } }); From 77a1c3251246cadac51a606f11d0ee2d27fc5ff0 Mon Sep 17 00:00:00 2001 From: Francisco de la Vega Date: Mon, 27 Apr 2026 13:36:30 +0200 Subject: [PATCH 3/3] Check with the backend if an offer is ready to be launched --- src/app/services/app-init.service.ts | 1 + src/app/services/product-service.service.ts | 5 +++++ .../general-info/general-info.component.html | 2 +- .../offer/general-info/general-info.component.ts | 16 +++++++++++++++- .../status-selector/status-selector.component.ts | 10 ++++++++-- src/environments/environment.development.ts | 3 ++- src/environments/environment.production.ts | 3 ++- src/environments/environment.ts | 3 ++- 8 files changed, 36 insertions(+), 7 deletions(-) diff --git a/src/app/services/app-init.service.ts b/src/app/services/app-init.service.ts index ba5cb027..6f294b24 100644 --- a/src/app/services/app-init.service.ts +++ b/src/app/services/app-init.service.ts @@ -47,6 +47,7 @@ export class AppInitService { environment.TENDER_ENABLED = config.tenderingEnabled ?? false environment.DATA_SPACE_ENABLED = config.dataSpaceEnabled ?? false environment.LEAR_URL = config.learUrl ?? '' + environment.LAUNCH_VALIDATION_ENABLED = config.launchValidationEnabled ?? false; environment.AI_SEARCH_ENABLED = aiConfig.aiEnabled ?? config.aiEnabled ?? false; environment.AI_SEARCH_API_KEY = aiConfig.aiApiKey ?? config.aiApiKey ?? ''; environment.AI_SEARCH_API_URL = aiConfig.aiApiUrl ?? config.aiApiUrl ?? ''; diff --git a/src/app/services/product-service.service.ts b/src/app/services/product-service.service.ts index 185d9d0e..18f6ed29 100644 --- a/src/app/services/product-service.service.ts +++ b/src/app/services/product-service.service.ts @@ -177,6 +177,11 @@ export class ApiServiceService { return lastValueFrom(this.http.get(url)); } + checkOfferingLaunch(id: string): Promise<{ canBeLaunched: boolean }> { + const url = `${ApiServiceService.BASE_URL}/offering/${id}/launch`; + return lastValueFrom(this.http.get<{ canBeLaunched: boolean }>(url)); + } + getProductPrice(id: any) { let url = `${ApiServiceService.BASE_URL}${ApiServiceService.API_PRODUCT}/productOfferingPrice/${id}` diff --git a/src/app/shared/forms/offer/general-info/general-info.component.html b/src/app/shared/forms/offer/general-info/general-info.component.html index 7bfd3064..7f1292f2 100644 --- a/src/app/shared/forms/offer/general-info/general-info.component.html +++ b/src/app/shared/forms/offer/general-info/general-info.component.html @@ -10,7 +10,7 @@ } @if (statusControl && formType === 'update') { - + } @if (descControl) { diff --git a/src/app/shared/forms/offer/general-info/general-info.component.ts b/src/app/shared/forms/offer/general-info/general-info.component.ts index 1e752e99..284457b6 100644 --- a/src/app/shared/forms/offer/general-info/general-info.component.ts +++ b/src/app/shared/forms/offer/general-info/general-info.component.ts @@ -4,12 +4,14 @@ import {SharedModule} from "../../../shared.module"; import {MarkdownTextareaComponent} from "../../markdown-textarea/markdown-textarea.component"; import {StatusSelectorComponent} from "../../status-selector/status-selector.component"; import {EventMessageService} from "../../../../services/event-message.service"; +import {ApiServiceService} from "../../../../services/product-service.service"; import {FormChangeState} from "../../../../models/interfaces"; import {Subscription} from "rxjs"; import {debounceTime} from "rxjs/operators"; import { noWhitespaceValidator } from 'src/app/validators/validators'; import {Subject} from "rxjs"; import { takeUntil } from 'rxjs/operators'; +import { environment } from 'src/environments/environment'; interface GeneralInfo { name: string; @@ -41,7 +43,9 @@ export class GeneralInfoComponent implements OnInit, OnDestroy { private isEditMode: boolean = false; private destroy$ = new Subject(); - constructor(private eventMessage: EventMessageService) { + disabledStatuses: string[] = []; + + constructor(private eventMessage: EventMessageService, private apiService: ApiServiceService) { console.log('πŸ”„ Initializing GeneralInfoComponent'); } @@ -88,6 +92,16 @@ export class GeneralInfoComponent implements OnInit, OnDestroy { version: this.data.version, }; console.log('πŸ“ Original value stored:', this.originalValue); + + if (environment.LAUNCH_VALIDATION_ENABLED && this.data.id) { + this.apiService.checkOfferingLaunch(this.data.id).then((result) => { + if (!result.canBeLaunched) { + this.disabledStatuses = ['Launched']; + } + }).catch(() => { + this.disabledStatuses = ['Launched']; + }); + } } else { console.log('Initializing form in create mode'); this.formGroup.addControl('name', new FormControl('', [Validators.required, Validators.maxLength(100), noWhitespaceValidator])); diff --git a/src/app/shared/forms/status-selector/status-selector.component.ts b/src/app/shared/forms/status-selector/status-selector.component.ts index 9f799b59..e1ac5d10 100644 --- a/src/app/shared/forms/status-selector/status-selector.component.ts +++ b/src/app/shared/forms/status-selector/status-selector.component.ts @@ -19,7 +19,8 @@ import {SharedModule} from "../../shared.module"; styleUrl: './status-selector.component.css' }) export class StatusSelectorComponent implements ControlValueAccessor { - @Input() statuses: string[] = ['Active', 'Launched', 'Retired', 'Obsolete']; // Estados disponibles + @Input() statuses: string[] = ['Active', 'Launched', 'Retired', 'Obsolete']; + @Input() disabledStatuses: string[] = []; selectedStatus: string = ''; onChange = (status: string) => {}; @@ -38,8 +39,9 @@ export class StatusSelectorComponent implements ControlValueAccessor { } selectStatus(status: string): void { + if (this.disabledStatuses.includes(status)) return; this.selectedStatus = status; - this.onChange(status); // Notifica al formulario padre + this.onChange(status); this.onTouched(); } @@ -51,6 +53,10 @@ export class StatusSelectorComponent implements ControlValueAccessor { "Obsolete": 'text-gray-800' }; + if (this.disabledStatuses.includes(status)) { + return "opacity-40 cursor-not-allowed flex items-center justify-center p-4 rounded-lg space-x-4 transition-all text-gray-500 dark:text-gray-200"; + } + const baseClasses = "cursor-pointer flex items-center justify-center p-4 rounded-lg space-x-4 transition-all"; return this.selectedStatus === status diff --git a/src/environments/environment.development.ts b/src/environments/environment.development.ts index 953f877c..4acc3207 100644 --- a/src/environments/environment.development.ts +++ b/src/environments/environment.development.ts @@ -101,5 +101,6 @@ export const environment = { AI_SEARCH_SCORE_THRESHOLD: 0.3, AI_SEARCH_ANSWER_MAX_ITEMS: 5, AI_SEARCH_PROFILE: 'dome_prod', - LEAR_URL: '' + LEAR_URL: '', + LAUNCH_VALIDATION_ENABLED: false }; diff --git a/src/environments/environment.production.ts b/src/environments/environment.production.ts index 00290cde..df3045e3 100644 --- a/src/environments/environment.production.ts +++ b/src/environments/environment.production.ts @@ -100,5 +100,6 @@ export const environment = { AI_SEARCH_SCORE_THRESHOLD: 0.3, AI_SEARCH_ANSWER_MAX_ITEMS: 5, AI_SEARCH_PROFILE: 'dome_dev2', - LEAR_URL: '' + LEAR_URL: '', + LAUNCH_VALIDATION_ENABLED: false }; diff --git a/src/environments/environment.ts b/src/environments/environment.ts index 2fa52e8b..0fed215f 100644 --- a/src/environments/environment.ts +++ b/src/environments/environment.ts @@ -100,6 +100,7 @@ export const environment = { AI_SEARCH_SCORE_THRESHOLD: 0.3, AI_SEARCH_ANSWER_MAX_ITEMS: 5, AI_SEARCH_PROFILE: 'dome_prod', - LEAR_URL: '' + LEAR_URL: '', + LAUNCH_VALIDATION_ENABLED: false };