Skip to content

Commit da6bc3f

Browse files
authored
Tendering updates (#211)
* Changes on tender management, copied visual changes from the tailored, adjusted the rest to be consistent with tailored changes - Updated the Tender List component to improve the UI, including a new dashboard title and a link to the tender process guide. - Introduced new constants for quote and tender categories in the quote.constants file. - Added detailed status messages for tendering and coordinator quotes to improve user guidance. - Enhanced the Create Tender Modal with step-by-step instructions and improved input handling for dates and file uploads. - Updated the Quote Details Modal to conditionally display information based on quote categories, ensuring relevant details are shown for tailored, tendering, and coordinator quotes. - Improved the Confirm Dialog component's z-index for better visibility. These changes aim to streamline the tender creation and management process, providing users with clearer instructions and a more intuitive interface. * Various fixes to display users id, added constants for quote statuses and labels * Fixes for merge conflict * Various fixes to tailored and tendering Adjusted dropdown status labels Fixed name of porivders not showing after reload of modal in providers invited workaround for error when deleting invited provider removed fallback logic to populate search in the tendering when results are empty Adjusted chat button disabling when quotes were canceled
1 parent 5e98e07 commit da6bc3f

4 files changed

Lines changed: 140 additions & 86 deletions

File tree

src/app/features/quotes/pages/quote-list/quote-list.component.ts

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -92,11 +92,7 @@ import { environment } from 'src/environments/environment';
9292
class="form-select rounded-md border-gray-300 dark:bg-gray-700 dark:border-gray-800 dark:text-white shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
9393
>
9494
<option value="">All Statuses</option>
95-
<option [value]="QUOTE_STATUSES.PENDING">Pending</option>
96-
<option [value]="QUOTE_STATUSES.IN_PROGRESS">In Progress</option>
97-
<option [value]="QUOTE_STATUSES.APPROVED">Approved</option>
98-
<option [value]="QUOTE_STATUSES.CANCELLED">Cancelled</option>
99-
<option [value]="QUOTE_STATUSES.ACCEPTED">Accepted</option>
95+
<option *ngFor="let opt of filterStatusOptions" [value]="opt.value">{{ opt.label }}</option>
10096
</select>
10197
</div>
10298
</div>
@@ -605,6 +601,20 @@ export class QuoteListComponent implements OnInit {
605601
});
606602
}
607603

604+
get filterStatusOptions(): { value: string; label: string }[] {
605+
const labels = this.selectedRole === 'customer'
606+
? TAILORED_STATUSES_LABELS_CUSTOMER
607+
: TAILORED_STATUSES_LABELS_PROVIDER;
608+
return [
609+
{ value: QUOTE_STATUSES.PENDING, label: labels.PENDING },
610+
{ value: QUOTE_STATUSES.IN_PROGRESS, label: labels.IN_PROGRESS },
611+
{ value: QUOTE_STATUSES.APPROVED, label: labels.APPROVED },
612+
{ value: QUOTE_STATUSES.ACCEPTED, label: labels.ACCEPTED },
613+
{ value: QUOTE_STATUSES.CANCELLED, label: labels.CANCELLED },
614+
{ value: QUOTE_STATUSES.REJECTED, label: labels.REJECTED },
615+
];
616+
}
617+
608618
createQuote() {
609619
this.router.navigate(['/quotes/new']);
610620
}
@@ -1058,8 +1068,9 @@ export class QuoteListComponent implements OnInit {
10581068

10591069
switch (actionType) {
10601070
case 'viewDetails':
1061-
case 'chat':
10621071
return isCancelled; // Only disabled for cancelled quotes
1072+
case 'chat':
1073+
return false; // Chat is always available — users should be able to communicate regardless of quote status
10631074
case 'addAttachment':
10641075
case 'cancel':
10651076
return isFinalized; // Disabled for both accepted and cancelled

src/app/features/tenders/pages/tender-list/tender-list.component.ts

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -112,12 +112,7 @@ import { QUOTE_CATEGORIES, QUOTE_STATUSES, TENDER_COORDINATOR_STATUSES_LABELS, T
112112
class="form-select rounded-md border-gray-300 dark:bg-gray-700 dark:border-gray-800 dark:text-white shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
113113
>
114114
<option value="">All Statuses</option>
115-
<option value="pending">Pending</option>
116-
<option value="inProgress">In Progress</option>
117-
<option value="approved">Approved</option>
118-
<option value="rejected">Rejected</option>
119-
<option value="cancelled">Cancelled</option>
120-
<option value="accepted">Accepted</option>
115+
<option *ngFor="let opt of filterStatusOptions" [value]="opt.value">{{ opt.label }}</option>
121116
</select>
122117
</div>
123118
</div>
@@ -940,6 +935,19 @@ export class TenderListComponent implements OnInit {
940935
}
941936
}
942937

938+
get filterStatusOptions(): { value: string; label: string }[] {
939+
const labels = this.selectedRole === UI_ROLES.BUYER
940+
? TENDER_COORDINATOR_STATUSES_LABELS
941+
: TENDER_RELATED_QUOTES_LABELS_PROVIDER;
942+
return [
943+
{ value: QUOTE_STATUSES.PENDING, label: labels.PENDING },
944+
{ value: QUOTE_STATUSES.IN_PROGRESS, label: labels.IN_PROGRESS },
945+
{ value: QUOTE_STATUSES.APPROVED, label: labels.APPROVED },
946+
{ value: QUOTE_STATUSES.ACCEPTED, label: labels.ACCEPTED },
947+
{ value: QUOTE_STATUSES.CANCELLED, label: labels.CANCELLED },
948+
{ value: QUOTE_STATUSES.REJECTED, label: labels.REJECTED },
949+
];
950+
}
943951

944952
viewDetails(quote: Quote) {
945953
this.selectedQuoteId = quote.id!;
@@ -1769,8 +1777,9 @@ export class TenderListComponent implements OnInit {
17691777

17701778
switch (actionType) {
17711779
case 'viewDetails':
1772-
case 'chat':
17731780
return isCancelled; // Only disabled for cancelled quotes
1781+
case 'chat':
1782+
return false; // Chat is always available — users should be able to communicate regardless of quote status
17741783
case 'addAttachment':
17751784
case 'cancel':
17761785
return isFinalized; // Disabled for both accepted and cancelled

src/app/services/provider.service.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -95,11 +95,9 @@ export class ProviderService {
9595
if (Array.isArray(response)) return response as Provider[];
9696
if (response?.data && Array.isArray(response.data)) return response.data as Provider[];
9797
return [];
98-
}),
99-
catchError((error) => {
100-
console.warn('Providers for tender (new) API failed:', error);
101-
return of([]);
10298
})
99+
// No catchError here — callers must handle HTTP errors themselves so they can
100+
// distinguish a genuine empty search result from a failed request.
103101
);
104102
}
105103

src/app/shared/create-tender-modal/create-tender-modal.component.ts

Lines changed: 105 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { QuoteService } from 'src/app/features/quotes/services/quote.service';
88
import { NotificationService } from 'src/app/services/notification.service';
99
import { LocalStorageService } from 'src/app/services/local-storage.service';
1010
import { ProviderService, Provider } from 'src/app/services/provider.service';
11+
import { AccountServiceService } from 'src/app/services/account-service.service';
1112
import { Tender, TenderAttachment } from 'src/app/models/tender.model';
1213
import { LoginInfo } from 'src/app/models/interfaces';
1314
import { API_ROLES } from 'src/app/models/roles.constants';
@@ -436,6 +437,7 @@ export class CreateTenderModalComponent implements OnInit, OnChanges {
436437
private notificationService = inject(NotificationService);
437438
private localStorage = inject(LocalStorageService);
438439
private providerService = inject(ProviderService);
440+
private accountService = inject(AccountServiceService);
439441
private router = inject(Router);
440442

441443
// Properties for tender creation modal
@@ -520,8 +522,8 @@ export class CreateTenderModalComponent implements OnInit, OnChanges {
520522
}
521523
}
522524

523-
// Load filter options
524-
this.loadFilterOptions();
525+
// Filter options (categories, countries, compliance levels) and the provider list
526+
// are loaded lazily in proceedToProviderSelection() when the user enters step 3.
525527
}
526528

527529
ngOnChanges(changes: SimpleChanges) {
@@ -648,6 +650,8 @@ export class CreateTenderModalComponent implements OnInit, OnChanges {
648650

649651
// Move to Step 2: Date fields
650652
this.tenderCreationStep = 2;
653+
// Notify the parent dashboard so the new tender appears in the list immediately
654+
this.tenderUpdated.emit();
651655
},
652656
error: (error) => {
653657
console.error('Error creating tender:', error);
@@ -825,6 +829,16 @@ export class CreateTenderModalComponent implements OnInit, OnChanges {
825829
this.tenderLoading = false;
826830
this.notificationService.showSuccess('Tender details saved successfully!');
827831
this.tenderCreationStep = 3;
832+
833+
// Reset any previously active filters silently (emitEvent:false avoids
834+
// triggering the valueChanges → emitFilters → loadTenderProviders chain)
835+
this.orgFilters = { categories: [], countries: [], complianceLevels: [] };
836+
this.countriesCtrl.setValue([], { emitEvent: false });
837+
this.categoriesCtrl.setValue([], { emitEvent: false });
838+
this.complianceLevelsCtrl.setValue([], { emitEvent: false });
839+
840+
// Load filter criteria and the provider list in parallel
841+
this.loadFilterOptions();
828842
this.loadTenderProviders();
829843
},
830844
error: (error: any) => {
@@ -836,51 +850,29 @@ export class CreateTenderModalComponent implements OnInit, OnChanges {
836850
}
837851

838852
/**
839-
* Load providers from the provider API
853+
* Load providers from the search API.
854+
* An empty result set is valid (no providers match the active filters).
855+
* Only falls back to the full party-organisation list when the search
856+
* endpoint returns an actual HTTP error.
840857
*/
841858
loadTenderProviders() {
842859
this.tenderLoading = true;
843860
this.tenderError = null;
844861

845-
console.log('Loading providers from API...');
846-
847862
this.providerService.getProvidersForTenderNew(this.orgFilters).subscribe({
848863
next: (providers) => {
849-
// If search endpoint returns empty or fails, fallback to basic endpoint
850-
if (!providers || providers.length === 0) {
851-
console.log('Search returned no providers, trying fallback endpoint...');
852-
this.providerService.getProvidersForTender().subscribe({
853-
next: (fallbackProviders) => {
854-
this.tenderProviders = fallbackProviders;
855-
console.log('Fallback loaded providers:', fallbackProviders.length);
856-
this.tenderLoading = false;
857-
this.updateAvailableProviders();
858-
859-
if (this.tenderCreationStep === 3) {
860-
this.loadInvitedProviders();
861-
}
862-
},
863-
error: (fallbackErr) => {
864-
this.tenderError = 'Failed to load providers from both endpoints: ' + (fallbackErr.message || 'Unknown error');
865-
this.tenderLoading = false;
866-
console.error('Fallback endpoint also failed:', fallbackErr);
867-
}
868-
});
869-
} else {
870-
this.tenderProviders = providers;
871-
console.log('Search loaded providers:', providers.length);
872-
this.tenderLoading = false;
873-
this.updateAvailableProviders();
874-
875-
// After providers are loaded, load invited providers (if in edit mode)
876-
if (this.tenderCreationStep === 3) {
877-
this.loadInvitedProviders();
878-
}
864+
this.tenderProviders = providers ?? [];
865+
console.log('Search loaded providers:', this.tenderProviders.length);
866+
this.tenderLoading = false;
867+
this.updateAvailableProviders();
868+
869+
if (this.tenderCreationStep === 3) {
870+
this.loadInvitedProviders();
879871
}
880872
},
881873
error: (err) => {
882-
// Search endpoint failed completely, try fallback
883-
console.warn('Search endpoint failed, trying fallback...', err);
874+
// HTTP error from the search endpoint — fall back to the full organisation list
875+
console.warn('Search endpoint returned an error, falling back to full provider list:', err);
884876
this.providerService.getProvidersForTender().subscribe({
885877
next: (fallbackProviders) => {
886878
this.tenderProviders = fallbackProviders;
@@ -895,7 +887,7 @@ export class CreateTenderModalComponent implements OnInit, OnChanges {
895887
error: (fallbackErr) => {
896888
this.tenderError = 'Failed to load providers: ' + (fallbackErr.message || 'Unknown error');
897889
this.tenderLoading = false;
898-
console.error('Error loading tender providers:', fallbackErr);
890+
console.error('Fallback endpoint also failed:', fallbackErr);
899891
}
900892
});
901893
}
@@ -987,7 +979,13 @@ export class CreateTenderModalComponent implements OnInit, OnChanges {
987979
}
988980

989981
/**
990-
* Load already invited providers by fetching tendering quotes with the coordinator quote's externalId
982+
* Load already invited providers by fetching tendering quotes with the coordinator quote's externalId.
983+
*
984+
* Name resolution priority:
985+
* 1. Match the provider's org URN (tender.selectedProviders[0]) against the already-loaded
986+
* tenderProviders list — this covers the common case with no extra API calls.
987+
* 2. Fall back to AccountServiceService.getOrgInfo() for providers not in the cached list
988+
* (e.g. when reopening the modal without navigating to the provider-search step first).
991989
*/
992990
loadInvitedProviders() {
993991
if (!this.createdQuoteId || !this.currentUserId) {
@@ -998,34 +996,46 @@ export class CreateTenderModalComponent implements OnInit, OnChanges {
998996
console.log('Loading invited providers for externalId:', this.createdQuoteId);
999997

1000998
this.tenderLoading = true;
1001-
999+
10021000
this.quoteService.getTenderingQuotesByUser(this.currentUserId, API_ROLES.BUYER).subscribe({
1003-
next: (tenders) => {
1001+
next: async (tenders) => {
10041002
console.log('Received tenders:', tenders);
1005-
1006-
// Clear existing invited providers
1007-
this.invitedProviders = [];
1008-
1009-
// Filter tenders that match our createdQuoteId as externalId
1010-
const matchingTenders = tenders.filter(t => t.external_id === this.createdQuoteId);
1011-
1012-
// Convert to invited providers format
1013-
matchingTenders.forEach(tender => {
1014-
// TODO: Get proper provider info once provider service is available
1015-
const provider: Provider = {
1016-
id: tender.provider || undefined,
1017-
tradingName: tender.provider || 'Unknown Provider'
1018-
};
1019-
1020-
if (tender.id) {
1021-
this.invitedProviders.push({
1022-
provider: provider,
1023-
quoteId: tender.id
1024-
});
1025-
console.log('Added invited provider:', provider.tradingName, 'with quote ID:', tender.id);
1026-
}
1027-
});
1028-
1003+
1004+
// Filter tenders that match our coordinator quote as their parent
1005+
const matchingTenders = tenders.filter(t => t.external_id === this.createdQuoteId && !!t.id);
1006+
1007+
// Resolve provider display names, then populate invitedProviders
1008+
const entries = await Promise.all(
1009+
matchingTenders.map(async (tender) => {
1010+
// The org URN lives in selectedProviders[0] (mapped from relatedParty[Seller].id)
1011+
const providerOrgUrn = tender.selectedProviders?.[0];
1012+
1013+
// 1. Try the already-loaded provider list first (no extra network call)
1014+
const knownProvider = providerOrgUrn
1015+
? this.tenderProviders.find(p => p.id === providerOrgUrn)
1016+
: undefined;
1017+
1018+
if (knownProvider) {
1019+
return { provider: knownProvider, quoteId: tender.id! };
1020+
}
1021+
1022+
// 2. Fall back to account service to get the trading name by org URN
1023+
let tradingName = providerOrgUrn || 'Unknown Provider';
1024+
if (providerOrgUrn) {
1025+
try {
1026+
const org = await this.accountService.getOrgInfo(providerOrgUrn);
1027+
tradingName = org?.tradingName || org?.name || providerOrgUrn;
1028+
} catch {
1029+
// Network error — keep the URN as a recognisable fallback
1030+
}
1031+
}
1032+
1033+
const provider: Provider = { id: providerOrgUrn, tradingName };
1034+
return { provider, quoteId: tender.id! };
1035+
})
1036+
);
1037+
1038+
this.invitedProviders = entries;
10291039
console.log('Total invited providers loaded:', this.invitedProviders.length);
10301040
this.tenderLoading = false;
10311041
},
@@ -1155,8 +1165,33 @@ export class CreateTenderModalComponent implements OnInit, OnChanges {
11551165
this.tenderLoading = false;
11561166
},
11571167
error: (error) => {
1158-
console.error('Error deleting quote:', error);
1159-
this.notificationService.showError('Failed to remove provider invitation: ' + (error.message || 'Unknown error'));
1168+
// TEMPORARY WORKAROUND — sandbox environment issue:
1169+
// The TMForum/BAE backend successfully deletes the quote but then attempts to
1170+
// notify a downstream microservice (charging/events) that is unreachable in sandbox.
1171+
// This causes the BAE to return 500 {error: "Service unreachable"} AFTER the deletion
1172+
// has already completed. As a result, the HTTP 500 reaches this error handler even
1173+
// though the underlying operation succeeded.
1174+
//
1175+
// We detect this specific case (HTTP 500 + "Service unreachable" in the response body)
1176+
// and treat it as a success so the UI stays consistent with the actual backend state.
1177+
//
1178+
// TODO: Remove this workaround once the sandbox downstream service is reachable
1179+
// and the BAE no longer returns 500 on successful quote deletion.
1180+
const isKnownFalsePositive =
1181+
error.status === 500 &&
1182+
error.error?.error === 'Service unreachable';
1183+
1184+
if (isKnownFalsePositive) {
1185+
console.warn(
1186+
'[WORKAROUND] deleteQuote returned 500 "Service unreachable" for quoteId:', quoteId,
1187+
'— quote was deleted on the backend. Removing from UI anyway.'
1188+
);
1189+
this.invitedProviders = this.invitedProviders.filter(ip => ip.quoteId !== quoteId);
1190+
this.notificationService.showSuccess('Provider invitation removed successfully');
1191+
} else {
1192+
console.error('Error deleting quote:', error);
1193+
this.notificationService.showError('Failed to remove provider invitation: ' + (error.message || 'Unknown error'));
1194+
}
11601195
this.tenderLoading = false;
11611196
}
11621197
});
@@ -1245,10 +1280,11 @@ export class CreateTenderModalComponent implements OnInit, OnChanges {
12451280
}
12461281

12471282
/**
1248-
* Load filter options (countries, categories, compliance levels)
1283+
* Load filter options (countries, categories, compliance levels).
1284+
* Only fetches the option lists — does NOT reset selected filter values
1285+
* or trigger a provider reload.
12491286
*/
12501287
private loadFilterOptions(): void {
1251-
this.clearFilters();
12521288
this.providerService.getFilterOptions().subscribe({
12531289
next: ({ categories, countries, complianceLevels }) => {
12541290
this.categoriesOptions = categories ?? [];

0 commit comments

Comments
 (0)