diff --git a/cypress/e2e/catalog.cy.ts b/cypress/e2e/catalog.cy.ts index 4cbe45a0..58bc2b6b 100644 --- a/cypress/e2e/catalog.cy.ts +++ b/cypress/e2e/catalog.cy.ts @@ -16,7 +16,7 @@ describe('/my-offerings',{ const status = 'Active' const newCatalog = { id: 'test', href: 'test',name: name, description: description, lifecycleStatus: status, relatedParty: { id: local_items.partyId, - role: 'Owner', + role: 'Seller', '@referredType': '' }} let calls = 0 @@ -52,12 +52,12 @@ describe('/my-offerings',{ const status = 'Active' const newCatalog = { id: 'test', href: 'test',name: name, description: description, lifecycleStatus: status, relatedParty: { id: local_items.partyId, - role: 'Owner', + role: 'Seller', '@referredType': '' }} const getCatalog = { id: 'test', href: 'test',name: check, description: description, lifecycleStatus: status, relatedParty: { id: local_items.partyId, - role: 'Owner', + role: 'Seller', '@referredType': '' }} let calls = 0 diff --git a/cypress/e2e/offering.cy.ts b/cypress/e2e/offering.cy.ts index f11ad5e9..2bc0437f 100644 --- a/cypress/e2e/offering.cy.ts +++ b/cypress/e2e/offering.cy.ts @@ -24,7 +24,7 @@ describe('/my-offerings',{ const newCatalog = { id: 'test', href: 'test',name: 'catalogTest', description: '', lifecycleStatus: 'Launched', relatedParty: [{ id: local_items.partyId, - role: 'Owner', + role: 'Seller', '@referredType': '' }]} @@ -75,7 +75,7 @@ describe('/my-offerings',{ const newCatalog = { id: 'catalogId', href: 'catalogId',name: 'catalogTest', description: '', lifecycleStatus: 'Launched', relatedParty: [{ id: local_items.partyId, - role: 'Owner', + role: 'Seller', '@referredType': '' }]} @@ -154,7 +154,7 @@ describe('/my-offerings',{ const newCatalog = { id: 'catalogId', href: 'catalogId',name: 'catalogTest', description: '', lifecycleStatus: 'Launched', relatedParty: [{ id: local_items.partyId, - role: 'Owner', + role: 'Seller', '@referredType': '' }]} @@ -168,7 +168,7 @@ describe('/my-offerings',{ { "id": "mock:organization", "href": "mock:organization", - "role": "owner", + "role": "Seller", "@referredType": null } ], @@ -262,7 +262,7 @@ describe('/my-offerings',{ const newCatalog = { id: 'catalogId', href: 'catalogId',name: 'catalogTest', description: '', lifecycleStatus: 'Launched', relatedParty: [{ id: local_items.partyId, - role: 'Owner', + role: 'Seller', '@referredType': '' }]} diff --git a/cypress/support/constants.ts b/cypress/support/constants.ts index 9a446c3b..0a0ccf53 100644 --- a/cypress/support/constants.ts +++ b/cypress/support/constants.ts @@ -157,7 +157,7 @@ export const catalog_launched = [ { "id": "urn:ngsi-ld:individual:b73dd8ce-b63f-4c5b-be07-ca7ea10ad78e", "href": "urn:ngsi-ld:individual:b73dd8ce-b63f-4c5b-be07-ca7ea10ad78e", - "role": "Owner", + "role": "Seller", "@referredType": "" } ], @@ -313,7 +313,7 @@ export const productOffering = { { id: "urn:ngsi-ld:individual:56c77de4-f136-4167-95f0-92a36983ee0f", href: "urn:ngsi-ld:individual:56c77de4-f136-4167-95f0-92a36983ee0f", - role: "Owner", + role: "Seller", "@referredType": "" } ] @@ -331,7 +331,7 @@ export const productSpec = { { id: "urn:ngsi-ld:individual:56c77de4-f136-4167-95f0-92a36983ee0f", href: "urn:ngsi-ld:individual:56c77de4-f136-4167-95f0-92a36983ee0f", - role: "Owner", + role: "Seller", "@referredType": "" } ], diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index bd5d8259..2ca64b4b 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -23,7 +23,7 @@ import { UsageSpecsComponent } from "src/app/pages/usage-specs/usage-specs.compo import { DomeBlogComponent } from "src/app/pages/dome-blog/dome-blog.component" import { BlogEntryDetailComponent } from "src/app/pages/dome-blog/blog-entry-detail/blog-entry-detail.component" import { EntryFormComponent } from "src/app/pages/dome-blog/entry-form/entry-form.component" -import { environment } from '../environments/environment'; + const routes: Routes = [ diff --git a/src/app/app.module.ts b/src/app/app.module.ts index e79fa83b..9019ffc6 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -86,6 +86,7 @@ import { MultipleSelectComponent } from './shared/multiple-select/multiple-selec import {CharacteristicComponent} from "./shared/characteristic/characteristic.component"; import {PricePlanDrawerComponent} from "./shared/price-plan-drawer/price-plan-drawer.component"; import {OfferComponent} from "./shared/forms/offer/offer.component"; +import {CustomOfferComponent} from "./shared/forms/offer/custom-offer/custom-offer.component" import { ThemeService } from './services/theme.service'; import { ThemeAwareTranslateLoader } from './services/theme-aware-translate.loader'; import { RevenueReportComponent } from './shared/revenue-report/revenue-report.component' @@ -196,6 +197,7 @@ import { OperatorRevenueSharingComponent } from "src/app/pages/admin/operator-re PricePlanDrawerComponent, RevenueReportComponent, OfferComponent, + CustomOfferComponent, AboutDomeComponent, MarkdownTextareaComponent, ProviderRevenueSharingComponent, diff --git a/src/app/chatbot-widget/chatbot-widget.component.ts b/src/app/chatbot-widget/chatbot-widget.component.ts index f89e3ae4..b88abf9e 100644 --- a/src/app/chatbot-widget/chatbot-widget.component.ts +++ b/src/app/chatbot-widget/chatbot-widget.component.ts @@ -62,14 +62,14 @@ export class ChatbotWidgetComponent { // Build the message depending on the user role let name = "guest" - let role = "Customer" // FIXME: Default role must be guest when supported + let role = environment.BUYER_ROLE; // FIXME: Default role must be guest when supported const userInfo = this.localStorage.getObject('login_items') as LoginInfo; // The user is logged in if (userInfo.id) { let roles = [] - role = "Customer" + role = environment.BUYER_ROLE; name = userInfo.username if (userInfo.logged_as !== userInfo.id) { @@ -83,7 +83,7 @@ export class ChatbotWidgetComponent { }) } - if (roles.includes("seller")) { + if (roles.includes(environment.SELLER_ROLE)) { role = "Provider" } } diff --git a/src/app/features/quotes/services/quote.service.ts b/src/app/features/quotes/services/quote.service.ts index 131c271a..3a2eb091 100644 --- a/src/app/features/quotes/services/quote.service.ts +++ b/src/app/features/quotes/services/quote.service.ts @@ -172,9 +172,9 @@ export class QuoteService { getQuotesByUserAndRole(userId: string, role: 'customer' | 'seller'): Observable { let params = new HttpParams(); // API expects 'Customer' or 'Seller' (capitalized) - const apiRole = role === 'customer' ? 'Customer' : 'Seller'; + const apiRole = role === 'customer' ? environment.BUYER_ROLE : environment.SELLER_ROLE; params = params.set('role', apiRole); - + const encodedUserId = encodeURIComponent(userId); console.log('Getting quotes by user with URL:', `${this.apiUrl}/quoteByUser/${encodedUserId}`); return this.http.get(`${this.apiUrl}/quoteByUser/${encodedUserId}`, { params, ...this.httpOptions }); diff --git a/src/app/guard/auth.guard.ts b/src/app/guard/auth.guard.ts index 81d246b4..e45221d9 100644 --- a/src/app/guard/auth.guard.ts +++ b/src/app/guard/auth.guard.ts @@ -4,6 +4,7 @@ import {LocalStorageService} from "../services/local-storage.service"; import { Observable } from 'rxjs'; import { LoginInfo } from '../models/interfaces'; import * as moment from 'moment'; +import { environment } from 'src/environments/environment'; @Injectable({ providedIn: 'root', @@ -18,21 +19,27 @@ export class AuthGuard implements CanActivate { ): Observable | Promise | boolean { let aux = this.localStorage.getObject('login_items') as LoginInfo; const requiredRoles = route.data['roles'] as Array; - let userRoles: string | any[] = []; + let userRoles: string[] = []; + + const roleMapper:any = { + 'admin': environment.ADMIN_ROLE.toLowerCase(), + 'seller': environment.SELLER_ROLE.toLowerCase(), + 'buyer': environment.BUYER_ROLE.toLowerCase(), + 'orgAdmin': environment.ORG_ADMIN_ROLE.toLowerCase(), + 'certifier': environment.CERTIFIER_ROLE.toLowerCase(), + 'individual': 'individual' + } if(JSON.stringify(aux) != '{}' && (((aux.expire - moment().unix())-4) > 0)) { if(aux.logged_as == aux.id){ userRoles.push('individual') - for(let i=0; i < aux.roles.length; i++){ - userRoles.push(aux.roles[i].name) - } + aux.roles.forEach((role: any) => userRoles.push(role.name.toLowerCase())) } else { let loggedOrg = aux.organizations.find((element: { id: any; }) => element.id == aux.logged_as) - for(let i=0;i role.name === 'admin')){ - userRoles.push('admin') + loggedOrg.roles.forEach((role: any) => userRoles.push(role.name.toLowerCase())) + + if(aux.roles.some(role => role.name.toLowerCase() === environment.ADMIN_ROLE.toLowerCase())){ + userRoles.push(environment.ADMIN_ROLE.toLowerCase()) } } } else { @@ -41,7 +48,9 @@ export class AuthGuard implements CanActivate { } if (requiredRoles.length != 0) { - const hasRequiredRoles = requiredRoles.some(role => userRoles.includes(role)); + const hasRequiredRoles = requiredRoles.some((role: any) => { + return userRoles.includes(roleMapper[role]); + }); if (!hasRequiredRoles) { this.router.navigate(['/dashboard']); // Navigate to an access denied page or login page diff --git a/src/app/pages/checkout/checkout.component.ts b/src/app/pages/checkout/checkout.component.ts index 964ed3b2..10ea60da 100644 --- a/src/app/pages/checkout/checkout.component.ts +++ b/src/app/pages/checkout/checkout.component.ts @@ -268,7 +268,7 @@ export class CheckoutComponent implements OnInit, OnDestroy { { id: this.relatedParty, href: this.relatedParty, - role: 'Customer' + role: environment.BUYER_ROLE } ], priority: '4', @@ -422,7 +422,7 @@ export class CheckoutComponent implements OnInit, OnDestroy { groupItemsByOwner(ownerId: any) { const itemsForOwner = this.items.filter((item: any) => { - const owner = item.relatedParty?.find((rp: any) => rp.role === 'Owner')?.id; + const owner = item.relatedParty?.find((rp: any) => rp.role === environment.SELLER_ROLE)?.id; return owner === ownerId; }); @@ -507,15 +507,20 @@ export class CheckoutComponent implements OnInit, OnDestroy { ba.selected = false; } this.selectedBillingAddress = baddr; + console.log('billing addr selected....') + console.log(this.selectedBillingAddress) const updatedItems = JSON.parse(JSON.stringify(this.items)); // we need a deep clone isntead of shallow clone + for (const cartItem of updatedItems){ + console.log('---- cart item ----') + console.log(cartItem) const response = await lastValueFrom( this.priceService.calculatePrice({ "productOrder":{ "id": uuidv4(), "productOrderItem":[{ action: "add", id: cartItem.id, // product offering id - itemTotalPrice:[{ + itemTotalPrice: cartItem.options.pricing.length == 0 ? [] : [{ productOfferingPrice:{ id: cartItem.options.pricing[0].id, //product offering price parent id href: cartItem.options.pricing[0].id, diff --git a/src/app/pages/product-details/product-details.component.ts b/src/app/pages/product-details/product-details.component.ts index 219ccfa7..0629e6e5 100644 --- a/src/app/pages/product-details/product-details.component.ts +++ b/src/app/pages/product-details/product-details.component.ts @@ -861,10 +861,10 @@ async deleteProduct(product: Product | undefined){ let parties = this.prodSpec?.relatedParty; if(parties) for(let i=0; i { - this.orgInfo=org; + this.orgInfo = org; console.log(this.orgInfo) }) } diff --git a/src/app/pages/product-inventory/inventory-resources/inventory-resources.component.html b/src/app/pages/product-inventory/inventory-resources/inventory-resources.component.html index 4a853790..b8588211 100644 --- a/src/app/pages/product-inventory/inventory-resources/inventory-resources.component.html +++ b/src/app/pages/product-inventory/inventory-resources/inventory-resources.component.html @@ -85,35 +85,7 @@
- - - - - - @if(loading){ diff --git a/src/app/pages/product-inventory/inventory-services/inventory-services.component.html b/src/app/pages/product-inventory/inventory-services/inventory-services.component.html index db1e85e5..7f1e242d 100644 --- a/src/app/pages/product-inventory/inventory-services/inventory-services.component.html +++ b/src/app/pages/product-inventory/inventory-services/inventory-services.component.html @@ -82,38 +82,9 @@
{{ 'PRODUCT_INVENTORY._terminated' | translate }} - - - - - - @if(loading){ diff --git a/src/app/pages/product-orders/product-orders.component.ts b/src/app/pages/product-orders/product-orders.component.ts index 069c7a02..d1f7055e 100644 --- a/src/app/pages/product-orders/product-orders.component.ts +++ b/src/app/pages/product-orders/product-orders.component.ts @@ -51,7 +51,7 @@ export class ProductOrdersComponent implements OnInit, OnDestroy { filters: any[]=[]; check_custom:boolean=false; isSeller:boolean=false; - role:any='Customer' + role:any=environment.BUYER_ROLE show_orders: boolean = true; show_invoices: boolean = false; @@ -117,8 +117,8 @@ export class ProductOrdersComponent implements OnInit, OnDestroy { let userRoles = aux.roles.map((elem: any) => { return elem.name }) - if (userRoles.includes("seller")) { - this.isSeller=true; + if (userRoles.includes(environment.SELLER_ROLE)) { + this.isSeller = true; } } else { let loggedOrg = aux.organizations.find((element: { id: any; }) => element.id == aux.logged_as); @@ -126,8 +126,8 @@ export class ProductOrdersComponent implements OnInit, OnDestroy { let orgRoles = loggedOrg.roles.map((elem: any) => { return elem.name }) - if (orgRoles.includes("seller")) { - this.isSeller=true; + if (orgRoles.includes(environment.SELLER_ROLE)) { + this.isSeller = true; } } //this.partyId = aux.partyId; diff --git a/src/app/pages/product-orders/sections/invoices-info/invoices-info.component.html b/src/app/pages/product-orders/sections/invoices-info/invoices-info.component.html index 6b6e2ce8..22fdfa4e 100644 --- a/src/app/pages/product-orders/sections/invoices-info/invoices-info.component.html +++ b/src/app/pages/product-orders/sections/invoices-info/invoices-info.component.html @@ -1,10 +1,10 @@ + @if (acbr.periodCoverage) {

diff --git a/src/app/pages/product-orders/sections/invoices-info/invoices-info.component.ts b/src/app/pages/product-orders/sections/invoices-info/invoices-info.component.ts index c842cabb..de0849a4 100644 --- a/src/app/pages/product-orders/sections/invoices-info/invoices-info.component.ts +++ b/src/app/pages/product-orders/sections/invoices-info/invoices-info.component.ts @@ -48,11 +48,15 @@ export class InvoicesInfoComponent implements OnInit, OnDestroy { page_check:boolean = true; page: number=0; INVOICE_LIMIT: number = environment.INVOICE_LIMIT; - filters: any[]=[]; - check_custom:boolean=false; - isSeller:boolean=false; - role:any='Customer' - name:any='' + + sellerRole: string = environment.SELLER_ROLE; + buyerRole: string = environment.BUYER_ROLE; + + filters: any[] = []; + check_custom:boolean = false; + isSeller:boolean = false; + role:any = this.buyerRole; + name:any = '' show_orders: boolean = true; show_billing: boolean = false; @@ -117,8 +121,8 @@ export class InvoicesInfoComponent implements OnInit, OnDestroy { let userRoles = aux.roles.map((elem: any) => { return elem.name }) - if (userRoles.includes("seller")) { - this.isSeller=true; + if (userRoles.includes(this.sellerRole)) { + this.isSeller = true; } } else { let loggedOrg = aux.organizations.find((element: { id: any; }) => element.id == aux.logged_as); @@ -126,13 +130,13 @@ export class InvoicesInfoComponent implements OnInit, OnDestroy { let orgRoles = loggedOrg.roles.map((elem: any) => { return elem.name }) - if (orgRoles.includes("seller")) { - this.isSeller=true; + if (orgRoles.includes(this.sellerRole)) { + this.isSeller = true; } } //this.partyId = aux.partyId; - this.page=0; - this.invoices=[]; + this.page = 0; + this.invoices = []; this.getInvoices(false); } initFlowbite(); @@ -272,10 +276,10 @@ export class InvoicesInfoComponent implements OnInit, OnDestroy { return totalPrice } - async toggleShowDetails(invoice:any){ + async toggleShowDetails(invoice: any){ console.log(invoice) - this.showInvoiceDetails=true; - this.invoiceToShow=invoice; + this.showInvoiceDetails = true; + this.invoiceToShow = invoice; this.appliedCustomerBillingRates = []; this.loadingACBRs = true; @@ -290,8 +294,8 @@ export class InvoicesInfoComponent implements OnInit, OnDestroy { } async onRoleChange(role: any) { - this.role=role; - console.log('ROLE',this.role); + this.role = role; + console.log('ROLE', this.role); await this.getInvoices(false); } @@ -301,23 +305,30 @@ export class InvoicesInfoComponent implements OnInit, OnDestroy { editInvoice(index: number, invoice: any) { this.editingIndex = index; - this.editableInvoiceName = invoice.name; + this.editableInvoiceName = invoice.billNo; } saveInvoice(index: number, invoice: any) { - let oldName = invoice.name; - invoice.name = this.editableInvoiceName; - this.invoicesService.updateInvoice(invoice, invoice.id).subscribe({ + let oldName = invoice.billNo; + invoice.billNo = this.editableInvoiceName; + this.invoicesService.updateInvoice({ + billNo: this.editableInvoiceName + }, invoice.id).subscribe({ next: data => { console.log('actualizado invoice') }, error: error => { - invoice.name = oldName; + invoice.billNo = oldName; console.error('There was an error while updating!', error); } }); this.editingIndex = null; } + downloadInvoice(invoice: any) { + console.log('Downloading invoice') + let url = `${environment.BASE_URL}/invoicing/invoices/${invoice.id}` + window.open(url, '_blank'); + } } diff --git a/src/app/pages/product-orders/sections/order-info/order-info.component.html b/src/app/pages/product-orders/sections/order-info/order-info.component.html index e1e7ac04..ffe75412 100644 --- a/src/app/pages/product-orders/sections/order-info/order-info.component.html +++ b/src/app/pages/product-orders/sections/order-info/order-info.component.html @@ -1,10 +1,10 @@

-
- - - - - diff --git a/src/app/pages/seller-offerings/offerings/seller-offer/seller-offer.component.html b/src/app/pages/seller-offerings/offerings/seller-offer/seller-offer.component.html index cbe6840f..34d5c0d3 100644 --- a/src/app/pages/seller-offerings/offerings/seller-offer/seller-offer.component.html +++ b/src/app/pages/seller-offerings/offerings/seller-offer/seller-offer.component.html @@ -187,6 +187,14 @@

{{ 'OFFERINGS._order_by' | translate }}

+ @if(customMap[offer.id]){ + + } diff --git a/src/app/pages/user-profile/user-profile.component.ts b/src/app/pages/user-profile/user-profile.component.ts index 208c4bdc..475f0028 100644 --- a/src/app/pages/user-profile/user-profile.component.ts +++ b/src/app/pages/user-profile/user-profile.component.ts @@ -13,6 +13,8 @@ import { initFlowbite } from 'flowbite'; import {EventMessageService} from "../../services/event-message.service"; import * as moment from 'moment'; import { environment } from 'src/environments/environment'; +import { lastValueFrom } from 'rxjs'; +import { HttpClient } from '@angular/common/http'; import { Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; @@ -37,7 +39,8 @@ export class UserProfileComponent implements OnInit, OnDestroy { constructor( private localStorage: LocalStorageService, private cdr: ChangeDetectorRef, - private eventMessage: EventMessageService + private eventMessage: EventMessageService, + private http: HttpClient ) { this.eventMessage.messages$ .pipe(takeUntil(this.destroy$)) @@ -130,6 +133,15 @@ export class UserProfileComponent implements OnInit, OnDestroy { initFlowbite(); } + getPayment() { + const paymentInfoUrl = `${environment.BASE_URL}/paymentInfo`; + + lastValueFrom(this.http.get(paymentInfoUrl)).then(data => { + window.open(data.providerUrl, '_blank'); + }).catch(() => { + }); + } + goToOrders(){ this.selectOrder(); this.show_billing=false; diff --git a/src/app/services/account-service.service.ts b/src/app/services/account-service.service.ts index dec8cdc9..0b20eccd 100644 --- a/src/app/services/account-service.service.ts +++ b/src/app/services/account-service.service.ts @@ -52,6 +52,11 @@ export class AccountServiceService { return lastValueFrom(this.http.get(url)); } + getOrgList(){ + let url = `${AccountServiceService.BASE_URL}/party/organization`; + return lastValueFrom(this.http.get(url)); + } + updateUserInfo(partyId:any,profile:any){ let url = `${AccountServiceService.BASE_URL}/party/individual/${partyId}`; return this.http.patch(url, profile); diff --git a/src/app/services/app-init.service.ts b/src/app/services/app-init.service.ts index 5cf0ddba..d27b62ba 100644 --- a/src/app/services/app-init.service.ts +++ b/src/app/services/app-init.service.ts @@ -31,6 +31,11 @@ export class AppInitService { environment.KB_GUIDELNES_URL = config.domeGuidelines; environment.REGISTRATION_FORM_URL = config.domeRegistrationForm; environment.DFT_CATALOG_ID = config.defaultId; + environment.SELLER_ROLE = config.roles.seller; + environment.BUYER_ROLE = config.roles.customer; + environment.ADMIN_ROLE = config.roles.admin; + environment.ORG_ADMIN_ROLE = config.roles.orgAdmin; + environment.CERTIFIER_ROLE = config.roles.certifier; environment.quoteApi = config.quoteApi ?? 'http://localhost:8080/quoteManagement'; environment.analytics = config.analytics ?? 'https://analytics.dome-marketplace-sbx.org/', environment.feedbackCampaign = config.feedbackCampaign ?? false, diff --git a/src/app/services/event-message.service.ts b/src/app/services/event-message.service.ts index eca269d9..fb82fa60 100644 --- a/src/app/services/event-message.service.ts +++ b/src/app/services/event-message.service.ts @@ -6,7 +6,7 @@ import { LoginInfo } from 'src/app/models/interfaces'; export interface EventMessage { type: 'AddedFilter' | 'RemovedFilter' | 'AddedCartItem' | 'RemovedCartItem' | 'FilterShown' | 'ToggleCartDrawer' | 'LoginProcess' | 'BillAccChanged' | 'SellerProductSpec' | 'SellerCreateProductSpec' | 'SellerServiceSpec' | 'SellerCreateServiceSpec' | 'SellerResourceSpec' | 'SellerCreateResourceSpec' | - 'SellerOffer' | 'SellerCreateOffer' | 'SellerUpdateProductSpec' | 'SellerUpdateServiceSpec' | 'SellerUpdateResourceSpec' | 'SellerUpdateOffer' | + 'SellerOffer' | 'SellerCreateOffer' | 'SellerUpdateProductSpec' | 'SellerUpdateServiceSpec' | 'SellerUpdateResourceSpec' | 'SellerUpdateOffer' | 'SellerCreateCustomOffer' | 'SellerCatalog' | 'SellerCatalogCreate' | 'SellerCatalogUpdate' | 'CategoryAdded' | 'CategoryRemoved' | 'ChangedSession' | 'CloseCartCard'| 'AdminCategories' | 'CreateCategory' | 'UpdateCategory' | 'ShowCartToast' | 'HideCartToast' | 'CloseContact' | 'OpenServiceDetails' | 'OpenResourceDetails' | 'OpenProductInvDetails' | 'SavePricePlan' | 'UpdatePricePlan' | 'ToggleEditPrice' | 'ToggleNewPrice' | @@ -109,6 +109,10 @@ export class EventMessageService { this.eventMessageSubject.next({ type: 'SellerUpdateOffer', value: offer }); } + emitSellerCreateCustomOffer(offer:any){ + this.eventMessageSubject.next({type: 'SellerCreateCustomOffer', value: offer}) + } + emitSellerCatalog(show:boolean){ this.eventMessageSubject.next({ type: 'SellerCatalog', value: show }); } diff --git a/src/app/services/product-service.service.ts b/src/app/services/product-service.service.ts index 81bba553..62b752de 100644 --- a/src/app/services/product-service.service.ts +++ b/src/app/services/product-service.service.ts @@ -141,8 +141,8 @@ export class ApiServiceService { return lastValueFrom(this.http.get(url)); } - getProductOfferByOwner(page:any,status:any[],partyId:any,sort:any,isBundle:any) { - let url = `${ApiServiceService.BASE_URL}${ApiServiceService.API_PRODUCT}/productOffering?limit=${ApiServiceService.PRODUCT_LIMIT}&offset=${page}&relatedParty=${partyId}`; + getProductOfferByOwner(page:any, status:any[], partyId:any, sort:any, isBundle:any) { + let url = `${ApiServiceService.BASE_URL}${ApiServiceService.API_PRODUCT}/productOffering?limit=${ApiServiceService.PRODUCT_LIMIT}&offset=${page}&relatedParty.id=${partyId}`; if(sort!=undefined){ url=url+'&sort='+sort @@ -340,6 +340,9 @@ export class ApiServiceService { postProductOffering(prod:any,catalogId:any){ //POST - El item va en el body de la petición let url = `${ApiServiceService.BASE_URL}${ApiServiceService.API_PRODUCT}/catalog/${catalogId}/productOffering`; + if(!catalogId){ + url = `${ApiServiceService.BASE_URL}${ApiServiceService.API_PRODUCT}/productOffering`; + } return this.http.post(url, prod); } diff --git a/src/app/shared/billing-account-form/billing-account-form.component.ts b/src/app/shared/billing-account-form/billing-account-form.component.ts index acde475e..b9c94414 100644 --- a/src/app/shared/billing-account-form/billing-account-form.component.ts +++ b/src/app/shared/billing-account-form/billing-account-form.component.ts @@ -25,6 +25,7 @@ import {getCountries, getCountryCallingCode, CountryCode} from 'libphonenumber-j import {parsePhoneNumber} from 'libphonenumber-js/max' import {TranslateModule} from "@ngx-translate/core"; import { getLocaleId } from '@angular/common'; +import { environment } from 'src/environments/environment'; import { Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; @@ -151,7 +152,7 @@ export class BillingAccountFormComponent implements OnInit, OnDestroy { id: this.partyId, name: aux.user, href : this.partyId, - role: "Owner" + role: environment.SELLER_ROLE } } else { let loggedOrg = aux.organizations.find((element: { id: any; }) => element.id == aux.logged_as) @@ -162,7 +163,7 @@ export class BillingAccountFormComponent implements OnInit, OnDestroy { id: this.partyId, name: loggedOrg.name, href : this.partyId, - role: "Owner" + role: environment.SELLER_ROLE } } } diff --git a/src/app/shared/card/card.component.ts b/src/app/shared/card/card.component.ts index 7bbf3ab8..c23ceaad 100644 --- a/src/app/shared/card/card.component.ts +++ b/src/app/shared/card/card.component.ts @@ -625,7 +625,7 @@ async deleteProduct(product: Product | undefined){ let parties = this.prodSpec?.relatedParty; if(parties) for(let i=0; i { this.orgInfo=org; diff --git a/src/app/shared/cart-drawer/cart-drawer.component.html b/src/app/shared/cart-drawer/cart-drawer.component.html index 3825bd50..e9e79beb 100644 --- a/src/app/shared/cart-drawer/cart-drawer.component.html +++ b/src/app/shared/cart-drawer/cart-drawer.component.html @@ -107,7 +107,7 @@
- + + + @if(currentStep === 3){ + + } +
+ + } + + + @if(showError){ +
+ +
+ } + \ No newline at end of file diff --git a/src/app/shared/forms/offer/custom-offer/custom-offer.component.spec.ts b/src/app/shared/forms/offer/custom-offer/custom-offer.component.spec.ts new file mode 100644 index 00000000..4e08da7f --- /dev/null +++ b/src/app/shared/forms/offer/custom-offer/custom-offer.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { CustomOfferComponent } from './custom-offer.component'; + +describe('CustomOfferComponent', () => { + let component: CustomOfferComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [CustomOfferComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(CustomOfferComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/shared/forms/offer/custom-offer/custom-offer.component.ts b/src/app/shared/forms/offer/custom-offer/custom-offer.component.ts new file mode 100644 index 00000000..824ad475 --- /dev/null +++ b/src/app/shared/forms/offer/custom-offer/custom-offer.component.ts @@ -0,0 +1,408 @@ +import {Component, Input, OnInit, OnDestroy} from '@angular/core'; +import {FormBuilder, FormControl, FormGroup, ReactiveFormsModule, Validators} from "@angular/forms"; +import {TranslateModule} from "@ngx-translate/core"; +import {ProdSpecComponent} from "../prod-spec/prod-spec.component"; +import {NgClass, NgIf} from "@angular/common"; +import {ApiServiceService} from "../../../../services/product-service.service"; +import {PricePlansComponent} from "../price-plans/price-plans.component"; +import {ProcurementModeComponent} from "../procurement-mode/procurement-mode.component" +import {RelatedPartyIdComponent} from "../related-party-id/related-party-id.component" +import {OfferSummaryComponent} from "../offer-summary/offer-summary.component" +import { lastValueFrom } from 'rxjs'; +import {components} from "src/app/models/product-catalog"; +import {EventMessageService} from "src/app/services/event-message.service"; +import {FormChangeState, PricePlanChangeState} from "../../../../models/interfaces"; +import {Subscription} from "rxjs"; +import * as moment from 'moment'; +import { environment } from 'src/environments/environment'; + +type ProductOffering_Create = components["schemas"]["ProductOffering_Create"]; +type ProductOfferingPrice = components["schemas"]["ProductOfferingPrice"] + +@Component({ + selector: 'app-custom-offer', + standalone: true, + imports: [ + TranslateModule, + ProdSpecComponent, + ReactiveFormsModule, + PricePlansComponent, + ProcurementModeComponent, + OfferSummaryComponent, + RelatedPartyIdComponent, + NgClass], + templateUrl: './custom-offer.component.html', + styleUrl: './custom-offer.component.css' +}) +export class CustomOfferComponent implements OnInit { + @Input() offer: any = {}; + @Input() partyId: any | undefined; + + productOfferForm: FormGroup; + currentStep = 0; + highestStep = 0; + steps = [ + 'Party Info', + 'Price Plans', + 'Procurement Mode', + 'Summary' + ]; + isFormValid = false; + pricePlans:any = []; + errorMessage:any=''; + showError:boolean=false; + loading:boolean=false; + loadingData:boolean=false; + bundleChecked:boolean=false; + offersBundle:any[]=[]; + + + offerToCreate:ProductOffering_Create | undefined; + + + constructor(private api: ApiServiceService, + private eventMessage: EventMessageService, + private fb: FormBuilder) { + + this.productOfferForm = this.fb.group({ + prodSpec: new FormControl(null, [Validators.required]), + partyInfo: new FormControl(null, [Validators.required]), + pricePlans: new FormControl([]), + procurementMode: this.fb.group({}) + }); + + } + + async ngOnInit() { + console.log('--------- OFFER DATA ----------') + console.log(this.offer) + console.log(this.partyId) + console.log('-------------------------------') + await this.loadOfferData(); + this.loadingData=false; + } + + async loadOfferData() { + console.log('Loading offer into form...', this.offer); + + if(this.offer.productOfferingTerm){ + console.log('Found productOfferingTerm:', this.offer.productOfferingTerm); + + //PROCUREMENT + const procurementTerm = this.offer.productOfferingTerm.find( + (element: { name: string; }) => element.name === 'procurement' + ); + if(procurementTerm){ + const procurementValue = { + id: procurementTerm.description, + name: procurementTerm.description + }; + console.log('Setting procurement value:', procurementValue); + this.productOfferForm.patchValue({ + procurementMode: procurementValue + }); + } else { + this.productOfferForm.patchValue({ + procurementMode: { + id: 'manual', + name: 'Manual' + } + }); + } + } + + // Product Specification + if (this.offer.productSpecification) { + await this.api.getProductSpecification(this.offer.productSpecification.id).then(async data => { + this.productOfferForm.patchValue({ + prodSpec: data || null // Cargar si existe, o dejar en null + }); + }) + } + + } + + async createOffer() { + this.loading=true; + const plans = this.productOfferForm.value.pricePlans; + + console.log('---- party info value ----') + console.log(this.productOfferForm.get('partyInfo')?.value) + console.log('---- full form value ----') + console.log(this.productOfferForm.value) + + if (plans.length === 0) { + this.saveOfferInfo(); + return; + } + + for (let i = 0; i < plans.length; i++) { + const plan = plans[i]; + const components = plan.priceComponents || []; + + try { + let createdPriceId: string; + + const compRel = await Promise.all( + components.map((comp: any) => this.createPriceComponent(comp, plan.currency)) + ); + const bundledPricePlan = this.createBundledPricePlan(plan, compRel); + const created = await lastValueFrom(this.api.postOfferingPrice(bundledPricePlan)); + createdPriceId = created.id; + + this.productOfferForm.value.pricePlans[i].id = createdPriceId; + + if (i === plans.length - 1) { + this.saveOfferInfo(); + } + } catch (error: any) { + this.handleApiError(error); + } + } + } + + saveOfferInfo(): void { + const formValue = this.productOfferForm.value; + + const prices = formValue.pricePlans.map((plan: any) => ({ + id: plan.id, + href: plan.id + })); + + const license = this.offer.productOfferingTerm.find((t: { name: string; }) => t.name === 'License'); + + const offer: any = { + name: this.offer.name, + description: this.offer?.description || '', + lifecycleStatus: 'Active', + isBundle: this.bundleChecked, + bundledProductOffering: this.offersBundle, + place: [], + version: this.offer.version, + category: this.offer.category, + productOfferingPrice: prices, + validFor: { + startDateTime: new Date().toISOString() + }, + relatedParty: [ + { + "role": environment.BUYER_ROLE, + "href": this.productOfferForm.get('partyInfo')?.value.id, + "id": this.productOfferForm.get('partyInfo')?.value.id + } + ], + productOfferingTerm: [ + { + name: 'License', + description: license?.description || '' + }, + { + name: 'procurement', + description: formValue.procurementMode.mode + } + ] + }; + + if (!this.bundleChecked) { + offer.productSpecification = this.offer.productSpecification; + } + + this.offerToCreate = offer; + console.log('---- Offer to create -----') + console.log(this.offerToCreate) + + const request$ = this.api.postProductOffering(offer, null) + + request$.subscribe({ + next: (data) => { + console.log('product offer created:'); + console.log(data); + this.loading=false; + this.goBack(); + }, + error: (error) => { + console.error('Error during offer save/update:', error); + this.errorMessage = error?.error?.error ? 'Error: ' + error.error.error : 'An error occurred while saving the offer!'; + this.loading=false; + this.showError = true; + setTimeout(() => (this.showError = false), 3000); + } + }); + } + + private async createPriceComponent(component: any, currency: string): Promise { + console.log('component format') + console.log(component) + let priceComp: ProductOfferingPrice = { + name: component.name, + description: component.description ?? component?.newValue.description, + lifecycleStatus: component?.lifecycleStatus ?? component?.newValue?.lifecycleStatus ?? 'Active', + priceType: component.priceType ?? component?.newValue?.priceType, + price: { unit: currency, value: component?.price ?? component?.newValue.price }, + recurringChargePeriodType: undefined, + recurringChargePeriodLength: undefined, + unitOfMeasure: undefined, + prodSpecCharValueUse: undefined + }; + + let priceType = component.priceType ?? component?.newValue?.priceType; + + if (['recurring', 'recurring-prepaid'].includes(priceType)) { + priceComp.recurringChargePeriodType = component.recurringPeriod; + priceComp.recurringChargePeriodLength = 1; + } + + if (priceType === 'usage') { + console.log(component.newValue) + priceComp.unitOfMeasure = { + amount: 1, + units: component.usageUnit ?? component.newValue.usageUnit + } + priceComp['@baseType'] = "ProductOfferingPrice"; + priceComp['@schemaLocation'] = "https://raw.githubusercontent.com/laraminones/tmf-new-schemas/main/UsageSpecId.json"; + (priceComp as any).usageSpecId = component.usageSpecId ?? component?.newValue?.usageSpecId; + + + console.log('-- here') + console.log(priceComp) + } + + if (component?.selectedCharacteristic || component?.newValue?.selectedCharacteristic) { + priceComp.prodSpecCharValueUse = component.selectedCharacteristic ?? component.newValue.selectedCharacteristic; + } + + if (component?.unitOfMeasure) { + priceComp.unitOfMeasure = component.usageUnit; + } + + if (component?.discountValue != null) { + const discount = await this.createPriceAlteration(component, currency); + priceComp.popRelationship = [{ id: discount.id, href: discount.id, name: discount.name }]; + } + console.log('create price comp') + console.log(priceComp) + const created = await lastValueFrom(this.api.postOfferingPrice(priceComp)); + return { id: created.id, href: created.id, name: created.name }; + } + + private async createPriceAlteration(component: any, currency: string): Promise { + const priceAlter: ProductOfferingPrice = { + name: 'discount', + priceType: 'discount', + validFor: { + startDateTime: moment().toISOString(), + endDateTime: moment().add(Number(component.discountDuration), component.discountDurationUnit).toISOString() + }, + unitOfMeasure: { + amount: component.discountDuration, + units: component.discountDurationUnit + } + }; + + if (component.discountUnit === 'percentage') { + priceAlter.percentage = component.discountValue; + } else { + priceAlter.price = { value: component.discountValue, unit: currency }; + } + + return await lastValueFrom(this.api.postOfferingPrice(priceAlter)); + } + + private createBundledPricePlan(plan: any, compRel: any[]): ProductOfferingPrice { + const price: ProductOfferingPrice = { + name: plan.name ?? plan?.newValue?.name, + isBundle: true, + description: plan.description ?? plan?.newValue?.description, + lifecycleStatus: plan.lifecycleStatus ?? plan?.newValue?.lifecycleStatus, + bundledPopRelationship: compRel + }; + + if(plan?.priceType){ + if(plan?.priceType == 'custom'){ + price.priceType='custom' + } + } + + if (plan.prodSpecCharValueUse) { + price.prodSpecCharValueUse = plan.prodSpecCharValueUse.map((item: any) => ({ + ...item, + productSpecCharacteristicValue: item.productSpecCharacteristicValue + .filter((v: any) => v.isDefault) + })); + } + + + + if(plan?.newValue?.prodSpecCharValueUse){ + price.prodSpecCharValueUse = plan?.newValue?.prodSpecCharValueUse.map((item: any) => ({ + ...item, + productSpecCharacteristicValue: item.productSpecCharacteristicValue + .filter((v: any) => v.isDefault) + })); + } + + console.log(price.prodSpecCharValueUse) + + if (plan.usageUnit) { + price.unitOfMeasure = plan.usageUnit; + } + + if(plan?.newValue?.usageUnit){ + price.unitOfMeasure = plan?.newValue?.usageUnit; + } + + return price; + } + + goToStep(index: number) { + // Validar el paso actual + console.log('click go to step') + const currentStepValid = this.validateCurrentStep(); + console.log(!currentStepValid) + if (!currentStepValid) { + return; // No permitir avanzar si el paso actual no es válido + } + this.currentStep = index; + if(this.currentStep>this.highestStep){ + this.highestStep=this.currentStep + } + } + + goBack() { + this.eventMessage.emitSellerOffer(true); + } + + private handleApiError(error: any): void { + console.error('Error while creating offer price!', error); + this.errorMessage = error?.error?.error ? 'Error: ' + error.error.error : 'Error creating offer price!'; + this.showError = true; + setTimeout(() => (this.showError = false), 3000); + } + + validateCurrentStep(): boolean { + console.log('--- party') + console.log(this.productOfferForm.get('partyInfo')?.value) + switch (this.currentStep) { + case 0: // Party Info + const value = this.productOfferForm.get('partyInfo')?.value; + return value && Object.keys(value).length != 0; + case 1: // Price Plans + return true; + case 2: // Procurement Mode + return this.productOfferForm.get('procurementMode')?.valid || false; + default: + return true; + } + } + + canNavigate(index: number) { + return (this.productOfferForm.get('partyInfo')?.valid && (index <= this.currentStep)) || (this.productOfferForm.get('partyInfo')?.valid && (index <= this.highestStep)); + } + + handleStepClick(index: number): void { + if (this.canNavigate(index)) { + this.goToStep(index); + } + } + +} diff --git a/src/app/shared/forms/offer/offer.component.html b/src/app/shared/forms/offer/offer.component.html index c9e032f4..7f253113 100644 --- a/src/app/shared/forms/offer/offer.component.html +++ b/src/app/shared/forms/offer/offer.component.html @@ -95,6 +95,7 @@

}@else{ @@ -114,6 +115,7 @@

} diff --git a/src/app/shared/forms/offer/price-plans/price-plans.component.html b/src/app/shared/forms/offer/price-plans/price-plans.component.html index 9d0a5b02..64ea033d 100644 --- a/src/app/shared/forms/offer/price-plans/price-plans.component.html +++ b/src/app/shared/forms/offer/price-plans/price-plans.component.html @@ -15,24 +15,27 @@

{{ 'FORMS.P } --> @if (pricePlans.length == 0) { - - - } @else { -