Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
80bf9e0
Category browsing (#241)
imitev-bae Apr 22, 2026
302b811
Merge branch 'main' into feat/new-search-page
fdelavega Apr 27, 2026
c59beff
Merge branch 'main' into feat/new-search-page
fdelavega May 6, 2026
2ce85d5
SBX DEV2 Added size to search provider's request, cleaned branch
BazRoe May 12, 2026
00640ef
Merge branch 'main' into feat/new-search-page
fdelavega May 13, 2026
ff35ec9
Merge branch 'main' into tender_qa_fixing
BazRoe May 15, 2026
b53d293
Category browsing (#241)
imitev-bae Apr 22, 2026
8445912
Recover providers catalog link
fdelavega May 18, 2026
1865941
Merge
fdelavega May 18, 2026
12fd522
SBX DEV2, fix to strip ?size from the called URL
BazRoe May 19, 2026
1926fce
Addedd comment
BazRoe May 19, 2026
0953d67
Merge branch 'main' into tender_qa_fixing
BazRoe May 19, 2026
61f0e7a
Merge remote-tracking branch 'refs/remotes/upstream/main' into tender…
Sh3rd3n May 20, 2026
c454365
Merge remote-tracking branch 'refs/remotes/upstream/feat/new-search-p…
Sh3rd3n May 20, 2026
42422ba
SBX first batch of ticket fixes
BazRoe May 22, 2026
1cf954c
SBX added variable to hide open and close tender buttons in PRD
BazRoe May 22, 2026
9bb6dfa
test: define tender provider search filter contract
Sh3rd3n May 21, 2026
7bc49ff
feat: load provider countries from shared DOME list
Sh3rd3n May 21, 2026
2d57f86
feat: wire live tender provider filters
Sh3rd3n May 21, 2026
fd19615
style: align tender dashboard with browse search
Sh3rd3n May 21, 2026
f657a81
fix: ignore stale tender filter responses
Sh3rd3n May 21, 2026
d17cce5
style: unify tendering surfaces
Sh3rd3n May 21, 2026
6ea2a82
fix: keep tender provider selection order stable
Sh3rd3n May 21, 2026
6112bc7
style: separate tender table headers
Sh3rd3n May 21, 2026
b4fc746
fix: load tender filter option lists
Sh3rd3n May 21, 2026
d9a216f
fix: avoid unfiltered tender fallback on active filters
Sh3rd3n May 21, 2026
5af4bae
fix: route tender provider search through backend base url
Sh3rd3n May 21, 2026
f55fe51
style: compact tender provider modal
Sh3rd3n May 21, 2026
17b399f
fix: prevent tender modal horizontal overflow
Sh3rd3n May 21, 2026
e48fb4a
fix: close tender filter dropdowns on outside click
Sh3rd3n May 21, 2026
b0689ec
style: clarify tender hover and selected states
Sh3rd3n May 22, 2026
4613111
style: use friendly tender status labels
Sh3rd3n May 22, 2026
872d4f3
style: improve provider tender status badge fit
Sh3rd3n May 22, 2026
6e94e81
fix: prevent quote details modal micro scroll
Sh3rd3n May 22, 2026
b73d2e6
Merge branch 'tender_qa_fixing' into feature/tendering-search-filters…
Sh3rd3n May 22, 2026
af49afe
test: stabilize search filter readiness
Sh3rd3n May 25, 2026
6b12e28
fix: add provider country config to environments
Sh3rd3n May 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/app/app-routing.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,11 @@ const routes: Routes = [
canActivate: [AuthGuard], data: { roles: ['admin'] }
},

{
path: 'browse',
loadComponent: () => import('./pages/browse/browse.component').then(c => c.BrowseComponent),
},

{
path: 'landing-page',
children: [{
Expand Down
14 changes: 13 additions & 1 deletion src/app/data/availableFilters.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,29 @@
export type Filter = {
name: string
label?: string
clientSide?: boolean
children?: Filter[]
}

const availableFilters: Filter[] = [
export const availableFilters: Filter[] = [
{
name: 'compliance_profile',
label: 'Compliance level',
children: [
{ name: 'Baseline' },
{ name: 'Professional' },
{ name: 'Professional+' },
],
},
{
name: 'procurement_type',
label: 'Procurement type',
clientSide: true,
children: [
{ name: 'Ready to Buy' },
{ name: 'Request Quote' },
],
},
]

export default availableFilters
29 changes: 29 additions & 0 deletions src/app/data/categoryIcons.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { IconDefinition } from '@fortawesome/fontawesome-svg-core';
import {
faBolt,
faBrain,
faCode,
faLayerGroup,
faPlug,
faServer,
faShieldHalved,
faUserTie
} from '@fortawesome/pro-solid-svg-icons';

export const DEFAULT_CATEGORY_ICON: IconDefinition = faLayerGroup;

export const CATEGORY_ICONS: Record<string, IconDefinition> = {
Security: faShieldHalved,
Infrastructure: faServer,
Productivity: faBolt,
DevOps: faCode,
Professional: faUserTie,
Specific: faLayerGroup,
'Data and AI': faBrain,
Integration: faPlug,
};

export function iconForCategory(name: string | null | undefined): IconDefinition {
if (!name) return DEFAULT_CATEGORY_ICON;
return CATEGORY_ICONS[name] ?? DEFAULT_CATEGORY_ICON;
}
12 changes: 7 additions & 5 deletions src/app/features/quotes/services/quote.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Injectable } from '@angular/core';
import { HttpClient, HttpParams, HttpHeaders } from '@angular/common/http';
import { Observable, map, forkJoin } from 'rxjs';
import { Quote, Quote_Create, Quote_Update, QuoteStateType } from 'src/app/models/quote.model';
import { Tender } from 'src/app/models/tender.model';
import { Tender, TenderStateType } from 'src/app/models/tender.model';
import { ApiRole, API_ROLES } from 'src/app/models/roles.constants';
import { QUOTE_STATUSES, QUOTE_CATEGORIES } from 'src/app/models/quote.constants';
import { environment } from '../../../../environments/environment';
Expand Down Expand Up @@ -577,14 +577,16 @@ export class QuoteService {
// - pending → draft → 'draft'
// - inProgress → pre-launched → 'pre-launched'
// - approved → sent → 'launched'
// - accepted/cancelled/rejected → closed → 'closed'
let state: 'draft' | 'pre-launched' | 'pending' | 'sent' | 'closed' = 'draft';
// - accepted → closed → 'closed'
// - cancelled → cancelled (preserved to distinguish from accepted/rejected)
// - rejected → rejected (preserved to distinguish from accepted/cancelled)
let state: TenderStateType = 'draft';
if (quoteItemState === QUOTE_STATUSES.PENDING) state = 'draft';
else if (quoteItemState === QUOTE_STATUSES.IN_PROGRESS) state = 'pre-launched';
else if (quoteItemState === QUOTE_STATUSES.APPROVED) state = 'sent';
else if (quoteItemState === QUOTE_STATUSES.ACCEPTED) state = 'closed';
else if (quoteItemState === QUOTE_STATUSES.CANCELLED) state = 'closed';
else if (quoteItemState === QUOTE_STATUSES.REJECTED) state = 'closed';
else if (quoteItemState === QUOTE_STATUSES.CANCELLED) state = 'cancelled';
else if (quoteItemState === QUOTE_STATUSES.REJECTED) state = 'rejected';

// Extract external_id, provider and buyerPartyId from quote
const external_id = quote.externalId;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { Router } from '@angular/router';
import { of } from 'rxjs';
import { TenderListComponent } from './tender-list.component';
import { QuoteService } from '../../../quotes/services/quote.service';
import { LocalStorageService } from 'src/app/services/local-storage.service';
import { NotificationService } from 'src/app/services/notification.service';
import { AccountServiceService } from 'src/app/services/account-service.service';
import { ProviderService } from 'src/app/services/provider.service';
import { ApiServiceService } from 'src/app/services/product-service.service';
import { QUOTE_CATEGORIES, QUOTE_STATUSES } from 'src/app/models/quote.constants';
import { UI_ROLES } from 'src/app/models/roles.constants';
import { Quote } from 'src/app/models/quote.model';

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

beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [TenderListComponent],
providers: [
{
provide: QuoteService,
useValue: {
getCoordinatorQuotesByUser: () => of([]),
getTenderingQuotesByUser: () => of([]),
},
},
{
provide: LocalStorageService,
useValue: {
getObject: () => ({
id: 'user-1',
logged_as: 'user-1',
partyId: 'party-1',
organizations: [],
}),
},
},
{ provide: NotificationService, useValue: { showError: jasmine.createSpy('showError'), showSuccess: jasmine.createSpy('showSuccess') } },
{ provide: AccountServiceService, useValue: {} },
{ provide: ProviderService, useValue: { getProviderCountryOptions: () => of([]) } },
{ provide: ApiServiceService, useValue: {} },
{ provide: Router, useValue: { navigate: jasmine.createSpy('navigate') } },
],
}).compileComponents();

fixture = TestBed.createComponent(TenderListComponent);
component = fixture.componentInstance;
});

it('renders tender status badges with a bordered surface so they remain distinct in tables', () => {
fixture.detectChanges();

component.selectedRole = UI_ROLES.BUYER;
component.loading = false;
component.error = null;
component.filteredQuotes = [
{
id: 'quote-1',
category: QUOTE_CATEGORIES.COORDINATOR,
description: 'Tender with closed state',
quoteItem: [{ state: QUOTE_STATUSES.ACCEPTED }],
} as Quote,
];

fixture.detectChanges();

const badge = fixture.nativeElement.querySelector('.status-badge') as HTMLElement;

expect(badge).not.toBeNull();

const computedStyle = getComputedStyle(badge);

expect(computedStyle.borderStyle).not.toBe('none');
expect(computedStyle.borderWidth).not.toBe('0px');
});

it('shows tender status filter options with user friendly labels', () => {
component.selectedRole = UI_ROLES.BUYER;

const labels = component.filterStatusOptions.map(option => option.label);

expect(labels).toContain('Not Yet Submitted');
expect(labels).toContain('Invites Sent, Waiting Acceptance');
expect(labels).toContain('Tender Started');
expect(labels).toContain('Tender Closed');
expect(labels.some(label => label.includes('-'))).toBeFalse();
});

it('renders tender status badges with user friendly text', () => {
fixture.detectChanges();

component.selectedRole = UI_ROLES.BUYER;
component.loading = false;
component.error = null;
component.filteredQuotes = [
{
id: 'quote-1',
category: QUOTE_CATEGORIES.COORDINATOR,
description: 'Tender with closed state',
quoteItem: [{ state: QUOTE_STATUSES.ACCEPTED }],
} as Quote,
];

fixture.detectChanges();

const badge = fixture.nativeElement.querySelector('.status-badge') as HTMLElement;

expect(badge.textContent?.trim()).toBe('Tender Closed');
});

it('keeps provider tender status badges readable in the table row', () => {
fixture.detectChanges();

component.selectedRole = UI_ROLES.SELLER;
component.loading = false;
component.error = null;
component.filteredQuotes = [
{
id: 'quote-1',
category: QUOTE_CATEGORIES.TENDER,
description: 'Provider invite',
quoteDate: '2026-05-22T00:00:00.000Z',
quoteItem: [{ state: QUOTE_STATUSES.PENDING }],
relatedParty: [],
} as Quote,
];

fixture.detectChanges();

const badge = fixture.nativeElement.querySelector('.status-badge') as HTMLElement;
const row = fixture.nativeElement.querySelector('[data-quote-id="quote-1"]') as HTMLElement;

expect(badge.textContent?.trim()).toBe('Invite Received');
expect(badge.className).toContain('whitespace-nowrap');
expect(row.className).toContain('grid-cols-12');
expect(row.querySelector('[data-testid="provider-tender-status-cell"]')?.className).toContain('col-span-2');
});
});
Loading
Loading