Se ha producido un error al procesar la plantilla.
The following has evaluated to null or missing:
==> cpProductId  [in template "44616#44647#3234203" at line 93, column 92]

----
Tip: If the failing expression is known to legally refer to something that's sometimes null or missing, either specify a default value like myOptionalVar!myDefault, or use <#if myOptionalVar??>when-present<#else>when-missing</#if>. (These only cover the last step of the expression; to cover the whole expression, use parenthesis: (myOptionalVar.foo)!myDefault, (myOptionalVar.foo)??
----

----
FTL stack trace ("~" means nesting-related):
	- Failed at: ${cpProductId}  [in template "44616#44647#3234203" at line 93, column 90]
----
1<#assign 
2commerceContext = renderRequest.getAttribute("COMMERCE_CONTEXT") 
3channelId = commerceContext.getCommerceChannelId() 
4/> 
5<#if cpCatalogEntry?has_content> 
6	<#assign 
7	  cpDefinitionId = cpCatalogEntry.getCPDefinitionId() 
8		cpProductId = cpCatalogEntry.getCProductId() 
9	  productName = cpCatalogEntry.getName() 
10	  productShortDescription = cpCatalogEntry.getShortDescription() 
11	  productDescription = cpCatalogEntry.getDescription() 
12	  productImages = cpContentHelper.getImages(cpDefinitionId, false, themeDisplay) 
13		productImages = productImages?reverse 
14		cpAttachmentFileEntries = cpContentHelper.getCPMedias(cpDefinitionId, themeDisplay) 
15	/> 
16 
17		<nav class="breadcrumb-navigation" aria-label="breadcrumb"> 
18			<ol class="breadcrumb-list"> 
19				<li class="breadcrumb-item"><a id="level0CategoryCrumb" href='${languageUtil.get(locale, "what-we-offer-products-url")}' class="breadcrumb-link"></a></li> 
20 
21				<li  class="breadcrumb-item"><a id="level1CategoryCrumb" href="#" class="breadcrumb-link"></a></li> 
22				<li class="breadcrumb-item" aria-current="page">${productName}</li> 
23			</ol> 
24		</nav>	 
25	 
26		<div class="category-detail"> 
27			<h1 class="category-title">${productName}</h1> 
28			<p class="category-description">${productShortDescription}</p>         
29		</div>	 
30	 
31		<div class="product-catalog-detail-section"> 
32				<div class="accordion tabs"> 
33					<h2 class="toggle">Product Detail</h2> 
34					<div class="content" tabindex="0"> 
35						<div class="product-detail-content row align-items-lg-start align-items-sm-start align-items-start align-items-md-start flex-lg-row flex-sm-row flex-row flex-md-row"> 
36							<div class="product-detail-slider-wrap col col-lg-5 col-sm-12 col-12 col-md-5"> 
37								<span class="product-detail-image-paging-info visually-hidden" role="status"></span> 
38								<div class="product-detail-slider"> 
39								 <#if productImages?has_content> 
40									<#list productImages as currentImage> 
41										<li class="product-detail-slider-slide"> 
42											 
43											<img class="product-detail-slider-slide-img" src="${currentImage.getURL()}" width="428" height="428" alt="${currentImage.getTitle()}" /> 
44										</li>	 
45									</#list> 
46								 </#if>	   
47								</div> 
48								<div class="product-detail-slider-nav"> 
49								 <#if productImages?has_content> 
50									<#list productImages as currentImage> 
51										<li role="button" class="product-detail-slider-nav-btn"> 
52											<img class="product-detail-slider-slide-img" src="${currentImage.getURL()}" width="65" height="65" alt="${currentImage.getTitle()}" /> 
53										</li>	 
54									</#list> 
55								 </#if>	   
56								 </div> 
57							</div> 
58							<div class="product-detail-content-desc col col-lg-7 col-sm-12 col-12 col-md-7"> 
59								<p>${productDescription}</p> 
60								<a class="btn btn-primary" href="/contact">Contact Sales</a> 
61							</div>	    
62						</div> 
63					</div> 
64 
65						<#assign contentTypeMap = {}/> 
66						<#assign youtubeVideoMap = []/> 
67	 
68					<#if cpAttachmentFileEntries?has_content> 
69						<h2 class="toggle">Resources</h2> 
70						<div  class="content" tabindex="0"> 
71							<div id="resourcesDiv" > 
72							</div> 
73						</div> 
74					</#if>	 
75						<h2 class="toggle">Related Products</h2> 
76						<div class="content" tabindex="0"> 
77							<div id="relatedProductDiv"> 
78	 
79							</div> 
80					 </div> 
81 
82							<h2 id="videoTitle" class="toggle">Videos</h2> 
83							<div class="content" tabindex="0"> 
84								<div id="videoDiv" > 
85									<ul id="videoListSection" class="product-catalog-list-detail"></ul>		 
86								</div> 
87							</div> 		 
88				</div>  
89		</div>	 
90</#if> 
91	<script> 
92 
93	fetch('/o/headless-commerce-delivery-catalog/v1.0/channels/${channelId}/products/${cpProductId}/categories') 
94  .then(response => response.json()) 
95  .then(data => { 
96		const productCategoryItems = data.items; 
97    if (productCategoryItems && productCategoryItems.length > 0) { 
98			productCategoryItems.forEach(function (productCategoryItem) { 
99				const categoryId = productCategoryItem.id 
100				fetch('/o/c/levelonecategories/?fields=categoryLiferayId,categoryName,categoryURL&filter=categoryLiferayId eq '+categoryId + '&p_auth='+ Liferay.authToken) 
101				.then(response => response.json()) 
102				.then(data => { 
103					const level1Categories = data.items; 
104					if (level1Categories && level1Categories.length > 0) { 
105						level1Categories.forEach(function (level1Category) { 
106							if(level1Category){ 
107								const productCategoryTitle = level1Category.categoryName; 
108								const productCategoryURL = 	level1Category.categoryURL; 
109								const productCategoryFinalURL = 	"/categories/" + productCategoryURL; 
110								document.getElementById('level1CategoryCrumb').setAttribute('href', productCategoryFinalURL); 
111								document.getElementById('level1CategoryCrumb').innerHTML = productCategoryTitle; 
112 
113								const level0Hash = {  
114									"Electricity Meters + Modules":"Measurement + Sensing", 
115									"Gas Meters + Modules":"Measurement + Sensing", 
116									"Sensing + Control":"Measurement + Sensing", 
117									"Thermal Energy Meters + Modules":"Measurement + Sensing", 
118									"Water Meters + Modules":"Measurement + Sensing", 
119									"Grid Management":"Networks + Operations", 
120									"Mobile Meter Reading":"Networks + Operations", 
121									"Network Infrastructure + Management ":"Networks + Operations", 
122									"Operations Management":"Networks + Operations", 
123									"Smart Cities":"Networks + Operations", 
124									"Distributed Energy Management":"Software + Services", 
125									"Energy Forecasting":"Software + Services", 
126									"Meter Data Management + Analytics":"Software + Services", 
127									"Prepayment":"Software + Services", 
128									"Services":"Software + Services", 
129					        "CityEdge":"Portfolio Type", 
130					        "Gas Edge":"Portfolio Type", 
131					        "Grid Edge Intelligence":"Portfolio Type", 
132									"Resiliency Solutions":"Portfolio Type", 
133									"Emergency Preparedness + Response":"Software + Services", 
134									"Damage Prevention":"Software + Services", 
135									"Worker Safety":"Software + Services", 
136					        "Smart Water Solutions":"Portfolio Type" 
137								}; 
138								 
139								const level0CategoryName = level0Hash[productCategoryTitle]; 
140								document.getElementById('level0CategoryCrumb').innerHTML = level0CategoryName; 
141
142						}); 
143						 
144					} else { 
145						console.log('No level1Categories found in the response.'); 
146
147				}) 
148				.catch(error => { 
149					console.error('Error:', error); 
150				}); 
151				 
152			}); 
153 
154    } else { 
155      console.log('No productCategoryItems found in the response.'); 
156
157  }) 
158  .catch(error => { 
159    console.error('Error:', error); 
160  }); 
161	fetch('/o/headless-commerce-delivery-catalog/v1.0/channels/${channelId}/products/${cpProductId}/attachments?accountId=0&pageSize=50') 
162  .then(response => response.json()) 
163  .then(data => { 
164		const attachmentsMap = new Map(); 
165		const youTubeMap = new Map(); 
166    const productAttachmentItems = data.items.sort((a,b) => (a.title > b.title) ? 1 : ((b.title > a.title) ? -1 : 0)); 
167    if (productAttachmentItems && productAttachmentItems.length > 0) { 
168			productAttachmentItems.forEach(function (productAttachmentItem) { 
169      	const productAttachmentURL = productAttachmentItem.src.split("?")[0]; 
170				const productAttachmentURLStripped = productAttachmentURL.replace("https://itronportal.lxc.liferay.com",""); 
171      	const productAttachmentTitle = productAttachmentItem.title; 
172				const productAttachmentCustomFields = productAttachmentItem.customFields 
173				var productAttachmentContentType = "Article" 
174				var displayAttachment = true; 
175				productAttachmentCustomFields.forEach(function (productAttachmentCustomField) { 
176					const productAttachmentCustomFieldName = productAttachmentCustomField.name 
177					if(productAttachmentCustomFieldName == "ContentType"){ 
178						productAttachmentContentType = productAttachmentCustomField.customValue.data; 
179
180					if(productAttachmentCustomFieldName == "LanguageKey"){ 
181						const productAttachmentLanguageKey = productAttachmentCustomField.customValue.data; 
182						if((productAttachmentLanguageKey != null || productAttachmentLanguageKey != "") && productAttachmentLanguageKey != 'en_US'){ 
183								displayAttachment = false; 
184								if(productAttachmentLanguageKey === '${locale}'){ 
185									displayAttachment = true; 
186
187
188
189				}); 
190 
191						if(productAttachmentContentType == "YouTube Video"){ 
192							const productAttachmentFileEntryId = productAttachmentItem.fileEntryId; 
193							fetch('/o/headless-delivery/v1.0/documents/' + productAttachmentFileEntryId) 
194							.then(response => response.json()) 
195							.then(data => { 
196								const contentCustomFields = data.customFields; 
197								if (contentCustomFields) { 
198									let youTubeURL = ""; 
199									let youTubeCode = ""; 
200									let videoTitle = ""; 
201									contentCustomFields.forEach(function (contentCustomField) { 
202										const contentName = contentCustomField.name 
203										 
204										if(contentName == "VideoUrl"){ 
205											youTubeURL = contentCustomField.customValue.data; 
206											const splitURL = youTubeURL.split("/"); 
207											const splitSize = splitURL.length; 
208											youTubeCode = splitURL[splitSize-1]; 
209
210										if(contentName == "VideoTitle"){ 
211											videoTitle = contentCustomField.customValue.data; 
212
213									}); 
214									let videoListItem = '<li class="product-catalog-list-item-detail"><button class="btn btn-link youtube-modal-link global-youtube-modal-trigger" data-bs-toggle="modal" data-bs-target="#global-youtube-modal" data-modal-url="'+youTubeCode+'" data-modal-title="Itron Optimizer Portfolio Overview"><div class="card-inner"><div class="card-image-div"><img width="279" height="157" loading="lazy" alt="" src="//img.youtube.com/vi/'+youTubeCode+'/maxresdefault.jpg" /><span class="modal-video-btn"><span class="visually-hidden">Play Video</span></span></div><div class="card-body"><h2 class="card-title">'+videoTitle+'</h2></div></div></button></li>'; 
215								  let existingVideoList = document.getElementById('videoListSection').innerHTML; 
216									 
217									document.getElementById('videoListSection').innerHTML = existingVideoList + videoListItem; 
218									showHideVideoSection(); 
219								} else { 
220									console.log('No contentCustomFields found in the response.'); 
221
222							}) 
223							.catch(error => { 
224								console.error('Error:', error); 
225							}); 
226						} else { 
227							if(displayAttachment){ 
228								const listItem = "<li><a target='_blank' href="+ productAttachmentURLStripped+" class='list-link'>"+productAttachmentTitle+"</a></li>"; 
229								let mapValue = attachmentsMap.get(productAttachmentContentType); 
230								let newMapValue = ""; 
231								if(mapValue){ 
232									let newMapValue = mapValue + listItem; 
233									attachmentsMap.set(productAttachmentContentType, newMapValue); 
234								} else { 
235									attachmentsMap.set(productAttachmentContentType, listItem); 
236
237
238
239			}); 
240 
241			let totalList = ''; 
242			let sortedMap = new Map([...attachmentsMap.entries()].sort()); 
243			for (const [key, value] of sortedMap.entries()) { 
244				let ulist = '<h2 class="list-title">' + key + '</h2><ul class="list">' + value + '</ul>' 
245				totalList = totalList + ulist; 
246
247			 
248			document.getElementById('resourcesDiv').innerHTML = totalList; 
249    } else { 
250      console.log('No attachments for selected product.'); 
251
252  }) 
253  .catch(error => { 
254    console.error('Error:', error); 
255  }); 
256		 
257		 
258fetch(`/o/headless-commerce-delivery-catalog/v1.0/channels/${channelId}/products/${cpProductId}/related-products`) 
259  .then(response => response.json()) 
260  .then(data => { 
261    let relatedList = '<ul class="product-catalog-list-detail">'; 
262    const relatedProductItems = data.items; 
263    const totalCount = data.totalCount; 
264    let fetchedProducts = []; // Array to store products with names 
265    let fetchCount = 0; // Counter for completed fetch requests 
266 
267    if (relatedProductItems && relatedProductItems.length > 0) { 
268      relatedProductItems.forEach((relatedProductItem) => { 
269        const relatedProductId = relatedProductItem.productId; 
270 
271        fetch('/o/headless-commerce-delivery-catalog/v1.0/channels/${channelId}/products/' + relatedProductId) 
272          .then((response) => response.json()) 
273          .then((productData) => { 
274            if (productData) { 
275              const relatedProductName = productData.name; 
276              const relatedProductImageURL = productData.urlImage; 
277              const relatedProductFriendlyURL = productData.urls['en_US'] || ''; 
278 
279              // Push product details to the fetchedProducts array 
280              fetchedProducts.push({ 
281                name: relatedProductName, 
282                imageURL: relatedProductImageURL, 
283                friendlyURL: relatedProductFriendlyURL, 
284              }); 
285
286          }) 
287          .catch((error) => { 
288            console.error("Error fetching product details:", error); 
289          }) 
290          .finally(() => { 
291            fetchCount++; 
292            if (fetchCount === relatedProductItems.length) { 
293              // Sort products by name after all fetches are completed 
294              fetchedProducts.sort((a, b) => a.name.localeCompare(b.name)); 
295 
296              // Generate the list items 
297              fetchedProducts.forEach((product) => { 
298                if (product.name) { 
299                  relatedList += '<li class="product-catalog-list-item-detail"><a class="product-catalog-list-item-link" href="/products/' + product.friendlyURL + '"><div class="card-inner"><div class="card-image-div"><img class="product-catalog-list-item-img" width="227" height="227" loading="lazy" src="'+product.imageURL+'"alt="'+product.name+'" /></div><div class="card-body"><h2 class="card-title card-title__xl">' +product.name + '</h2></div></div></a></li>'; 
300
301              }); 
302 
303              // Close the list and update the DOM 
304              relatedList += '</ul>'; 
305              document.getElementById('relatedProductDiv').innerHTML = relatedList; 
306              showHideRelatedProductsSection(); 
307
308          }); 
309      }); 
310    } else { 
311      console.log("No relatedProductItems found in the response."); 
312
313  }) 
314  .catch((error) => { 
315    console.error("Error fetching related products:", error); 
316  }); 
317 
318 
319	</script>