@@ -357,7 +259,10 @@ import { LoginInfo } from 'src/app/models/interfaces';
@@ -456,8 +361,10 @@ import { LoginInfo } from 'src/app/models/interfaces';
export class QuoteListComponent implements OnInit {
private router = inject(Router);
private quoteService = inject(QuoteService);
- private localStorage = inject(LocalStorageService,);
+ private localStorage = inject(LocalStorageService);
private notificationService = inject(NotificationService);
+ private accountService = inject(AccountServiceService);
+ private productService = inject(ApiServiceService);
quotes: Quote[] = [];
filteredQuotes: Quote[] = [];
@@ -467,6 +374,10 @@ export class QuoteListComponent implements OnInit {
deleteConfirmMessage = '';
quoteToDelete: Quote | null = null;
+ // Data enrichment maps
+ organizationNames: Map = new Map();
+ productNames: Map = new Map();
+
// State update modal
showStateUpdate = false;
quoteToUpdate: Quote | null = null;
@@ -527,13 +438,18 @@ export class QuoteListComponent implements OnInit {
this.quoteService.getQuotesByUserAndRole(this.currentUserId, this.selectedRole).subscribe({
next: (quotes) => {
this.quotes = quotes;
-
- // Debug: Log quote states
+
+ // Debug: Log quote states and product info
console.log('Loaded quotes:', quotes.length);
quotes.forEach(quote => {
console.log(`Quote ${this.extractShortId(quote.id)}: main state = "${quote.state}", primary state = "${this.getPrimaryState(quote)}"`);
+ const productOffering = quote.quoteItem?.[0]?.productOffering;
+ console.log(` - productOffering:`, productOffering);
});
-
+
+ // Enrich quote data with organization and product names
+ this.enrichQuoteData(quotes);
+
this.filterQuotesByStatus();
this.loading = false;
},
@@ -545,6 +461,83 @@ export class QuoteListComponent implements OnInit {
});
}
+ /**
+ * Check if an ID is an organization URN that can be fetched from the API
+ */
+ private isOrganizationId(id: string): boolean {
+ return id.startsWith('urn:ngsi-ld:organization:');
+ }
+
+ /**
+ * Enrich quote data by fetching organization names and product names
+ */
+ private enrichQuoteData(quotes: Quote[]) {
+ // Collect unique organization IDs and product offering IDs
+ const orgIds = new Set();
+ const productIds = new Set();
+
+ quotes.forEach(quote => {
+ // Collect organization IDs from relatedParty (only valid organization URNs)
+ quote.relatedParty?.forEach(party => {
+ if (party.id && !this.organizationNames.has(party.id) && this.isOrganizationId(party.id)) {
+ orgIds.add(party.id);
+ }
+ });
+
+ // Collect product offering IDs from quoteItems
+ quote.quoteItem?.forEach(item => {
+ if (item.productOffering?.id && !this.productNames.has(item.productOffering.id)) {
+ productIds.add(item.productOffering.id);
+ }
+ });
+ });
+
+ // Fetch organization names in parallel
+ if (orgIds.size > 0) {
+ const orgRequests = Array.from(orgIds).map(id => {
+ return this.accountService.getOrgInfo(id).then(
+ (org: any) => ({ id, name: org?.tradingName || org?.name || id }),
+ () => ({ id, name: id }) // Fallback to ID on error
+ );
+ });
+
+ Promise.all(orgRequests).then(results => {
+ results.forEach(({ id, name }) => {
+ this.organizationNames.set(id, name);
+ });
+ // Trigger change detection by reassigning filteredQuotes
+ this.filteredQuotes = [...this.filteredQuotes];
+ });
+ }
+
+ // Fetch product names in parallel
+ console.log('Product IDs to fetch:', Array.from(productIds));
+ if (productIds.size > 0) {
+ const productRequests = Array.from(productIds).map(id => {
+ console.log('Fetching product:', id);
+ return this.productService.getProductById(id).then(
+ (product: any) => {
+ console.log('Product fetched:', id, product?.name);
+ return { id, name: product?.name || id };
+ },
+ (error) => {
+ console.error('Product fetch error:', id, error);
+ return { id, name: id }; // Fallback to ID on error
+ }
+ );
+ });
+
+ Promise.all(productRequests).then(results => {
+ results.forEach(({ id, name }) => {
+ this.productNames.set(id, name);
+ });
+ console.log('Product names map:', Array.from(this.productNames.entries()));
+ // Trigger change detection by reassigning filteredQuotes
+ this.filteredQuotes = [...this.filteredQuotes];
+ });
+ }
+ }
+
refreshQuotes() {
this.loadQuotes();
}
@@ -569,6 +562,13 @@ export class QuoteListComponent implements OnInit {
return primaryState === this.statusFilter;
});
}
+
+ // Sort by quoteDate descending (newest first)
+ this.filteredQuotes.sort((a, b) => {
+ const dateA = a.quoteDate ? new Date(a.quoteDate).getTime() : 0;
+ const dateB = b.quoteDate ? new Date(b.quoteDate).getTime() : 0;
+ return dateB - dateA;
+ });
}
createQuote() {
@@ -590,6 +590,19 @@ export class QuoteListComponent implements OnInit {
this.selectedQuoteId = null;
}
+ onQuoteUpdated(updatedQuote: Quote) {
+ // Update the quote in the list
+ const index = this.quotes.findIndex(q => q.id === updatedQuote.id);
+ if (index !== -1) {
+ this.quotes[index] = updatedQuote;
+ this.filterQuotesByStatus();
+ }
+
+ const shortId = this.extractShortId(updatedQuote.id);
+ const state = this.getPrimaryState(updatedQuote);
+ this.notificationService.showSuccess(`Quote ${shortId} has been updated to ${state}.`);
+ }
+
closeChatModal() {
this.showChatModal = false;
this.selectedChatQuoteId = null;
@@ -875,6 +888,75 @@ export class QuoteListComponent implements OnInit {
return id.length > 8 ? id.slice(-8) : id;
}
+ /**
+ * Get the name of the other party based on the current role view.
+ * If viewing "as Customer" → show Provider/Seller name
+ * If viewing "as Provider" → show Buyer name
+ */
+ getOtherPartyName(quote: Quote): string {
+ if (!quote.relatedParty || quote.relatedParty.length === 0) {
+ return 'Unknown';
+ }
+
+ // Determine which role to look for based on current view
+ // Use environment roles: BUYER_ROLE = 'Buyer', SELLER_ROLE = 'Seller'
+ const targetRole = this.selectedRole === 'customer'
+ ? environment.SELLER_ROLE
+ : environment.BUYER_ROLE;
+
+ // Find the party with the target role (case-insensitive)
+ const party = quote.relatedParty.find(p =>
+ p.role?.toLowerCase() === targetRole.toLowerCase()
+ );
+
+ if (!party?.id) {
+ return 'Unknown';
+ }
+
+ // Look up the name from the enriched data map
+ const enrichedName = this.organizationNames.get(party.id);
+ if (enrichedName && enrichedName !== party.id) {
+ return enrichedName;
+ }
+
+ // Fallback: show loading indicator or shortened ID
+ return 'Loading...';
+ }
+
+ /**
+ * Get the product name from the quote's first quote item.
+ */
+ getProductName(quote: Quote): string {
+ if (!quote.quoteItem || quote.quoteItem.length === 0) {
+ return 'Unknown Product';
+ }
+
+ const firstItem = quote.quoteItem[0];
+ const productId = firstItem.productOffering?.id;
+
+ // Try to get name from productOffering if available (inline in quote data)
+ if (firstItem.productOffering?.name) {
+ return firstItem.productOffering.name;
+ }
+
+ // Look up the name from the enriched data map
+ if (productId) {
+ const enrichedName = this.productNames.get(productId);
+ if (enrichedName) {
+ return enrichedName;
+ }
+ // Still loading
+ return 'Loading...';
+ }
+
+ // Fallback to product name
+ if (firstItem.product?.name) {
+ return firstItem.product.name;
+ }
+
+ return 'Unknown Product';
+ }
+
getPrimaryState(quote: Quote): string {
// First try quoteItem state (this is where the actual state is stored)
if (Array.isArray(quote.quoteItem) && quote.quoteItem.length > 0) {
diff --git a/src/app/models/quote.constants.ts b/src/app/models/quote.constants.ts
new file mode 100644
index 00000000..a9b49a90
--- /dev/null
+++ b/src/app/models/quote.constants.ts
@@ -0,0 +1,100 @@
+/**
+ * Quote status constants and messages used throughout the application
+ *
+ */
+
+export interface StatusInfo {
+ explanation: string;
+ availableActions: string;
+}
+
+export interface StatusMessages {
+ provider: StatusInfo;
+ buyer: StatusInfo;
+}
+
+/**
+ * Quote status explanation messages for each state and role
+ */
+export const QUOTE_STATUS_MESSAGES: Record = {
+ pending: {
+ provider: {
+ explanation: 'At this stage you can decide whether to accept or reject the quote request.',
+ availableActions: 'Send messages to customer (chat), accept or decline the quote.'
+ },
+ buyer: {
+ explanation: 'The quote is created and sent to the provider. At this stage the provider must either accept or decline the request.',
+ availableActions: 'Send messages to provider (chat), view quote request details or cancel the request.'
+ }
+ },
+ inProgress: {
+ provider: {
+ explanation: "At this point you need to provide a proposal in the form of a PDF attachment.",
+ availableActions: 'Send messages to customer (chat), upload attachment or decline the quote.'
+ },
+ buyer: {
+ explanation: 'Your quote request is being processed. The provider is currently building the proposal based on your specifications. You will be notified once the quote is ready for review and a PDF is uploaded.',
+ availableActions: 'Send messages to provider (chat), view quote details or cancel the request.'
+ }
+ },
+ approved: {
+ provider: {
+ explanation: 'The quote details are now locked and cannot be modified. The quote has been sent to the customer. You should wait for the customer to review the proposal and use the chat to get in touch with them.',
+ availableActions: 'Send messages to customer (chat) or cancel the quote.'
+ },
+ buyer: {
+ explanation: 'The provider has approved and sent you a quote proposal. Please review the details and attached documents. You can accept, reject, or request updates through the chat.',
+ availableActions: 'Send messages to provider (chat), accept the proposal or reject the proposal or cancel the quote.'
+ }
+ },
+ accepted: {
+ provider: {
+ explanation: 'The customer has accepted your quote proposal. You should now create a customized version of the offering that will be visible only to the customer. The chat remains open for further communication.',
+ availableActions: 'Send messages to customer (chat), create customized offering.'
+ },
+ buyer: {
+ explanation: 'You have accepted the quote proposal. The provider will now create a customized offering based on this agreement. Once available, you can proceed with the normal order process to subscribe to the service.',
+ availableActions: 'Send messages to provider (chat).'
+ }
+ },
+ rejected: {
+ provider: {
+ explanation: 'The customer has rejected this quote proposal. The negotiation process has ended. You can reach out to the customer through the chat and consider submitting a new proposal.',
+ availableActions: 'View quote details and chat history.'
+ },
+ buyer: {
+ explanation: 'You have rejected this quote proposal. The negotiation process has been closed. If you wish, you can submit a new quote request with updated requirements.',
+ availableActions: 'View quote details and chat history.'
+ }
+ },
+ cancelled: {
+ provider: {
+ explanation: 'This quote has been cancelled.',
+ availableActions: 'View quote details and chat history. No further actions available.'
+ },
+ buyer: {
+ explanation: 'This quote has been cancelled.',
+ availableActions: 'View quote details and chat history. No further actions available.'
+ }
+ }
+};
+
+/**
+ * Auto-generated chat messages for status changes
+ */
+export const QUOTE_CHAT_MESSAGES = {
+ STATUS_CHANGE: (status: string) => `Status changed to: ${status}`,
+ ATTACHMENT_UPLOADED: (filename: string) => `Attachment uploaded: ${filename}`
+};
+
+/**
+ * Action button helper texts displayed next to buttons in quote details modal
+ */
+export const QUOTE_ACTION_BUTTON_TEXTS = {
+ ACCEPT_QUOTE_PROVIDER: 'Accept the request of the customer.',
+ ACCEPT_PROPOSAL_CUSTOMER: 'Accept the proposal that has been sent to you by the provider.',
+ CANCEL_QUOTE_PROVIDER: 'Cancel the quote request.',
+ REJECT_PROPOSAL_CUSTOMER: 'Reject the proposal.',
+ CREATE_OFFER: 'Create a customized offering based on this accepted quote.',
+ EXPECTED_DATE_REQUIRED: 'Set an expected date for the delivery of the proposal first'
+};
diff --git a/src/app/shared/chat-modal/chat-modal.component.ts b/src/app/shared/chat-modal/chat-modal.component.ts
index 9cf60ee4..6740002f 100644
--- a/src/app/shared/chat-modal/chat-modal.component.ts
+++ b/src/app/shared/chat-modal/chat-modal.component.ts
@@ -62,16 +62,22 @@ type QuoteNote = Note;