Se ha producido un error inesperado.
Se ha producido un error inesperado.
Se ha producido un error al procesar la plantilla.
The following has evaluated to null or missing:
==> product.productId [in template "34352066712900#33336#null" at line 12, column 22]
----
Tip: It's the step after the last dot that caused this error, not those before it.
----
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 productId = product.productId [in template "34352066712900#33336#null" at line 12, column 1]
----
1<#-- Variables -->
2<#assign isDebug = false>
3<#assign channelResponse = restClient.get("/headless-commerce-delivery-catalog/v1.0/channels?filter=name eq 'Aenor Tienda'")>
4<#assign channel = channelResponse.items[0]>
5<#assign channelId = channel.id>
6<#assign product = getProduct(channelId, CPDefinition_cProductId.getData()) />
7
8<#-- Product data -->
9<#assign displayDateProduct = CPDefinition_displayDate.getData() />
10<#assign productId = product.productId />
11<#assign cpDefinitionId = product.id />
12<#assign productERC = product.externalReferenceCode />
13<#assign normaIdOnly = productERC?replace("^[^-]*-", "", "r") />
14
15<#assign categoriesProduct = getProductCategories(channelId, productId) />
16<#assign hasProductCategoriaTipoEntidadLibro = isVocabularyNameIntoCategories(categoriesProduct, 'entity type', 'libro') />
17<#assign hasProductCategoriaTipoEntidadNorma = isVocabularyNameIntoCategories(categoriesProduct, 'entity type', 'norma') />
18<#assign hasProductCategoriaTipoEntidadColeccionTematica = isVocabularyNameIntoCategories(categoriesProduct, 'entity type', 'coleccion tematica') />
19<#assign organism = getOrganism(categoriesProduct, 'organismos') />
20
21
22
23<#-- Functions -->
24<#function getProductCategories channelId productId>
25 <#return restClient.get("/headless-commerce-delivery-catalog/v1.0/channels/${channelId}/products/${productId}/categories?sort=vocabulary").items>
26</#function>
27
28
29<#function isVocabularyNameIntoCategories categories vocabulary name>
30 <#assign found = false />
31
32 <#if categories?has_content && vocabulary?has_content && name?has_content>
33
34 <#assign vocabNorm = normalize(vocabulary) />
35 <#assign nameNorm = normalize(name) />
36
37 <#list categories as category>
38 <#if !found>
39 <#assign catVocabNorm = normalize(category.vocabulary) />
40 <#assign catNameNorm = normalize(category.name) />
41
42 <#if catVocabNorm == vocabNorm && catNameNorm == nameNorm>
43 <#assign found = true />
44 </#if>
45 </#if>
46 </#list>
47
48 </#if>
49
50 <#return found>
51</#function>
52
53
54<#function getProduct channelId productId>
55 <#return restClient.get("/headless-commerce-delivery-catalog/v1.0/channels/${channelId}/products/${productId}")>
56</#function>
57
58
59<#function getOrganism categories vocabulary>
60 <#assign organism = "">
61 <#assign vocabNorm = normalize(vocabulary)>
62
63 <#list categories as category>
64 <#assign catVocabNorm = normalize(category.vocabulary) />
65 <#if catVocabNorm == vocabNorm>
66 <#assign organism = category.name>
67 <#if organism == "EN" || organism == "CEN" || organism == "CENELEC">
68 <#assign organism = "UNE">
69 </#if>
70 </#if>
71 </#list>
72 <#return organism>
73</#function>
74
75
76<#function normalize text onlyAccents = false>
77 <#-- proteger null -->
78 <#if !text?has_content>
79 <#return "">
80 </#if>
81
82 <#assign t = text />
83
84 <#-- quitar acentos -->
85 <#assign t = t
86 ?replace("á","a")?replace("é","e")?replace("í","i")
87 ?replace("ó","o")?replace("ú","u")?replace("ü","u")
88 ?replace("ñ","n")
89 ?replace("Á","A")?replace("É","E")?replace("Í","I")
90 ?replace("Ó","O")?replace("Ú","U")?replace("Ü","U")
91 ?replace("Ñ","N")
92 />
93
94 <#-- si NO es solo acentos, normalización completa -->
95 <#if !onlyAccents>
96 <#assign t = t?lower_case />
97 <#assign t = t?trim />
98 <#assign t = t?replace("\\s+", " ", "r") />
99 </#if>
100
101 <#return t>
102</#function>
103
104
105<#-- Div with data -->
106<#-- Se incluye desde dxp-ecom-portal/misc/adt/tienda/Detalle producto/ECOM-Generic-Scripts.ftl -->
107<#--
108 <#if hasProductCategoriaTipoEntidadNorma>
109 <div id="ecom-scripts">
110 <div class="data-product-wrapper"
111 data-product-id="${productId}"
112 data-product-erc="${productERC}"
113 data-product-cpdefinition-id="${cpDefinitionId}"
114 data-channel-id="${channelId}"
115 data-product-islibro="${hasProductCategoriaTipoEntidadLibro?c}"
116 data-product-isnorma="${hasProductCategoriaTipoEntidadNorma?c}"
117 data-product-iscolecciontematica="${hasProductCategoriaTipoEntidadColeccionTematica?c}"
118 />
119 </div>
120 </#if>
121-->
122
123
124<#-- Script JS -->
125<#if hasProductCategoriaTipoEntidadNorma>
126
127 <script id="ecom-norma-scripts">
128
129 window.ecomNormaScripts = window.ecomNormaScripts || (function () {
130
131 /* =====================================
132 PROPIEDADES PRIVADAS
133 ===================================== */
134 const privateProps = window.ecomNormaScripts?.privateProps || {
135 //test1: {},
136 //test2: 0
137 };
138
139 /* =====================================
140 INICIALIZACIÓN DE PROPERTIES PRIVADAS
141 ===================================== */
142 (function initPrivateProps() {
143 /* privateProps.test1 = {
144 test11: "",
145 test111: {
146 test1112: "00,00",
147 test1113: "€",
148 test1114: "00",
149 test1115: "000-00-000",
150 test1116: "0000",
151 test1117: "000"
152 },
153 test2: ""
154 };*/
155 })();
156
157 /* =====================================
158 PROPIEDADES PÚBLICAS
159 ===================================== */
160
161 const publicProps = {
162 funcsEcomGlobalScripts: null,
163 propsEcomGlobalScripts: null,
164 isDebug: false
165 };
166
167 /* =====================================
168 INICIALIZACIÓN DE PROPERTIES PÚBLICAS
169 ===================================== */
170 (function initPublicProps() {
171
172 //publicProps.TEST1 = "";
173 //publicProps.TEST2 = 0;
174
175 })();
176
177 /* =====================================
178 FUNCIONES PÚBLICAS
179 ===================================== */
180
181 //En functions declaramos la functions publicas
182 const functions = {};
183
184 functions.init = async function () {
185 await _initPublicProps();
186 await _DOMContentLoaded();
187 };
188
189 /* =====================================
190 FUNCIONES PRIVADAS
191 ===================================== */
192
193 const _initPublicProps = async function() {
194
195 // Obtener referencias de ecomGlobalScripts
196 publicProps.funcsEcomGlobalScripts = window.ecomGlobalScripts?.functions || {};
197
198 publicProps.propsEcomGlobalScripts = window.ecomGlobalScripts?.properties || {};
199
200 // Configurar debug
201 publicProps.isDebug = (${isDebug?c} || publicProps.propsEcomGlobalScripts?.isDebug) ?? false;
202
203 // Asegurar que querySelectors existe (crearlo si no existe)
204 publicProps.propsEcomGlobalScripts.querySelectors ??= {};
205
206 // Agregar las propiedades querySelectors
207 publicProps.propsEcomGlobalScripts.querySelectors.rootSelector = "#ecom-norma-detail-normas-referenced .standards-section";
208 };
209
210 const _DOMContentLoaded = async function () {
211 //Se llama desde [dxp-ecom-portal/misc/adt/tienda/Detalle producto/ECOM-Global-Scripts.ftl] desde: document.addEventListener("DOMContentLoaded")
212
213 if (publicProps.isDebug) console.log("DOMContentLoaded ecom Norma scripts");
214
215 window.ecomGlobalScripts?.functions?.pushViewItemEvent?.({
216 name: "${product.name?js_string}",
217 id: "${normaIdOnly?js_string}",
218 category: "Normas ${organism?js_string}"
219 });
220 };
221
222 // ---- Execute Listener DOMContentLoaded ----
223 document.addEventListener("DOMContentLoaded", async function() {
224 //Se llama desde [dxp-ecom-portal/misc/adt/tienda/Detalle producto/ECOM-Global-Scripts.ftl] desde: document.addEventListener("DOMContentLoaded")
225 //await _DOMContentLoaded();
226 });
227
228 /* =====================================
229 API PÚBLICA
230 ===================================== */
231 return {
232 properties: publicProps, // properties public
233 functions: functions // functions public
234 };
235
236 })();
237
238 </script>
239
240
241 <script id="ecom-norma-referenced">
242 //TODO: repasar y quitar lo relacionado con selectores y precio.
243
244 /*TODO:
245 -Se llama a la [async function init()] dentro de aqui [ecomNormaReferencedScript]
246 desde fragment [dxp-ecom-portal/misc/fragments/Collection Tienda/ECOM-Global_functions/index.js] de la siguiente manera:
247
248 if (typeof window.ecomNormaReferencedScript?.init === "function") {
249 await window.ecomNormaReferencedScript.init();
250 }
251 */
252
253 window.ecomNormaReferencedScript = (function ($) {
254
255 /* === CONFIG === */
256 let DEBUG = false;
257 let ROOT_SELECTOR = null;
258
259 function log(...args) {
260 if (DEBUG) console.log("[ecomNorma]", ...args);
261 }
262
263 /* === FUNCIONES PRIVADAS === */
264
265 function _euros(v) {
266 const n = Number(v) || 0;
267 return n.toLocaleString("es-ES", { minimumFractionDigits: 2, maximumFractionDigits: 2 }) + " €";
268 }
269
270 function _uniq(a) {
271 return Array.from(new Set(a));
272 }
273
274 function _fill($sel, values, textFn) {
275 const keep = $sel.val();
276 $sel.empty();
277 values.forEach(v => {
278 $sel.append($("<option>", { value: v, text: textFn ? textFn(v) : v }));
279 });
280 if (keep && values.includes(keep)) $sel.val(keep);
281 else if (values.length) $sel.val(values[0]);
282 }
283
284 function _relabel($sel, textFn) {
285 $sel.find("option").each(function () {
286 const v = $(this).attr("value");
287 $(this).text(textFn ? textFn(v) : v);
288 });
289 }
290
291 /* === initCarousel — PÚBLICA === */
292 function initCarousel(force = false) {
293 const $owl = $(ROOT_SELECTOR + " .standards-container");
294
295 // no inicializar si no existe, está oculto o no tiene items
296 if (!$owl.length || !$owl.hasClass("owl-carousel") || !$owl.is(':visible') || $owl.children().length === 0) {
297 log("initCarousel abortado: no visible o sin items", $owl);
298 return;
299 }
300
301 if (force && $owl.hasClass("owl-loaded") && $owl.data("owl.carousel")) {
302 log("initCarousel Destruyendo Owl Carousel existent:", $owl);
303
304 $owl.trigger('destroy.owl.carousel');
305
306 // limpiar también owl-hidden para permitir reinicialización
307 $owl.removeClass('owl-loaded owl-hidden');
308
309 // limpiar markup de Owl
310 $owl.find('.owl-stage-outer').children().unwrap();
311 }
312
313 if ($owl.length && !$owl.hasClass("owl-loaded") && !$owl.data("owl.carousel")) {
314
315 $owl.owlCarousel({
316 nav: true,
317 navText: [
318 '<i class="fa-solid fa-chevron-left"></i>',
319 '<i class="fa-solid fa-chevron-right"></i>'
320 ],
321 loop: false,
322 dots: false,
323 pagination: false,
324 margin: 25,
325 autoHeight: false,
326 responsive: {
327 0: { items: 1 }, 500: { items: 1 }, 767: { items: 2 }, 1000: { items: 3 }, 1200: { items: 4 }
328 }
329 });
330
331 $owl.off("initialized.owl.carousel refreshed.owl.carousel resized.owl.carousel translated.owl.carousel")
332 .on("initialized.owl.carousel refreshed.owl.carousel resized.owl.carousel translated.owl.carousel", adjustHeights);
333 log("initCarousel executed");
334 }
335 }
336
337 /* === initCard — PÚBLICA === */
338 function initCard($card) {
339 const dataNode = $card.find("script.normas-data")[0];
340 let data = [];
341 if (dataNode) {
342 try { data = JSON.parse(dataNode.textContent); } catch {}
343 }
344 const hasData = Array.isArray(data) && data.length > 0;
345
346 let langMap = {};
347 const mapNode = $card.find("script.lang-map")[0];
348 try { if (mapNode) langMap = JSON.parse(mapNode.textContent); } catch {}
349
350 const $lang = $card.find(".select-language");
351 const $fmt = $card.find(".select-format");
352 const $price = $card.find(".price-ae");
353
354 const toUC = s => (s || "").toString().toUpperCase();
355 const labelOf = lang => langMap[toUC(lang)] || lang;
356
357 const formatsFor = lang => _uniq(data.filter(d => d.language === lang).map(d => d.format));
358 const languagesFor = fmt => _uniq(data.filter(d => d.format === fmt).map(d => d.language));
359 const priceOf = (lang, fmt) => {
360 const m = data.find(d => d.language === lang && d.format === fmt);
361 return m ? Number(m.price) || 0 : 0;
362 };
363 const parsePriceText = (text) => {
364 if (!text) return 0;
365 const clean = String(text).replace(/[^\d,.-]/g, "").trim();
366 if (!clean) return 0;
367 if (clean.includes(",") && clean.includes(".")) {
368 const normalized = clean.replace(/\./g, "").replace(",", ".");
369 return Number(normalized) || 0;
370 }
371 const normalized = clean.replace(",", ".");
372 return Number(normalized) || 0;
373 };
374
375 if (hasData) {
376 function onLanguageChange() {
377 const lang = $lang.val();
378 const fmts = formatsFor(lang);
379 _fill($fmt, fmts);
380 $price.text(_euros(priceOf(lang, $fmt.val())));
381 }
382
383 function onFormatChange() {
384 const fmt = $fmt.val();
385 const langs = languagesFor(fmt);
386 _fill($lang, langs, labelOf);
387 $price.text(_euros(priceOf($lang.val(), fmt)));
388 }
389
390 const l0 = $lang.val();
391 const f0 = $fmt.val();
392 if (!data.find(d => d.language === l0 && d.format === f0)) onLanguageChange();
393 else $price.text(_euros(priceOf(l0, f0)));
394
395 _relabel($lang, labelOf);
396
397 $lang.off("change.ae").on("change.ae", onLanguageChange);
398 $fmt.off("change.ae").on("change.ae", onFormatChange);
399 }
400
401 async function onAdd(e) {
402 e.preventDefault();
403 var entryType = ($card.data('entrytype') || 'Norma');
404 var code = ($card.data('code') || '').toString();
405 var codIdioma = ($card.find('.select-language').val() || '').toString();
406 var codFormato = ($card.find('.select-format').val() || '').toString();
407 if (!code) code = ($card.find('.title-standard').text() || '').trim();
408
409 await window.ecomGlobalScripts.functions.addToCart({
410 entryType: entryType,
411 code: code,
412 codIdioma: codIdioma,
413 codFormato: codFormato,
414 price: Number(priceOf(codIdioma, codFormato)) || 0,
415 name: ($card.data('title') || '').toString(),
416 amount: 1
417 });
418 }
419
420 $card.find(".standard-button").off("click.ae").on("click.ae", function(e){
421 const btn = this;
422 const run = () => onAdd(e);
423 const helper = window.ecomGlobalScripts?.functions?.withButtonSpinner;
424 if (typeof helper === "function") {
425 return helper(btn, run, { spinnerClass: "spinner-border spinner-border-sm text-light", mode: "replace" });
426 }
427 return run();
428 });
429 // Evita que Owl Carousel interprete el click como drag horizontal
430 $card.find(".standard-button")
431 .off("pointerdown.owl mousedown.owl touchstart.owl")
432 .on("pointerdown.owl mousedown.owl touchstart.owl", function(e){
433 e.stopPropagation();
434 });
435 }
436
437 /* === adjustHeights — PÚBLICA === */
438 function adjustHeights() {
439 if ($(window).width() > 766.98) {
440 let hT = 0, hD = 0;
441 $(ROOT_SELECTOR + " .title-standard").css("height","auto").each(function(){ hT=Math.max(hT,$(this).outerHeight()); });
442 $(ROOT_SELECTOR + " .description-text").css("height","auto").each(function(){ hD=Math.max(hD,$(this).outerHeight()); });
443 $(ROOT_SELECTOR + " .title-standard").height(hT);
444 $(ROOT_SELECTOR + " .description-text").height(hD);
445 } else {
446 $(ROOT_SELECTOR + " .title-standard, .description-text").css("height","auto");
447 }
448 log("adjustHeights executed");
449 }
450
451 /* === init — PÚBLICA === */
452 async function init() {
453
454 // debug externo
455 DEBUG = window.ecomNormaScripts?.properties?.isDebug === true;
456
457 // prefijo para todos los selectores
458 ROOT_SELECTOR =
459 window.ecomGlobalScripts?.properties?.querySelectors?.rootSelector ||
460 window.ecomNormaScripts?.properties?.querySelectors?.rootSelector ||
461 "#ecom-norma-detail-normas-referenced .standards-section";
462
463 log("window.ecomNormaReferencedScript init");
464
465 $(ROOT_SELECTOR + " .item-standard").each(function(){ initCard($(this)); });
466 initCarousel(true);
467 adjustHeights();
468 $(window).off("resize.standards").on("resize.standards", adjustHeights);
469 log("init executed");
470 }
471
472 /* === API pública === */
473 return { init, initCard, adjustHeights, initCarousel };
474
475 })(jQuery);
476
477 </script>
478
479</#if>
Se ha producido un error inesperado.










