diff --git a/src/app/models/filter-options.model.ts b/src/app/models/filter-options.model.ts new file mode 100644 index 00000000..da464c39 --- /dev/null +++ b/src/app/models/filter-options.model.ts @@ -0,0 +1,5 @@ +export interface FilterOptions { + categories: string[]; + countries: string[]; + complianceLevels: string[]; +} diff --git a/src/app/models/search-organizations-filters.model.ts b/src/app/models/search-organizations-filters.model.ts new file mode 100644 index 00000000..98a13ede --- /dev/null +++ b/src/app/models/search-organizations-filters.model.ts @@ -0,0 +1,37 @@ +export interface SearchOrganizationsFilters { + categories: string[]; + countries: string[]; + complianceLevels: string[]; +} + +const regionNames = new Intl.DisplayNames(['en'], { type: 'region' }); + +const codeAliases: Record = { + EL: 'GR', // Greece + UK: 'GB', // United Kingdom +}; + +export function countryName(code: string | null | undefined): string { + if( code?.length !== 2 ) return code ?? ''; + if (!code) return ''; + const upper = code.toUpperCase(); + const normalized = codeAliases?.[upper] ?? upper; + return regionNames.of(normalized) ?? upper; +} + +export function complianceLevelsName(code: string | null | undefined): string { + if (!code) return ''; + + const upper = code.toUpperCase(); + + const map: Record = { + 'BL': 'Baseline', + 'P': 'Professional', + 'P+': 'Professional+', + }; + + return map[upper] ?? upper; +} + + + diff --git a/src/app/services/provider.service.ts b/src/app/services/provider.service.ts index bf3165fd..ee79ccc3 100644 --- a/src/app/services/provider.service.ts +++ b/src/app/services/provider.service.ts @@ -1,7 +1,9 @@ import { Injectable, inject } from '@angular/core'; import { HttpClient, HttpParams } from '@angular/common/http'; -import { Observable, map, catchError, of } from 'rxjs'; +import { Observable, map, catchError, of, forkJoin } from 'rxjs'; import { environment } from '../../environments/environment'; +import { FilterOptions } from '../models/filter-options.model'; +import { SearchOrganizationsFilters } from '../models/search-organizations-filters.model'; export interface Provider { id?: string; @@ -18,9 +20,7 @@ export interface Provider { }) export class ProviderService { private http = inject(HttpClient); - private readonly endpoint = `${environment.BASE_URL}/party/v4/organization`; - //TODO FOR DEV ONLY - //private readonly endpoint = `${environment.BASE_URL}/party/organization`; + private readonly endpoint = `${environment.BASE_URL}/party/organization`; getProviders(params: { fields?: string; offset?: number; limit?: number } = {}): Observable { let httpParams = new HttpParams(); @@ -85,5 +85,57 @@ export class ProviderService { }) ); } + + + getProvidersForTenderNew(filters: SearchOrganizationsFilters): Observable { + const url = environment.searchOrganizationsEndpoint; + + return this.http.post(url, filters).pipe( + map((response) => { + if (Array.isArray(response)) return response as Provider[]; + if (response?.data && Array.isArray(response.data)) return response.data as Provider[]; + return []; + }), + catchError((error) => { + console.warn('Providers for tender (new) API failed:', error); + return of([]); + }) + ); + } + + //Methods for the search engine + getFilterOptions(): Observable { + const base = environment.searchOrganizationsEndpoint.replace(/\/searchOrganizations$/, ''); + const categories$ = this.http.get(`${base}/categories`).pipe( + map(res => (Array.isArray(res) ? res : Array.isArray(res?.data) ? res.data : [])), + catchError(err => { + console.warn('Categories API failed:', err); + return of([]); + }) + ); + + const countries$ = this.http.get(`${base}/countries`).pipe( + map(res => (Array.isArray(res) ? res : Array.isArray(res?.data) ? res.data : [])), + catchError(err => { + console.warn('Countries API failed:', err); + return of([]); + }) + ); + + const complianceLevels$ = this.http.get(`${base}/complianceLevels`).pipe( + map(res => (Array.isArray(res) ? res : Array.isArray(res?.data) ? res.data : [])), + catchError(err => { + console.warn('ComplianceLevels API failed:', err); + return of([]); + }) + ); + + return forkJoin({ + categories: categories$, + countries: countries$, + complianceLevels: complianceLevels$, + }); + } + } diff --git a/src/app/shared/create-tender-modal/create-tender-modal.component.ts b/src/app/shared/create-tender-modal/create-tender-modal.component.ts index ee9f2b13..f275a0e8 100644 --- a/src/app/shared/create-tender-modal/create-tender-modal.component.ts +++ b/src/app/shared/create-tender-modal/create-tender-modal.component.ts @@ -11,11 +11,14 @@ import { ProviderService, Provider } from 'src/app/services/provider.service'; import { Tender, TenderAttachment } from 'src/app/models/tender.model'; import { LoginInfo } from 'src/app/models/interfaces'; import { API_ROLES } from 'src/app/models/roles.constants'; +import { SearchOrganizationsFilters, countryName, complianceLevelsName } from 'src/app/models/search-organizations-filters.model'; +import { FormControl } from '@angular/forms'; +import { ReactiveFormsModule } from '@angular/forms'; @Component({ selector: 'app-create-tender-modal', standalone: true, - imports: [CommonModule, FormsModule], + imports: [CommonModule, FormsModule, ReactiveFormsModule], template: `
@@ -263,8 +266,79 @@ import { API_ROLES } from 'src/app/models/roles.constants'; Select Providers to Invite +
+ +
+ +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+
+
+
-
+ + +
+ +
-
-

No more providers available. All providers have been invited.

+
+ +

+ No more providers available. All providers have been invited. +

+
+ +

+ No filters Selected. Adjust Countries/Categories and click Search. +

+
@@ -370,6 +453,31 @@ export class CreateTenderModalComponent implements OnInit, OnChanges { tenderError: string | null = null; currentUserId: string | null = null; + // Filter options + countriesOptions: string[] = []; + categoriesOptions: string[] = []; + complianceLevelsOptions: string[] = []; + _safeInvitedList: Provider[] = []; + + // Form controls for filters + countriesCtrl = new FormControl([], { nonNullable: true }); + categoriesCtrl = new FormControl([], { nonNullable: true }); + complianceLevelsCtrl = new FormControl([], { nonNullable: true }); + + // Default organization search filters + orgFilters: SearchOrganizationsFilters = { + categories: [], + countries: [], + complianceLevels: [] + }; + + // Helper functions for display + complianceLevelsName = complianceLevelsName; + countryName = countryName; + + // Available providers list + availableProviders: Provider[] = []; + // Tender form fields - Step 1: Title only tenderTitle: string = ''; @@ -402,6 +510,9 @@ export class CreateTenderModalComponent implements OnInit, OnChanges { this.currentUserId = loggedOrg?.partyId; } } + + // Load filter options + this.loadFilterOptions(); } ngOnChanges(changes: SimpleChanges) { @@ -681,32 +792,107 @@ export class CreateTenderModalComponent implements OnInit, OnChanges { console.log('Loading providers from API...'); - this.providerService.getProvidersForTender().subscribe({ + this.providerService.getProvidersForTenderNew(this.orgFilters).subscribe({ next: (providers) => { - console.log('Providers loaded successfully:', providers.length); this.tenderProviders = providers; this.tenderLoading = false; + this.updateAvailableProviders(); - // Load invited providers after providers are loaded + // After providers are loaded, load invited providers (if in edit mode) if (this.tenderCreationStep === 3) { this.loadInvitedProviders(); } }, - error: (error) => { - console.error('Error loading providers:', error); - this.tenderError = 'Failed to load providers. Please try again.'; - this.tenderProviders = []; + error: (err) => { + this.tenderError = 'Failed to load providers: ' + (err.message || 'Unknown error'); this.tenderLoading = false; + console.error('Error loading tender providers:', err); } }); } + /** + * Emit filter changes and reload providers + */ + emitFilters(): void { + const newFilters: SearchOrganizationsFilters = { + countries: this.countriesCtrl.value ?? [], + categories: this.categoriesCtrl.value ?? [], + complianceLevels: this.complianceLevelsCtrl.value ?? [] + }; + console.log(newFilters); + this.orgFilters = newFilters; + this.loadTenderProviders(); + } + + /** + * Are any filters currently active? + */ + hasActiveFilters(): boolean { + const hasCountries = (this.orgFilters.countries?.length ?? 0) == 0; + const hasCategories = (this.orgFilters.categories?.length ?? 0) == 0; + const hasComplianceLevels = (this.orgFilters.complianceLevels?.length ?? 0) == 0; + + return hasCountries && hasCategories && hasComplianceLevels; + } + + /** + * Clear all filters + */ + clearFilters() { + // Reset both controls to empty arrays (and emit change) + this.countriesCtrl.setValue([], { emitEvent: true }); + this.categoriesCtrl.setValue([], { emitEvent: true }); + this.complianceLevelsCtrl.setValue([], { emitEvent: true }); + + // If you rely on (change) only, also call emit explicitly: + this.emitFilters(); + } + toggleProviderSelection(providerId: string) { - if (this.selectedProviders.has(providerId)) { - this.selectedProviders.delete(providerId); + // find in local safe list (which stores { provider, quoteId }) + const idx = this._safeInvitedList.findIndex(x => x?.id === providerId); + + if (idx >= 0) { + // UNCHECK → remove from local safe list + this._safeInvitedList.splice(idx, 1); } else { - this.selectedProviders.add(providerId); + // CHECK → add to local safe list + const p = this.tenderProviders.find(tp => tp.id === providerId); + if (p) { + this._safeInvitedList.push(p); + } } + + // Re-derive selectedProviders + available list in one place + this.rebuildSelectionAndAvailable(); + } + + private rebuildSelectionAndAvailable(): Provider[] { + // 1) selectedProviders = IDs from local safe list + this.selectedProviders = new Set( + this._safeInvitedList + .map(x => x?.id) + .filter((id): id is string => !!id) + ); + + // 2) all IDs that must be excluded from availability (server invited + locally selected) + const excludeIds = new Set([ + ...this.invitedProviders + .map(ip => ip?.provider?.id) + .filter((id): id is string => !!id), + ...Array.from(this.selectedProviders), + ]); + + // 3) compute available list + const available = this.tenderProviders + .filter(p => !!p?.id && !excludeIds.has(p.id!)) + .map(p => ({ ...p } as Provider)); + + // keep a cached copy if you want to bind directly in template + this.availableProviders = available; + + return available; } /** @@ -779,12 +965,19 @@ export class CreateTenderModalComponent implements OnInit, OnChanges { return dateString; } + /** + * Update available providers list + */ + updateAvailableProviders(): void { + this.availableProviders = this.getAvailableProviders(); + } + /** * Get available providers (excluding already invited ones) */ getAvailableProviders(): Provider[] { - const invitedProviderIds = new Set(this.invitedProviders.map(ip => ip.provider.id)); - return this.tenderProviders.filter(p => p.id && !invitedProviderIds.has(p.id)); + // Simple and clean — everything is handled by the helper + return this.rebuildSelectionAndAvailable(); } /** @@ -807,41 +1000,45 @@ export class CreateTenderModalComponent implements OnInit, OnChanges { console.log('Creating tendering quotes for providers:', providerIds); - // Create tendering quotes for multiple providers - this.quoteService.createMultipleTenderingQuotes( - this.currentUserId, - providerIds, - this.createdQuoteId, - customerMessage - ).subscribe({ - next: (createdTenders) => { - console.log('Tendering quotes created:', createdTenders); - + // Create tendering quotes one by one to capture individual quote IDs + const requests = providerIds.map(providerId => { + const provider = this._safeInvitedList.find(p => p.id === providerId); + + return this.quoteService.createTenderingQuote( + this.currentUserId!, + providerId, + this.createdQuoteId!, + customerMessage + ).toPromise().then(tender => { + if (!tender || !tender.id || !provider) { + throw new Error('Failed to create quote for provider'); + } + return { + provider: provider, + quoteId: tender.id + }; + }); + }); + + Promise.all(requests) + .then(results => { + console.log('Tendering quotes created:', results); + // Add to invited providers list - createdTenders.forEach((tender, index) => { - if (tender.id) { - const provider = this.tenderProviders.find(p => p.id === providerIds[index]); - if (provider) { - this.invitedProviders.push({ - provider: provider, - quoteId: tender.id - }); - } - } - }); - - // Clear selection + this.invitedProviders.push(...results); + + // Clear selection and safe list this.selectedProviders.clear(); - + this._safeInvitedList = []; + this.notificationService.showSuccess(`${providerIds.length} provider(s) invited successfully!`); this.tenderLoading = false; - }, - error: (error) => { + }) + .catch(error => { console.error('Error creating tendering quotes:', error); this.notificationService.showError('Failed to invite providers: ' + (error.message || 'Unknown error')); this.tenderLoading = false; - } - }); + }); } /** @@ -945,4 +1142,37 @@ export class CreateTenderModalComponent implements OnInit, OnChanges { } }); } + + /** + * Load filter options (countries, categories, compliance levels) + */ + private loadFilterOptions(): void { + this.clearFilters(); + this.providerService.getFilterOptions().subscribe({ + next: ({ categories, countries, complianceLevels }) => { + this.categoriesOptions = categories ?? []; + this.countriesOptions = countries ?? []; + this.complianceLevelsOptions = complianceLevels ?? []; + }, + error: (err) => { + console.warn('Failed to load filter options', err); + } + }); + } + + /** + * Toggle selection in multi-select dropdown + */ + toggleFromSelect(ctrl: FormControl, value: string, event: MouseEvent) { + event.preventDefault(); // stop native multi-select behavior + event.stopPropagation(); + + const cur = ctrl.value ?? []; + const next = cur.includes(value) + ? cur.filter(v => v !== value) + : [...cur, value]; + + ctrl.setValue(next); + } } + diff --git a/src/environments/environment.development.ts b/src/environments/environment.development.ts index 0128d711..0ea16661 100644 --- a/src/environments/environment.development.ts +++ b/src/environments/environment.development.ts @@ -19,6 +19,9 @@ export const environment = { BILLING: '/billing', CHARGING: '/charging', + searchOrganizationsEndpoint: 'http://dome-search-svc.search-engine.svc.cluster.local:8080/api/searchOrganizations', + //searchOrganizationsEndpoint: 'org-api/searchOrganizations', + CUSTOMER_BILLING:'/customerBill', CONSUMER_BILLING_URL: 'http://localhost:8640', INVOICE_LIMIT: 100, diff --git a/src/environments/environment.production.ts b/src/environments/environment.production.ts index 2d25a751..6b93637f 100644 --- a/src/environments/environment.production.ts +++ b/src/environments/environment.production.ts @@ -18,6 +18,8 @@ export const environment = { BILLING: '/billing', CHARGING: '/charging', + searchOrganizationsEndpoint: 'http://dome-search-svc.search-engine.svc.cluster.local:8080/api/searchOrganizations', + CONSUMER_BILLING_URL: 'http://localhost:8640', INVOICE_LIMIT: 100, diff --git a/src/environments/environment.ts b/src/environments/environment.ts index c164ab9d..28532d5f 100644 --- a/src/environments/environment.ts +++ b/src/environments/environment.ts @@ -19,6 +19,8 @@ export const environment = { CHARGING: '/charging', BILLING: '/billing', + searchOrganizationsEndpoint: 'http://dome-search-svc.search-engine.svc.cluster.local:8080/api/searchOrganizations', + CUSTOMER_BILLING:'/customerBill', CONSUMER_BILLING_URL: 'http://localhost:8640', //API PAGINATION