Une erreur s'est produite lors du traitement du modèle.
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					        "Smart Water Solutions":"Portfolio Type" 
133								}; 
134								 
135								const level0CategoryName = level0Hash[productCategoryTitle]; 
136								document.getElementById('level0CategoryCrumb').innerHTML = level0CategoryName; 
137
138						}); 
139						 
140					} else { 
141						console.log('No level1Categories found in the response.'); 
142
143				}) 
144				.catch(error => { 
145					console.error('Error:', error); 
146				}); 
147				 
148			}); 
149 
150    } else { 
151      console.log('No productCategoryItems found in the response.'); 
152
153  }) 
154  .catch(error => { 
155    console.error('Error:', error); 
156  }); 
157	fetch('/o/headless-commerce-delivery-catalog/v1.0/channels/${channelId}/products/${cpProductId}/attachments?accountId=0&pageSize=50') 
158  .then(response => response.json()) 
159  .then(data => { 
160		const attachmentsMap = new Map(); 
161		const youTubeMap = new Map(); 
162    const productAttachmentItems = data.items.sort((a,b) => (a.title > b.title) ? 1 : ((b.title > a.title) ? -1 : 0)); 
163    if (productAttachmentItems && productAttachmentItems.length > 0) { 
164			productAttachmentItems.forEach(function (productAttachmentItem) { 
165      	const productAttachmentURL = productAttachmentItem.src.split("?")[0]; 
166				const productAttachmentURLStripped = productAttachmentURL.replace("https://itronportal.lxc.liferay.com",""); 
167      	const productAttachmentTitle = productAttachmentItem.title; 
168				const productAttachmentCustomFields = productAttachmentItem.customFields 
169				var productAttachmentContentType = "Article" 
170				var displayAttachment = true; 
171				productAttachmentCustomFields.forEach(function (productAttachmentCustomField) { 
172					const productAttachmentCustomFieldName = productAttachmentCustomField.name 
173					if(productAttachmentCustomFieldName == "ContentType"){ 
174						productAttachmentContentType = productAttachmentCustomField.customValue.data; 
175
176					if(productAttachmentCustomFieldName == "LanguageKey"){ 
177						const productAttachmentLanguageKey = productAttachmentCustomField.customValue.data; 
178						if((productAttachmentLanguageKey != null || productAttachmentLanguageKey != "") && productAttachmentLanguageKey != 'en_US'){ 
179								displayAttachment = false; 
180								if(productAttachmentLanguageKey === '${locale}'){ 
181									displayAttachment = true; 
182
183
184
185				}); 
186 
187						if(productAttachmentContentType == "YouTube Video"){ 
188							const productAttachmentFileEntryId = productAttachmentItem.fileEntryId; 
189							fetch('/o/headless-delivery/v1.0/documents/' + productAttachmentFileEntryId) 
190							.then(response => response.json()) 
191							.then(data => { 
192								const contentCustomFields = data.customFields; 
193								if (contentCustomFields) { 
194									let youTubeURL = ""; 
195									let youTubeCode = ""; 
196									let videoTitle = ""; 
197									contentCustomFields.forEach(function (contentCustomField) { 
198										const contentName = contentCustomField.name 
199										 
200										if(contentName == "VideoUrl"){ 
201											youTubeURL = contentCustomField.customValue.data; 
202											const splitURL = youTubeURL.split("/"); 
203											const splitSize = splitURL.length; 
204											youTubeCode = splitURL[splitSize-1]; 
205
206										if(contentName == "VideoTitle"){ 
207											videoTitle = contentCustomField.customValue.data; 
208
209									}); 
210									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>'; 
211								  let existingVideoList = document.getElementById('videoListSection').innerHTML; 
212									 
213									document.getElementById('videoListSection').innerHTML = existingVideoList + videoListItem; 
214									showHideVideoSection(); 
215								} else { 
216									console.log('No contentCustomFields found in the response.'); 
217
218							}) 
219							.catch(error => { 
220								console.error('Error:', error); 
221							}); 
222						} else { 
223							if(displayAttachment){ 
224								const listItem = "<li><a target='_blank' href="+ productAttachmentURLStripped+" class='list-link'>"+productAttachmentTitle+"</a></li>"; 
225								let mapValue = attachmentsMap.get(productAttachmentContentType); 
226								let newMapValue = ""; 
227								if(mapValue){ 
228									let newMapValue = mapValue + listItem; 
229									attachmentsMap.set(productAttachmentContentType, newMapValue); 
230								} else { 
231									attachmentsMap.set(productAttachmentContentType, listItem); 
232
233
234
235			}); 
236 
237			let totalList = ''; 
238			let sortedMap = new Map([...attachmentsMap.entries()].sort()); 
239			for (const [key, value] of sortedMap.entries()) { 
240				let ulist = '<h2 class="list-title">' + key + '</h2><ul class="list">' + value + '</ul>' 
241				totalList = totalList + ulist; 
242
243			 
244			document.getElementById('resourcesDiv').innerHTML = totalList; 
245    } else { 
246      console.log('No attachments for selected product.'); 
247
248  }) 
249  .catch(error => { 
250    console.error('Error:', error); 
251  }); 
252		 
253		 
254fetch(`/o/headless-commerce-delivery-catalog/v1.0/channels/${channelId}/products/${cpProductId}/related-products`) 
255  .then(response => response.json()) 
256  .then(data => { 
257    let relatedList = '<ul class="product-catalog-list-detail">'; 
258    const relatedProductItems = data.items; 
259    const totalCount = data.totalCount; 
260    let fetchedProducts = []; // Array to store products with names 
261    let fetchCount = 0; // Counter for completed fetch requests 
262 
263    if (relatedProductItems && relatedProductItems.length > 0) { 
264      relatedProductItems.forEach((relatedProductItem) => { 
265        const relatedProductId = relatedProductItem.productId; 
266 
267        fetch('/o/headless-commerce-delivery-catalog/v1.0/channels/${channelId}/products/' + relatedProductId) 
268          .then((response) => response.json()) 
269          .then((productData) => { 
270            if (productData) { 
271              const relatedProductName = productData.name; 
272              const relatedProductImageURL = productData.urlImage; 
273              const relatedProductFriendlyURL = productData.urls['en_US'] || ''; 
274 
275              // Push product details to the fetchedProducts array 
276              fetchedProducts.push({ 
277                name: relatedProductName, 
278                imageURL: relatedProductImageURL, 
279                friendlyURL: relatedProductFriendlyURL, 
280              }); 
281
282          }) 
283          .catch((error) => { 
284            console.error("Error fetching product details:", error); 
285          }) 
286          .finally(() => { 
287            fetchCount++; 
288            if (fetchCount === relatedProductItems.length) { 
289              // Sort products by name after all fetches are completed 
290              fetchedProducts.sort((a, b) => a.name.localeCompare(b.name)); 
291 
292              // Generate the list items 
293              fetchedProducts.forEach((product) => { 
294                if (product.name) { 
295                  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>'; 
296
297              }); 
298 
299              // Close the list and update the DOM 
300              relatedList += '</ul>'; 
301              document.getElementById('relatedProductDiv').innerHTML = relatedList; 
302              showHideRelatedProductsSection(); 
303
304          }); 
305      }); 
306    } else { 
307      console.log("No relatedProductItems found in the response."); 
308
309  }) 
310  .catch((error) => { 
311    console.error("Error fetching related products:", error); 
312  }); 
313 
314 
315	</script> 
Region Selector Select a region and country for the best experience.