Filtrar:
Se ha producido un error al procesar la plantilla.
The following has evaluated to null or missing:
==> getProduct(channelId, entry.getTitle()?trim) [in template "34352066712900#33336#65792029" at line 32, column 28]
----
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: #assign product = getProduct(channelI... [in template "34352066712900#33336#65792029" at line 32, column 9]
----
1<#setting url_escaping_charset="UTF-8">
2
3<#-- ===== Idiomas: helpers y datos al inicio ===== -->
4<#function getListTypeEntriesByERC erc>
5 <#attempt>
6 <#return restClient.get(
7 "/headless-admin-list-type/v1.0/list-type-definitions/by-external-reference-code/${erc}/list-type-entries?fields=key,name&sort"
8 ).items>
9 <#recover>
10 <#return []>
11 </#attempt>
12</#function>
13
14<#-- Siempre lista, nunca null -->
15<#assign LANG_ENTRIES = (getListTypeEntriesByERC("IDIOMAS_NORMAS_PICKLIST")?default([]))![]>
16
17<#function langLiteral key>
18 <#assign k = (key!"")?upper_case>
19 <#list LANG_ENTRIES as e>
20 <#if (e.key!"")?upper_case == k>
21 <#return e.name!"">
22 </#if>
23 </#list>
24 <#return key!"" >
25</#function>
26
27<section class="list-standards-section">
28 <div class="standards-container view-grid">
29 <#if entries?has_content>
30 <#assign channelId = getChannelId()>
31 <#list entries as entry>
32 <#assign product = getProduct(channelId, entry.getTitle()?trim)>
33 <#assign productId = product.productId />
34 <#assign cpDefinitionId = product.id />
35 <#assign productERC = product.externalReferenceCode />
36 <#assign categories = getProductCategories(channelId, product.productId)![]>
37 <#assign specs = getProductSpecifications(channelId, product.productId)![]>
38 <#assign status = getStatus(categories)!"" >
39 <#assign organism = getOrganism(categories)!"" >
40 <#assign currentStateDate = getCurrentStateDate(specs)!"" >
41 <#assign entryUrl = "">
42 <#attempt>
43 <#-- si el entry trae slug -->
44 <#assign entryUrl = "/p/${entry.getUrl()}">
45 <#recover>
46 <#-- si no, usa viewURL o el slug del product -->
47 <#assign entryUrl = (entry.getViewURL()!"")>
48 <#if !entryUrl?has_content && (product.slug?? && product.slug?has_content)>
49 <#assign entryUrl = "/p/${product.slug}">
50 </#if>
51 <#if !entryUrl?has_content>
52 <#assign entryUrl = "#">
53 </#if>
54 </#attempt>
55
56 <#-- Detalle de normas (idioma, formato, precio) -->
57 <#assign normasDetails = getNormasDetails(product.id)![]>
58
59 <#-- idiomas únicos -->
60 <#assign languages = []>
61 <#list normasDetails as n>
62 <#if n.language?? && n.language?has_content && !languages?seq_contains(n.language)>
63 <#assign languages = languages + [n.language]>
64 </#if>
65 </#list>
66
67 <#-- combinación por defecto -->
68 <#assign defaultLang = "">
69 <#assign defaultFmt = "">
70 <#assign defaultPrice = 0>
71 <#if normasDetails?has_content>
72 <#assign defaultLang = (normasDetails[0].language!"")>
73 <#assign defaultFmt = (normasDetails[0].format!"")>
74 <#assign defaultPrice = (normasDetails[0].price!'0')?number>
75 </#if>
76
77 <#-- formatos válidos para el idioma por defecto -->
78 <#assign formatsForDefault = []>
79 <#list normasDetails?filter(nd -> (nd.language!"") == defaultLang) as nd>
80 <#if nd.format?? && nd.format?has_content && !formatsForDefault?seq_contains(nd.format)>
81 <#assign formatsForDefault = formatsForDefault + [nd.format]>
82 </#if>
83 </#list>
84 <#if formatsForDefault?has_content && !formatsForDefault?seq_contains(defaultFmt)>
85 <#assign defaultFmt = formatsForDefault[0]>
86 </#if>
87
88 <#-- precio real para la combinación por defecto -->
89 <#list normasDetails as nd>
90 <#if (nd.language!"") == defaultLang && (nd.format!"") == defaultFmt>
91 <#assign defaultPrice = (nd.price!'0')?number>
92 </#if>
93 </#list>
94
95 <#assign hasOptions = (languages?has_content && formatsForDefault?has_content && (defaultLang?has_content) && (defaultFmt?has_content))>
96
97 <div class="item-result-buscador item-standard"
98 data-cpDefinitionId="${cpDefinitionId}" data-productId="${productId}" data-erc="${productERC}"
99 data-entrytype="Norma"
100 data-code="${(product.externalReferenceCode!entry.getTitle())?html}"
101 data-title="${product.name?html}">
102 <#if organism?has_content>
103 <div class="tag-standard">${organism}</div>
104 <#else>
105 <div class="tag-standard">TAG</div>
106 </#if>
107
108 <div class="info-standard">
109 <a href=${entryUrl?keep_before("?")} data-senna-off="true"><h3 class="title-standard">${product.name}</h3></a>
110
111 <div class="status-box">
112 <#--
113 //TODO: usar getStatusInfo
114 //TODO: OK
115 -->
116 <#assign statusInfo = getStatusInfo(status, "badge status-standard")>
117 <#if status?? && status?has_content>
118 <div class="${statusInfo.tagClass}">${statusInfo.status}</div>
119
120 <#--
121 <#assign statusTagClass = ''>
122
123 <#if status?trim?upper_case == 'EN VIGOR'>
124 <#assign statusTagClass = 'tag-success'>
125 <#elseif status?trim?upper_case == 'ANULADA'>
126 <#assign statusTagClass = 'tag-danger'>
127 <#elseif status?trim?upper_case == 'PROYECTO'>
128 <#assign statusTagClass = 'tag-blue'>
129 </#if>
130
131 <#if statusTagClass?? && statusTagClass?has_content>
132 <#assign statusTagClass = "status-standard " + statusTagClass>
133 </#if>
134
135 <div class="badge ${statusTagClass}">${status}</div>
136 -->
137 </#if>
138
139 <span class="date-standard">
140 <#if currentStateDate?has_content>
141 ${currentStateDate?date.iso?string('yyyy-MM-dd')}
142 <#else>
143 -
144 </#if>
145 </span>
146 </div>
147
148 <div class="description-text">
149 ${product.description}
150 </div>
151
152 <div class="price-container">
153 <span class="price price-ae"></span>
154 </div>
155
156 <#if hasOptions>
157 <div data-productid="${productId}" data-erc="${productERC}" class="options-standard selector-language_format">
158 <select class="form-control select-language">
159 <#list languages as l>
160 <option value="${l?html}" <#if l == defaultLang>selected</#if>>
161 ${langLiteral(l)?html}
162 </option>
163 </#list>
164 </select>
165 <select class="form-control select-format">
166 <#list formatsForDefault as f>
167 <option value="${f?html}" <#if f == defaultFmt>selected</#if>>
168 ${f?html}
169 </option>
170 </#list>
171 </select>
172 </div>
173 <#else>
174 <div class="standard-not-available">
175 ${languageUtil.get(locale, "ecom-get_norma_mail")}
176 </div>
177 </#if>
178
179 <#if hasOptions>
180 <button
181 class="standard-button"
182 aria-disabled="false"
183 disabled>
184 ${languageUtil.get(locale, "ecom-add_cart")}
185 </button>
186 </#if>
187 </div>
188
189 <#-- Datos para JS -->
190 <#if hasOptions>
191 <script type="application/json" class="normas-data">[
192 <#list normasDetails as n>
193 <#-- solo pares válidos -->
194 <#if (n.language?? && n.language?has_content) && (n.format?? && n.format?has_content)>
195 {
196 "format": "${(n.format!"")?js_string}",
197 "language": "${(n.language!"")?js_string}",
198 "price": ${(n.price!'0')?c}
199 }<#if n_has_next>,</#if>
200 </#if>
201 </#list>
202 ]</script>
203 <script type="application/json" class="lang-map">{
204 <#list languages as l>
205 "${(l!"")?upper_case?js_string}": "${langLiteral(l)?js_string}"<#if l_has_next>,</#if>
206 </#list>
207 }</script>
208 </#if>
209 </div>
210 </#list>
211 <#else>
212 <div class="search-results-add-custom-empty-message"></div>
213 </#if>
214 </div>
215</section>
216
217<#-- ===== Helpers REST ===== -->
218<#function getChannelId>
219 <#return restClient.get("/headless-commerce-delivery-catalog/v1.0/channels?filter=name eq 'Aenor Tienda'&sort").items[0].id>
220</#function>
221
222<#function getProduct channelId name>
223 <#return restClient.get("/headless-commerce-delivery-catalog/v1.0/channels/${channelId}/products?filter=name eq '${name?url}'&sort").items[0]>
224</#function>
225
226<#function getProductCategories channelId productId>
227 <#return restClient.get("/headless-commerce-delivery-catalog/v1.0/channels/${channelId}/products/${productId}/categories?sort").items>
228</#function>
229
230<#function getProductSpecifications channelId productId>
231 <#return restClient.get("/headless-commerce-delivery-catalog/v1.0/channels/${channelId}/products/${productId}/product-specifications?sort").items>
232</#function>
233
234<#function getCurrentStateDate specifications>
235 <#list specifications as specification>
236 <#if specification.specificationKey == 'current-state-date'>
237 <#return specification.value>
238 </#if>
239 </#list>
240</#function>
241
242<#function getStatus categories>
243 <#list categories as category>
244 <#if category.vocabulary == 'status'>
245 <#return category.title>
246 </#if>
247 </#list>
248</#function>
249
250<#function getOrganism categories>
251 <#list categories as category>
252 <#if category.vocabulary == 'organismos'>
253 <#return category.name>
254 </#if>
255 </#list>
256</#function>
257
258<#function getNormasDetails productId>
259 <#return restClient.get("/c/standards/?filter=r_standards_CPDefinitionId eq '${productId}'").items>
260</#function>
261
262<#function getStatusInfo status class="">
263 <#local defaultResult = {
264 "status": status!"",
265 "tagClass": "",
266 "isInForce": false,
267 "isCancelled": false,
268 "isProject": false
269 }>
270
271 <#local trimmedStatus = (status!"")?trim>
272 <#if trimmedStatus == "">
273 <#return defaultResult>
274 </#if>
275
276 <#local normalizedStatus = trimmedStatus?upper_case>
277 <#local tagClass = "">
278 <#if class?has_content>
279 <#local tagClass = class>
280 </#if>
281
282 <#local isInForce = false>
283 <#local isCancelled = false>
284 <#local isProject = false>
285
286 <#if ["EN VIGOR", "IN FORCE", "EM VIGOR", "IN VIGORE"]?seq_contains(normalizedStatus)>
287 <#local tagClass = tagClass + " tag-success">
288 <#local isInForce = true>
289 <#elseif ["ANULADA", "CANCELLED", "ANULADA", "ANNULLATA"]?seq_contains(normalizedStatus)>
290 <#local tagClass = tagClass + " tag-danger">
291 <#local isCancelled = true>
292 <#elseif ["PROYECTO", "PROJECT", "PROJETO", "PROGETTO"]?seq_contains(normalizedStatus)>
293 <#local tagClass = tagClass + " tag-blue">
294 <#local isProject = true>
295 </#if>
296
297 <#return {
298 "status": status,
299 "tagClass": tagClass?trim,
300 "isInForce": isInForce,
301 "isCancelled": isCancelled,
302 "isProject": isProject
303 }>
304</#function>
305
306<script>
307jQuery(async function($){
308 function euros(v){
309 var n = Number(v) || 0;
310 return n.toLocaleString('es-ES', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) + ' €';
311 }
312 function uniq(a){ return Array.from(new Set(a)); }
313
314 function fill($sel, values, textFn){
315 var keep = $sel.val();
316 $sel.empty();
317 values.forEach(function(v){
318 $sel.append($('<option>', { value: v, text: textFn ? textFn(v) : v }));
319 });
320 if(keep && values.indexOf(keep) !== -1){ $sel.val(keep); }
321 else if(values.length){ $sel.val(values[0]); }
322 }
323
324 function relabel($sel, textFn){
325 $sel.find('option').each(function(){
326 var v = $(this).attr('value');
327 $(this).text(textFn ? textFn(v) : v);
328 });
329 }
330
331 function toUC(s){ return (s||'').toString().toUpperCase(); }
332
333 function initStandardCard($card){
334 var dataNode = $card.find('script.normas-data')[0];
335 if(!dataNode) return; // sin datos -> nada que sincronizar
336
337 var data = [];
338 try { data = JSON.parse(dataNode.textContent); } catch(e) { data = []; }
339 if(!data.length) return;
340
341 var mapNode = $card.find('script.lang-map')[0];
342 var langMap = {};
343 try { if(mapNode) langMap = JSON.parse(mapNode.textContent); } catch(e) { langMap = {}; }
344
345 var $lang = $card.find('.select-language');
346 var $fmt = $card.find('.select-format');
347 var $price = $card.find('.price-ae');
348
349 function labelOf(lang){ return langMap[toUC(lang)] || lang; }
350 function formatsFor(lang){
351 return uniq(data.filter(function(d){ return d.language === lang; }).map(function(d){ return d.format; }));
352 }
353 function languagesFor(fmt){
354 return uniq(data.filter(function(d){ return d.format === fmt; }).map(function(d){ return d.language; }));
355 }
356 function priceOf(lang, fmt){
357 var m = data.find(function(d){ return d.language === lang && d.format === fmt; });
358 return m ? Number(m.price) || 0 : 0;
359 }
360
361 function onLanguageChange(){
362 var lang = $lang.val();
363 var fmts = formatsFor(lang);
364 fill($fmt, fmts, null);
365 $price.text(euros(priceOf(lang, $fmt.val())));
366 }
367 function onFormatChange(){
368 var fmt = $fmt.val();
369 var langs = languagesFor(fmt);
370 fill($lang, langs, labelOf);
371 $price.text(euros(priceOf($lang.val(), fmt)));
372 }
373
374 var l0 = $lang.val();
375 var f0 = $fmt.val();
376 if(!data.find(function(d){ return d.language === l0 && d.format === f0; })){
377 onLanguageChange();
378 } else {
379 $price.text(euros(priceOf(l0, f0)));
380 }
381
382 relabel($lang, labelOf); // literal de idioma
383
384 $lang.off('change.std').on('change.std', onLanguageChange);
385 $fmt.off('change.std').on('change.std', onFormatChange);
386
387 // Añadir a la cesta
388 $card.find('.standard-button').off('click.add').on('click.add', function(e){
389 var btn = this;
390 if ($(btn).hasClass('disabled')) { e.preventDefault(); return; }
391 var run = async function(){
392 e.preventDefault();
393 var entryType = ($card.data('entrytype') || 'Norma');
394 var code = ($card.data('code') || '').toString();
395 var $langSel = $card.find('.select-language');
396 var $fmtSel = $card.find('.select-format');
397 var codIdioma = ($langSel.filter(function(){ return this.value; }).first().val() || $langSel.val() || '').toString();
398 var codFormato = ($fmtSel.filter(function(){ return this.value; }).first().val() || $fmtSel.val() || '').toString();
399 if ((!codIdioma || !codFormato) && data && data.length){
400 codIdioma = codIdioma || (data[0].language || data[0].codLanguage || '');
401 codFormato = codFormato || (data[0].format || data[0].codFormat || '');
402 }
403 if (!code) { code = ($card.find('.title-standard').text() || '').trim(); }
404 var priceToSend = Number(priceOf(codIdioma, codFormato)) || 0;
405 await window.ecomGlobalScripts.functions.addToCart({
406 entryType: entryType,
407 code: code,
408 codIdioma: codIdioma,
409 codFormato: codFormato,
410 price: priceToSend,
411 name: ($card.data('title') || '').toString(),
412 amount: 1
413 });
414 };
415 var helper = window.ecomGlobalScripts?.functions?.withButtonSpinner;
416 if (typeof helper === 'function') {
417 return helper(btn, run, { spinnerClass: 'spinner-border spinner-border-sm text-light', mode: 'replace' });
418 }
419 return run();
420 });
421 }
422
423 // Alturas por fila
424 function adjustHeightsByRow(){
425 var $cards = $('.list-standards-section .item-standard');
426 var $titles = $cards.find('.title-standard');
427 var $descs = $cards.find('.description-text');
428 var $infos = $cards.find('.info-standard');
429
430 $cards.css('height','auto');
431 $titles.css('height','auto');
432 $descs.css('height','auto');
433 $infos.css('height','auto');
434
435 if($(window).width() <= 767.98) return;
436
437 var rows = [];
438 var tol = 4; // tolerancia de alineación
439 $cards.each(function(){
440 var $c = $(this);
441 var top = Math.round($c.position().top);
442 var row = rows.find(function(r){ return Math.abs(r.top - top) <= tol; });
443 if(!row){ row = { top: top, cards: [] }; rows.push(row); }
444 row.cards.push($c);
445 });
446
447 rows.forEach(function(r){
448 var hCard = 0, hTitle = 0, hDesc = 0, hInfo = 0;
449 r.cards.forEach(function($c){
450 hCard = Math.max(hCard, $c.outerHeight());
451 hTitle = Math.max(hTitle, $c.find('.title-standard').outerHeight());
452 hDesc = Math.max(hDesc, $c.find('.description-text').outerHeight());
453 hInfo = Math.max(hInfo, $c.find('.info-standard').outerHeight());
454 });
455 r.cards.forEach(function($c){
456 $c.height(hCard);
457 $c.find('.title-standard').height(hTitle);
458 $c.find('.description-text').height(hDesc);
459 $c.find('.info-standard').height(hInfo);
460 });
461 });
462 }
463
464 async function init(){
465 $('.list-standards-section .item-standard').each(function(){ initStandardCard($(this)); });
466 adjustHeightsByRow();
467 setTimeout(adjustHeightsByRow, 50);
468 }
469
470 var so = localStorage.getItem('salesOrderId'); // flushQueue gestionado por header
471 await init();
472 $(window).on('resize', init);
473});
474</script>
475
476<style>
477.list-standards-section { margin: 40px auto 35px; }
478.list-standards-section .header-section { border-bottom: 1px solid #dbdbdb; margin-bottom: 32px; display: flex; justify-content: space-between; margin-left: 2px; margin-right: 2px; align-items: center; }
479.list-standards-section .header-section .order-elements { font-family: Segoe-UI-This; font-size: 16px; font-weight: 400; line-height: 23px; text-align: left; color: #2d2d2b; }
480.list-standards-section .title-section { font-family: SohoStd-Medium; font-size: 24px; font-weight: 500; line-height: 28.8px; text-align: left; color: var(--brand-color-3, #29337f); }
481.list-standards-section .standards-container { display: flex; flex-wrap: wrap; gap: 20px 0; padding-right: 0; padding-left: 0; }
482.list-standards-section .item-standard { position: relative; border: 1px solid #e0e0e0; padding: 16px 14px; border-radius: 4px; display: flex; flex-direction: column; }
483.list-standards-section .tag-standard { position: absolute; top: -10px; left: 16px; padding: 4px 10px; border-radius: 4px; background-color: #6a9bd3; color: #fff; font-weight: bold; font-size: 12px; z-index: 5; }
484.list-standards-section .date-ISO { font-family: Segoe-UI-This; font-size: 14px; font-weight: 400; line-height: 18.62px; text-align: left; color: #444; }
485.list-standards-section .title-standard { font-family: SohoGothicPro-Regular; font-size: 16px; font-weight: bold; color: #1a4b94; line-height: 24px; text-align: left; padding-top: 10px;}
486.list-standards-section .description-text { margin-top: 10px; margin-bottom: 12px; font-family: SohoGothicPro-Regular; font-weight: 400; line-height: 24px; font-size: 13px; text-align: left; color: #333; }
487.list-standards-section .info-standard { display: flex; flex-direction: column; flex: 1; }
488.list-standards-section .info-standard .status-box .tag-success {background: #2a7a36; color: #fff;}
489.list-standards-section .info-standard .status-box .tag-danger {background: #c00000; color: #fff;}
490.list-standards-section .info-standard .status-box .tag-blue {background: #0078c0; color: #fff;}
491.list-standards-section .price-container { display: flex; flex-direction: column; text-align: left; letter-spacing: -0.02em; margin-top: auto; margin-bottom: 16px; }
492.list-standards-section .price-container .price-ae { font-size: 23px; font-weight: bold; line-height: 22px; margin-bottom: 10px; color: var(--brand-color-1, #1f57a3); }
493.list-standards-section .standard-not-available {
494 border-style: solid;
495 border-width: 2px;
496 background-color: #f2dede;
497 border-color: #ebcccc;
498 color: #a94442;
499 margin-left: -2px !important;
500 padding-left: 4px !important;
501 margin-top: 25px;
502}
503.list-standards-section .standard-button { border: 0; outline: 0; box-shadow: none; appearance: none; -webkit-appearance: none; -moz-appearance: none; padding: 17px; width: 100%; text-align: center; display: block; background-color: var(--brand-color-1, #1f57a3); color: #fff; text-transform: uppercase; font-family: SohoStd-Medium; font-size: 14px; font-weight: 500; line-height: 14px; cursor: pointer; text-decoration: none; }
504.list-standards-section .standard-button:disabled,
505.list-standards-section .standard-button[disabled],
506.list-standards-section .standard-button.disabled,
507.list-standards-section .standard-button.ecom-btn-loading {
508 opacity: .6;
509 pointer-events: none;
510 filter: brightness(0.85);
511}
512.list-standards-section .standard-button:hover,
513.list-standards-section .standard-button:focus,
514.list-standards-section .standard-button:focus-visible,
515.list-standards-section .standard-button:active {
516 background-color: var(--brand-color-1, #1f57a3);
517 color: #fff;
518 text-decoration: none;
519 outline: 2px solid var(--brand-color-2, #6a9bd3);
520 outline-offset: 2px;
521 box-shadow: 0 0 0 3px rgba(106, 155, 211, 0.35);
522}
523.list-standards-section .standard-button:active {
524 filter: brightness(0.85);
525}
526.list-standards-section .view-grid{ gap: 20px 3.5%; }
527.list-standards-section .view-grid .item-standard { width: 31%; }
528@media (max-width:1200px){ .list-standards-section .view-grid .item-standard { width: 48%; } }
529@media (max-width: 576px) { .list-standards-section .view-grid .item-standard { width: 100%; } }
530.options-standard { display: flex; gap: 10px; margin-bottom: 16px; border: none; }
531.options-standard .select-language,
532.options-standard .select-format {
533 background-color: #f5f5f5;
534 border: 1px solid #d0d0d0;
535 color: #2d2d2b;
536 font-size: 13px;
537 height: 38px;
538}
539.options-standard .select-language:disabled,
540.options-standard .select-format:disabled {
541 background-color: #f5f5f5;
542 color: #2d2d2b;
543 opacity: 1;
544}
545.options-standard .select-language { flex: 2; }
546.options-standard .select-format { flex: 1; }
547select.form-control,
548select.form-control:focus,
549select.form-control:focus-visible {
550 background-image: url(/documents/d/global/ico-chevron-down);
551 background-size: 18px 10px;
552 background-position-x: 95%;
553 background-repeat: no-repeat;
554 position: relative;
555}
556.status-standard { margin-right: 10px; background-color: #107510; color: white; font-size: 12px; padding: 2px 6px; border-radius: 3px; font-weight: bold; }
557.date-standard { font-size: 12px; color: #666; }
558.js-checks-container { display: none; }
559</style>










