PageSourceSearch

https://panel.flexyconsent.com/consent/panel/js/script.js?v=1790848744000

js flexyconsent.com collected 2026-10-01 09:20:20 UTC 550,606 bytes, 13,111 lines download raw bytes

1let translations=window.messages||{};
2// let lang;
3let langId;
4
5function setTextContentTranslationByElement(element, key, prefix, suffix){if (element) {const text = translations[key];if (text)element.textContent = (prefix || "") + text + (suffix || "");else console.log('Translation not found: ' + key);}}
6function setTextContentTranslationById(id, key, prefix){const element = document.getElementById(id);if (element === null) return;const text = translations[key];if (text !== undefined) {if (prefix)element.textContent = prefix + text;else element.textContent = text;} else {console.log('Translation not found: ' + key);}}
7function setPlaceholderTranslationById(id, key, suffix) {const element = document.getElementById(id);if (element === null) return;const text = translations[key];if (text !== undefined) {if (suffix)element.setAttribute('placeholder', text + suffix);else element.setAttribute('placeholder', text);} else {console.log('Translation not found: ' + key);}}
8function setTextContentTranslationByQuery(query, key, prefix) {const element = document.querySelector(query);if (element === null) return;const text = translations[key];if (text !== undefined){if (prefix)element.textContent = prefix + text;else element.textContent = text;} else {console.log('Translation not found: ' + key);}}
9
10var background1 = "#0d6625", background2 = "#4a6767", color1 = "#faf7f7", color2 = "#faf7f7";
11// let languages = [];
12const currencyMap = { 'USD': '$', 'EUR': '€', 'GBP': '£', 'TR': '₺' };
13const endpoint = window.location.href.split("/").slice(0, -4).join("/") + '/rest';
14//const endpoint = "http://localhost:8080/com.westcoast.revenueflex.web/rest";
15//const endpoint = 'https://revenueflex.com/rest';
16const pageUrl = window.location.href;
17const page = pageUrl.split('?')[0].substring(pageUrl.lastIndexOf('/') + 1);
18
19function alog(strng) {
20	console.log("%cFCCMP.%c: " + strng, "background:" + background1 + ";color:" + color1 + ";border-radius:3px;font-weight:bold;padding-left:3px;padding-right:3px",
21		"background:" + background2 + ";color:" + color2 + ";font-weight:bold;");
22}
23
24// Per-region site-data (device storage) consent config.
25// `legallyRequired` means the toggle is forced on and cannot be turned off —
26// GDPR / Quebec Law 25 / KVKK all legally mandate opt-in for device storage.
27const SITE_DATA_REGIONS = [
28	{ region: 'eu', legallyRequired: true },
29	{ region: 'ca', legallyRequired: true },
30	{ region: 'tr', legallyRequired: true },
31	{ region: 'br', legallyRequired: true },
32	{ region: 'za', legallyRequired: true },
33	{ region: 'jp', legallyRequired: true },
34	{ region: 'ch', legallyRequired: true },
35	{ region: 'au', legallyRequired: true },
36	{ region: 'kr', legallyRequired: true },
37	{ region: 'th', legallyRequired: true },
38	{ region: 'in', legallyRequired: true },
39	{ region: 'us', legallyRequired: false },
40	{ region: 'others', legallyRequired: false }
41];
42
43function collectSiteDataConsent() {
44	const out = {};
45	SITE_DATA_REGIONS.forEach(function(r) {
46		const enable = document.getElementById('site_data_enable_' + r.region);
47		if (!enable) return;
48		// Publishers may override legally-required regions — the checkbox value is
49		// honoured verbatim. The CMP displays a "legally required" badge as guidance,
50		// but the user has the final say.
51		// No `title` — the Essential group has a fixed, localized label.
52		out[r.region] = { enabled: enable.checked ? 1 : 0 };
53	});
54	const qc = quebecValueToSave('site_data_enable_qc');
55	if (qc !== undefined) out.qc = { enabled: qc };
56	return out;
57}
58
59function isAnySiteDataEnabled() {
60	syncQuebecRows();
61	for (var i = 0; i < SITE_DATA_REGIONS.length; i++) {
62		var r = SITE_DATA_REGIONS[i];
63		var el = document.getElementById('site_data_enable_' + r.region);
64		if (el && el.checked) return true;
65	}
66	var qc = document.getElementById('site_data_enable_qc');
67	return !!(qc && qc.checked);
68}
69
70// J129: Quebec (Law 25) has its own rows, and they FOLLOW the Canada rows until the publisher changes one: while it
71// follows, the Quebec checkbox mirrors Canada's and a save leaves Quebec out, so the server keeps judging Quebec visitors
72// by the Canada row (Utils.regionFlag / siteDataRow), as for every project before Quebec was split out. Only a click on a
73// Quebec checkbox sets it apart (a programmatic change — Select all, Configure — never does).
74const QUEBEC_ROWS = [
75	{ qc: 'advertising_qc', ca: 'advertising_ca' },
76	{ qc: 'site_data_enable_qc', ca: 'site_data_enable_ca' }
77];
78
79function quebecFollowsCanada(qcId) {
80	const el = document.getElementById(qcId);
81	return !el || el.dataset.followsCanada !== '0';
82}
83
84function setQuebecFollows(qcId, follows) {
85	const el = document.getElementById(qcId);
86	if (!el) return;
87	el.dataset.followsCanada = follows ? '1' : '0';
88	const hint = document.getElementById(qcId + '_follows');
89	if (hint) hint.classList.toggle('d-none', !follows);
90}
91
92function syncQuebecRows() {
93	QUEBEC_ROWS.forEach(function(r) {
94		const qc = document.getElementById(r.qc), ca = document.getElementById(r.ca);
95		if (qc && ca && quebecFollowsCanada(r.qc)) qc.checked = ca.checked;
96	});
97}
98
99// advertising = the loaded cmpAdvertisingSettings: a stored Quebec value sets the row apart, an absent one follows Canada.
100function loadQuebecRows(advertising) {
101	advertising = advertising || {};
102	const siteDataQc = (advertising.siteDataConsent || {}).qc;
103	const stored = {
104		advertising_qc: Object.prototype.hasOwnProperty.call(advertising, 'qc') ? advertising.qc : undefined,
105		site_data_enable_qc: siteDataQc && Object.prototype.hasOwnProperty.call(siteDataQc, 'enabled') ? siteDataQc.enabled : undefined
106	};
107	QUEBEC_ROWS.forEach(function(r) {
108		const el = document.getElementById(r.qc);
109		if (!el) return;
110		const v = stored[r.qc];
111		setQuebecFollows(r.qc, v === undefined || v === null);
112		if (v !== undefined && v !== null) el.checked = !!v;
113	});
114	syncQuebecRows();
115}
116
117// 0/1 for a Quebec row set apart, undefined while it follows Canada (left out of the save).
118function quebecValueToSave(qcId) {
119	syncQuebecRows();
120	const el = document.getElementById(qcId);
121	if (!el || quebecFollowsCanada(qcId)) return undefined;
122	return el.checked ? 1 : 0;
123}
124
125function addQuebecAdvertising(advertisingSettings) {
126	const qc = quebecValueToSave('advertising_qc');
127	if (qc !== undefined) advertisingSettings.qc = qc;
128	return advertisingSettings;
129}
130
131document.addEventListener('change', function(e) {
132	const id = e.target && e.target.id;
133	if (!id) return;
134	QUEBEC_ROWS.forEach(function(r) {
135		if (id === r.qc && e.isTrusted) setQuebecFollows(r.qc, false);
136		else if (id === r.ca) syncQuebecRows();
137	});
138});
139
140function loadSiteDataConsent(siteData) {
141	if (!siteData) siteData = {};
142	SITE_DATA_REGIONS.forEach(function(r) {
143		const row = siteData[r.region] || {};
144		const enable = document.getElementById('site_data_enable_' + r.region);
145		// Legally-required default = on; user may uncheck on subsequent visits.
146		// Only apply the default when the stored value is undefined.
147		if (enable) {
148			if ('enabled' in row) enable.checked = row.enabled !== 0;
149			else enable.checked = !!r.legallyRequired;
150		}
151	});
152}
153
154// Canonical list of US-state-law GPP sections exposed in the Advertising tab.
155// Each entry: { key: JSON key under cmpSectionPreferences, prefix: DOM id prefix, withTargetedAds: whether the section shows a targeted-ads control }.
156// California doesn't ship a targeted-ads control (its CCPA/uscav1 behaviour bakes that into the single sale/share toggle),
157// so it's the only section with withTargetedAds=false.
158const US_SECTION_PREFS = [
159	{ key: 'usNat', prefix: 'us_nat_pref', withTargetedAds: true },
160	{ key: 'usCa',  prefix: 'us_ca_pref',  withTargetedAds: false },
161	{ key: 'usVa',  prefix: 'us_va_pref',  withTargetedAds: true },
162	{ key: 'usCo',  prefix: 'us_co_pref',  withTargetedAds: true },
163	{ key: 'usUt',  prefix: 'us_ut_pref',  withTargetedAds: true },
164	{ key: 'usCt',  prefix: 'us_ct_pref',  withTargetedAds: true },
165	{ key: 'usTx',  prefix: 'us_tx_pref',  withTargetedAds: true },
166	{ key: 'usOr',  prefix: 'us_or_pref',  withTargetedAds: true },
167	{ key: 'usMt',  prefix: 'us_mt_pref',  withTargetedAds: true },
168	{ key: 'usDe',  prefix: 'us_de_pref',  withTargetedAds: true },
169	{ key: 'usIa',  prefix: 'us_ia_pref',  withTargetedAds: true },
170	{ key: 'usNh',  prefix: 'us_nh_pref',  withTargetedAds: true },
171	{ key: 'usNj',  prefix: 'us_nj_pref',  withTargetedAds: true },
172	{ key: 'usTn',  prefix: 'us_tn_pref',  withTargetedAds: true },
173	{ key: 'usMn',  prefix: 'us_mn_pref',  withTargetedAds: true },
174	{ key: 'usNe',  prefix: 'us_ne_pref',  withTargetedAds: true },
175	{ key: 'usFl',  prefix: 'us_fl_pref',  withTargetedAds: true }
176];
177
178// Build the full cmpSectionPreferences object from the form DOM.
179// Returns an object like { usNat: {...}, usCa: {...}, usVa: {...}, ... }.
180// Missing/hidden elements are silently skipped so legacy forms that don't render every state are safe.
181function collectUsSectionPreferences() {
182	const out = {};
183	US_SECTION_PREFS.forEach(function(sec) {
184		const personal = document.getElementById(sec.prefix + '_personal_data');
185		const sensitive = document.getElementById(sec.prefix + '_sensitive_data');
186		if (!personal || !sensitive) return;
187		const obj = {
188			simplificationPersonalData: parseInt(personal.value),
189			sensitiveData: parseInt(sensitive.value)
190		};
191		if (sec.withTargetedAds) {
192			const targeted = document.getElementById(sec.prefix + '_targeted_ads');
193			if (targeted) obj.targetedAdvertising = parseInt(targeted.value);
194		}
195		out[sec.key] = obj;
196	});
197	return out;
198}
199
200// Load values from a previously-stored cmpSectionPreferences object into the form.
201function loadUsSectionPreferences(sectionPrefs) {
202	if (!sectionPrefs) return;
203	US_SECTION_PREFS.forEach(function(sec) {
204		const prefs = sectionPrefs[sec.key];
205		if (!prefs) return;
206		const personal = document.getElementById(sec.prefix + '_personal_data');
207		const sensitive = document.getElementById(sec.prefix + '_sensitive_data');
208		if (personal && 'simplificationPersonalData' in prefs) personal.value = prefs['simplificationPersonalData'];
209		if (sensitive && 'sensitiveData' in prefs) sensitive.value = prefs['sensitiveData'];
210		if (sec.withTargetedAds && 'targetedAdvertising' in prefs) {
211			const targeted = document.getElementById(sec.prefix + '_targeted_ads');
212			if (targeted) targeted.value = prefs['targetedAdvertising'];
213		}
214	});
215}
216
217// Canonical region codes — kept in sync with the server's
218// Utils.isSiteDataConsentLegallyRequired / getAdvertisingSettings region list.
219const ALL_REGION_CODES = ['eu','us','ca','tr','br','za','jp','ch','au','kr','th','in','others'];
220
221// Quebec (J129) is not in ALL_REGION_CODES: the defaults below leave it out so it follows Canada. The Configure modal
222// still moves its checkboxes with the group (a following one ends up equal to Canada's either way).
223const CFG_TCF_REGIONS = ALL_REGION_CODES.map(function(r){ return 'advertising_' + r; }).concat(['advertising_qc']);
224const CFG_REG_REGIONS = ['regulation_enable_us'];
225const CFG_GCM_REGIONS = ALL_REGION_CODES.map(function(r){ return 'site_data_enable_' + r; }).concat(['site_data_enable_qc']);
226
227// Shared payload builder: every TCF region and every site-data region set to
228// enabled. Used by the initial wizard and by the Configure modal's
229// "enable group" transition so the defaults stay in one place.
230function buildAllRegionsConsentDefaults() {
231	const out = { siteDataConsent: {} };
232	ALL_REGION_CODES.forEach(function(r) {
233		out[r] = 1;
234		out.siteDataConsent[r] = { enabled: 1 };
235	});
236	return out;
237}
238const CFG_GCM_SUB_CHECKBOXES = ['gcm_url_passthrough','gcm_ads_data_redaction','gcm_google_signals','gcm_ad_personalization_signals','gcm_restricted_data_processing'];
239
240function anyChecked(ids) {
241	return ids.some(function(id){ const el = document.getElementById(id); return el && el.checked; });
242}
243
244function setupConsentConfigureModal() {
245	const modal = document.getElementById('consentConfigureModal');
246	if (!modal) return;
247	const cfgTcf = document.getElementById('cfg_enable_tcf');
248	const cfgReg = document.getElementById('cfg_enable_regulations');
249	const cfgGcm = document.getElementById('cfg_enable_gcm');
250
251	// Sections stay visible at all times — the Configure modal only toggles the
252	// underlying per-region checkboxes. Hiding sections confused publishers who
253	// wanted to see "what am I about to save" after clicking Apply.
254
255	let snapshot = null;
256
257	function takeSnapshot() {
258		const s = { checkboxes: {}, values: {} };
259		CFG_TCF_REGIONS.concat(CFG_REG_REGIONS, CFG_GCM_REGIONS, CFG_GCM_SUB_CHECKBOXES)
260			.forEach(function(id){ const el = document.getElementById(id); if (el) s.checkboxes[id] = el.checked; });
261		const valIds = ['gcm_send_page_view','gcm_wait_for_update'];
262		US_SECTION_PREFS.forEach(function(sec){
263			valIds.push(sec.prefix + '_personal_data');
264			valIds.push(sec.prefix + '_sensitive_data');
265			if (sec.withTargetedAds) valIds.push(sec.prefix + '_targeted_ads');
266		});
267		valIds.forEach(function(id){ const el = document.getElementById(id); if (el) s.values[id] = el.value; });
268		return s;
269	}
270
271	function restoreSnapshot(s) {
272		if (!s) return;
273		Object.keys(s.checkboxes).forEach(function(id){ const el = document.getElementById(id); if (el) el.checked = s.checkboxes[id]; });
274		Object.keys(s.values).forEach(function(id){ const el = document.getElementById(id); if (el) el.value = s.values[id]; });
275	}
276
277	modal.addEventListener('show.bs.modal', function(){
278		syncQuebecRows();
279		snapshot = takeSnapshot();
280		// Master = ON only when every region in the group is checked. Pre-checked
281		// legally-required regions (EU/CA/TR for site data) mean the group is
282		// partial by default — that reads as "off" in the master so clicking it
283		// cleanly cascades to "enable everyone" on Apply.
284		cfgTcf.checked = CFG_TCF_REGIONS.every(function(id){ const el = document.getElementById(id); return el && el.checked; });
285		cfgReg.checked = CFG_REG_REGIONS.every(function(id){ const el = document.getElementById(id); return el && el.checked; });
286		cfgGcm.checked = CFG_GCM_REGIONS.every(function(id){ const el = document.getElementById(id); return el && el.checked; });
287	});
288
289	modal.addEventListener('hidden.bs.modal', function(){
290		if (snapshot) {
291			restoreSnapshot(snapshot);
292			snapshot = null;
293		}
294	});
295
296	const clearAllBtn = document.getElementById('consent_configure_clear_all');
297	if (clearAllBtn) clearAllBtn.addEventListener('click', function(){
298		cfgTcf.checked = false; cfgReg.checked = false; cfgGcm.checked = false;
299		CFG_TCF_REGIONS.concat(CFG_REG_REGIONS, CFG_GCM_REGIONS).forEach(function(id){
300			const el = document.getElementById(id); if (el) el.checked = false;
301		});
302		CFG_GCM_SUB_CHECKBOXES.forEach(function(id){
303			const el = document.getElementById(id); if (el) el.checked = true;
304		});
305		const spv = document.getElementById('gcm_send_page_view'); if (spv) spv.value = 'true';
306		const wfu = document.getElementById('gcm_wait_for_update'); if (wfu) wfu.value = '1000';
307		US_SECTION_PREFS.forEach(function(sec){
308			const p = document.getElementById(sec.prefix + '_personal_data'); if (p) p.value = '0';
309			const s = document.getElementById(sec.prefix + '_sensitive_data'); if (s) s.value = '0';
310			if (sec.withTargetedAds) {
311				const t = document.getElementById(sec.prefix + '_targeted_ads'); if (t) t.value = '1';
312			}
313		});
314	});
315
316	const applyBtn = document.getElementById('consent_configure_apply');
317	if (applyBtn) applyBtn.addEventListener('click', function(){
318		// Master semantics on Apply:
319		//   master ON  + subs not all on  → enable every region in the group
320		//   master OFF + any sub on       → disable every region in the group
321		//   otherwise                      → no change (already matches intent)
322		// This lets a partially-populated group (e.g. EU/CA/TR pre-checked for
323		// site data) still respond when the user clicks the master — previously
324		// the handler bailed because "at least one" was already checked.
325		function applyMasterToGroup(master, ids, onFreshEnable) {
326			const allChecked = ids.every(function(id){ const el = document.getElementById(id); return el && el.checked; });
327			const anyChecked = ids.some(function(id){ const el = document.getElementById(id); return el && el.checked; });
328			if (master.checked && !allChecked) {
329				ids.forEach(function(id){ const el = document.getElementById(id); if (el) el.checked = true; });
330				if (onFreshEnable && !anyChecked) onFreshEnable();
331			} else if (!master.checked && anyChecked) {
332				ids.forEach(function(id){ const el = document.getElementById(id); if (el) el.checked = false; });
333			}
334		}
335		applyMasterToGroup(cfgTcf, CFG_TCF_REGIONS);
336		applyMasterToGroup(cfgReg, CFG_REG_REGIONS, function(){
337			// Fresh enable: seed every US state/national section with display-friendly
338			// combo values so the visitor actually sees opt-out rows. Publisher can
339			// refine per-state afterwards in the US Section Preferences panel.
340			US_SECTION_PREFS.forEach(function(sec){
341				const p = document.getElementById(sec.prefix + '_personal_data'); if (p) p.value = '0';
342				const s = document.getElementById(sec.prefix + '_sensitive_data'); if (s) s.value = '1';
343				if (sec.withTargetedAds) {
344					const t = document.getElementById(sec.prefix + '_targeted_ads'); if (t) t.value = '1';
345				}
346			});
347		});
348		applyMasterToGroup(cfgGcm, CFG_GCM_REGIONS);
349
350		snapshot = null;
351		const instance = bootstrap.Modal.getInstance(modal) || new bootstrap.Modal(modal);
352		instance.hide();
353
354		// Persist immediately — Apply is the commit point for the whole
355		// Consent & Privacy configuration. Triggering the advertising form's
356		// submit handler reuses the existing save pipeline (collectSiteDataConsent,
357		// section preferences, etc.) so publishers don't have to find the
358		// page-level Save button afterwards. requestSubmit() is preferred
359		// (runs native validation first); fall back to a dispatched submit
360		// event on older browsers.
361		var advertisingFormEl = document.getElementById('advertising_settings_form');
362		if (advertisingFormEl) {
363			if (typeof advertisingFormEl.requestSubmit === 'function') {
364				advertisingFormEl.requestSubmit();
365			} else {
366				advertisingFormEl.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));
367			}
368		}
369	});
370}
371
372async function onLoad(){
373
374	//removePlaceholders();	
375	console.log('document load handler');
376	
377	// const pageUrl = window.location.href;
378	// const page = pageUrl.split('?')[0].substring(pageUrl.lastIndexOf('/') + 1);
379	const page = new URL(window.location.href).pathname.split('/').pop();
380
381	addLogoutHandler();
382	// addSubscriptionRequiredMessage();
383	// loadTemplates();
384	// loadTemplateSettings();
385	//setCancelBtnClicking(page);
386	displayPanelLanguageSelector();
387	let alertDisplaying = true;
388
389	switch (page) {
390		case 'websites.jsp':
391			alog("Jump to websites 1");
392			await loadWebsites();
393			setupDeletedDomainsToggle();
394			break;
395		case 'admin_users.jsp':
396			loadAdminUsers();
397			break;
398		case 'website_settings.jsp':
399			alog("Jump to website settings 2");
400			await loadWebsiteSettings();
401			break;
402		case 'user_settings.jsp':
403			alog("Jump to user settings 3");
404			await loadUserSettings(document.getElementById('user_settings_form'));
405			document.getElementById('formAccountSettings').addEventListener('submit', updatePassword);
406			break;
407		case 'dashboard.jsp':
408			alog("Jump to dashboard 4");
409			await loadDashboard();
410			break;
411		case 'consent_logs.jsp':
412			alog("Jump to consent log");
413			await loadConsentLogs();
414			break;
415		case 'subscriptions.jsp':
416			alog("Jump to subscriptions 5");
417			await loadSubscriptions();
418			break;
419		case 'payment.jsp':
420			alog("Jump to payment 6");
421			loadPayment();
422			break;
423		case 'payment_result.jsp':
424			alog("Jump to payment result 7");
425			loadPaymentResult();
426			break;
427		case 'payments.jsp':
428			alog("Jump to payments 8");
429			await loadPayments();
430			break;
431		case 'contact_us.jsp':
432			alog("Jump to contact form 9");
433			loadContactUs();
434			break;
435		case 'reseller.jsp':
436			alog("Jump to resellers 10");
437			loadResellers();
438			break;
439		case 'reseller_user.jsp':
440			alog("Jump to reseller user 11");
441			loadResellerUser();
442			break;
443		case 'templates.jsp':
444			alog("Jump to templates 12");
445			await loadTemplates();
446			break;
447		case 'template_settings.jsp':
448			alog("Jump to template settings 13");
449			await loadTemplateSettings();
450			// after load so the masters reflect the persisted checkbox states
451			setupSelectAllToggles();
452			break;
453		case 'initial_wizard.jsp':
454			alog("Jump to initial wizard 14");
455			await loadInitialWizard();
456			alertDisplaying = false;
457			break;
458		case 'document_generator.jsp':
459			alog("Jump to document generator 15");
460			await loadDocumentGenerator();
461			break;
462		case 'cookie_manager.jsp':
463             alog("Jump to cookie manager");
464             await loadCookieManager();
465             break;
466		default:
467			alog("Unidentified page");
468			return;
469	}
470
471	setTooltips();
472	if (alertDisplaying)
473		checkAlertIsRequired();
474	
475}
476
477if (document.readyState === "complete")onLoad();
478else document.addEventListener('DOMContentLoaded',  onLoad );
479
480//function removePlaceholders(){
481//	return;
482//	var inputs = document.querySelectorAll('span[data-placeholder-1]');
483//
484//	if (!inputs)
485//		return;
486//	
487//	inputs.forEach(iii => {
488//		iii.setAttribute('data-placeholder-1','');
489//
490//		/*
491//		var input=iii;
492//		if (!input.placeholder)
493//			input.placeholder = '';
494//
495//		// it will reset placeholder value on blur by dataset variable
496//		input.addEventListener('blur', (e) => {
497//			if (input.dataset.placeholder && input.dataset.placeholder.length>0 && input.placeholder === "" && (!input.value || input.value.length == 0 ))
498//				input.placeholder = input.dataset.placeholder;
499//		});
500//		
501//		// it will remove placeholder value on focus and store with dataset variable
502//		input.addEventListener('focus', e => {
503//			if (input.placeholder.length>0){
504//				input.dataset.placeholder = input.placeholder;
505//				input.placeholder = '';
506//			}
507//		});
508//		
509//		function ef(input){
510//			if ( !input.value || input.value.length == 0 ){
511//				if (input.placeholder.length>0){
512//					input.dataset.placeholder = input.placeholder;
513//					input.placeholder = '';
514//				}
515//			}
516//			else {
517//				console.log('4 ' + input.value);
518//				if (input.placeholder.length>0){
519//					console.log('5 ' + input.value);
520//					input.dataset.placeholder = input.placeholder;
521//					input.placeholder = '';
522//				}
523//			}
524//		}
525//		
526//		input.addEventListener('change', (e) => {
527//			//if (document.activeElement == input)
528//			//	return;			
529//			ef(input);
530//		});
531//		
532//		ef(input);
533//		*/
534//	});	
535//
536//	//setTimeout(removePlaceholders,1000);
537//}
538
539function checkIfLoginRequired() {
540	if (getSid() == null && !document.getElementById("login_form")) {
541		alog("Redirect to login");
542		window.location.href = 'login.jsp?redirect=' + encodeURIComponent(window.location.href);
543		return 1;
544	}
545	return 0;
546}
547
548function displayPanelLanguageSelector() {
549	let navbarLanguageSelector = document.getElementById('navbar_language_selector');
550	if (!navbarLanguageSelector) {
551		const languageSelectorContainer = document.createElement('div');
552		languageSelectorContainer.style.position = "absolute";
553		languageSelectorContainer.style.top = "10px";
554		languageSelectorContainer.style.right = "10px";
555		languageSelectorContainer.style.padding = "10px";
556		languageSelectorContainer.style.borderRadius = "6px";
557
558		const body = document.querySelector('body');
559		body.insertBefore(languageSelectorContainer, body.firstChild);
560
561		navbarLanguageSelector = document.createElement('div');
562		navbarLanguageSelector.classList.add('nav-item', 'dropdown', 'me-3', 'd-none');
563		navbarLanguageSelector.id = 'navbar_language_selector';
564		languageSelectorContainer.appendChild(navbarLanguageSelector);
565
566		const btn = document.createElement('button');
567		btn.classList.add('navbar-language-selector-btn', 'btn', 'nav-link', 'dropdown-toggle');
568		btn.type = 'button';
569		btn.setAttribute('data-bs-toggle', 'dropdown');
570		btn.setAttribute('aria-expanded', 'false');
571		navbarLanguageSelector.appendChild(btn);
572	}
573
574	// const params = new URLSearchParams(window.location.search);
575	// let lang = params.get('lang') || getCookie('lang') || "en";
576	let selectedLanguage = languages.find(l => l.bundle === lang);
577	if (!selectedLanguage) {
578		if (lang.includes('_'))
579			lang = lang.split('_')[0];
580		else if (lang.includes('-'))
581			lang = lang.split('-')[0];
582
583		selectedLanguage = languages.find(l => l.bundle.split('_')[0] === lang);
584	}
585
586	if (!selectedLanguage) {
587		console.error("Language not found: " + lang);
588		return;
589	}
590
591	langId = selectedLanguage.id;
592
593	const btn = navbarLanguageSelector.querySelector('.navbar-language-selector-btn');
594	btn.textContent = "🌐 " + selectedLanguage.localName;
595
596	const dropdown = document.createElement('ul');
597	dropdown.classList.add('dropdown-menu', 'dropdown-menu-end', 'dropdown-menu-scrollable');
598	navbarLanguageSelector.appendChild(dropdown);
599	navbarLanguageSelector.classList.remove('d-none');
600
601	const langCookie = getCookie('lang');
602	if (langCookie == null || langCookie !== lang)
603		createCookie('lang', lang);
604
605	languages.forEach(option => {
606		const li = document.createElement('li');
607		const link = document.createElement('a');
608		link.classList.add('dropdown-item');
609		link.href = '#';
610		link.addEventListener('click', function(ev) {
611			ev.preventDefault();
612			const url = new URL(window.location.href);
613			url.searchParams.set('lang', option.bundle);
614			window.location.href = url.toString();
615		});
616		// //<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/flag-icon-css/6.7.0/css/flag-icons.min.css">
617		// const flag = document.createElement('span');
618		// flag.classList.add('fi', 'fi-' + option.bundle);
619		// flag.setAttribute('aria-hidden', 'true');
620		// link.appendChild(flag);
621		// const span = document.createElement('span');
622		// span.textContent = option.name;
623		// span.classList.add('ms-2');
624		// link.appendChild(span);
625		link.textContent = option.name;
626
627		if (option.bundle === selectedLanguage.bundle)
628			link.classList.add('fw-bold');
629
630		li.appendChild(link);
631		dropdown.appendChild(li);
632	});
633}
634
635function addLogoutHandler(){
636	const logoutlink = document.getElementById('logout_btn');
637	if (logoutlink)
638		logoutlink.addEventListener('click', event => {
639			event.preventDefault();
640			logout();
641		});
642}
643
644function logout() {
645	//if (event)
646	//	event.preventDefault();
647
648	const sid = getSid();
649	// console.dir("sid: " + sid);
650	//if (sid == null)
651	//	return;
652
653	fetch(endpoint + "/logout/" + sid).then(async response => {
654		if (response.ok) {
655			const responseBody = await response.json();
656			if (responseBody.errorCode === 0) {
657				// console.log("Logout successful");
658				deleteCookie('sid');
659				deleteCookie('name');
660				deleteCookie('email');
661				deleteCookie('domainCounts');
662				clearLocalStorage();
663				alog("Jump to login 10");
664				window.location.href = '../views/login.jsp';
665			} else {
666				console.log('Logout Error Code: ' + responseBody.errorCode);
667			}
668		} else {
669			console.error('Logout failed: ' + JSON.stringify(response));
670		}
671	}).catch(err => {
672		console.log(err);
673	});
674
675	return false;
676}
677
678function checkAlertIsRequired() {
679	if (trialRemainDayCount > 0 && trialRemainDayCount <= 7)
680		displayAlert((translations['trialEndMsg'] || 'Your trial ends in [DayCount] days. Consider upgrading your subscription.').replace('[DayCount]', trialRemainDayCount), 'warning');
681
682	if (!isReseller && usedDomainCount > allowedDomainCount) {
683		const msg = (translations['usingDomainCountExceedMsg'] || 'You have added [usedDomainCount] domains but your plan supports [allowedDomainCount].')
684			.replace('[usedDomainCount]', usedDomainCount).replace('[allowedDomainCount]', allowedDomainCount);
685		// Extra domains are allowed; the user can also drop back under the plan limit
686		// by rejecting the ones they don't need on the domain management page.
687		const manageLinkLabel = translations['manageDomainsLink'] || 'Manage your domains';
688		const manageLink = '<a href="websites.jsp" class="alert-link">' + manageLinkLabel + '</a>';
689		const hint = (translations['manageDomainsHint'] || "Don't need them all? You can [manageDomainsLink] to reject any extra domains.")
690			.replace('[manageDomainsLink]', manageLink);
691		// sticky: stays until the user closes it (plan-limit info must not vanish on its own)
692		displayAlert(msg + '<br>' + hint, 'danger', true);
693	}
694
695	// if (exceededPageViewLimit) {
696	// 	displayAlert(translations['pageviewLimitMsg']);
697	// }
698}
699
700/* TEMPLATES */
701async function loadTemplates() {
702	const projectsTable = document.getElementById('templates_table');
703	if (!projectsTable || checkIfLoginRequired())
704		return;
705
706	if (isReseller)
707		displayUsedDomainCount();
708
709	const data = { sid: getSid(), projectTypeId: 2 };
710	const response = await postRequest(endpoint + '/project/list', data);
711	if (response.ok) {
712		const responseBody = await response.json();
713		if (responseBody.statusCode === 0) {
714			const projects = responseBody.contents;
715			if (projects && !projects.length)
716				return showTemplateCreateLink('templates_table_container', !0);
717
718			document.getElementById('templates_table').style.display = '';
719
720			const tableBody = projectsTable.querySelector('tbody');
721			projects.forEach(project => {
722				// if (project.projectTypeId === 2) {
723				// 	const data = { sid: getSid(), project: project.id };
724				// 	const projectResponse = await postRequest(endpoint + '/project/get', data);
725				// 	const projectResponseBody = await projectResponse.json();
726				// 	debugger;
727
728				const row = document.createElement('tr');
729				//				row.classList.add('gy-3')
730
731				var nameCell = document.createElement('td');
732				nameCell.textContent = project.projectName;
733				row.appendChild(nameCell);
734
735				// const nameCell = document.createElement('td');
736				// const nameLink = document.createElement('a');
737				// nameLink.textContent = project.projectName;
738				// nameLink.href = "template_settings.jsp?id=" + project.id;
739				// nameLink.classList.add("name-link", "align-middle");
740				// nameCell.appendChild(nameLink);
741				// row.appendChild(nameCell);
742
743				const websitesCell = document.createElement('td');
744				// websitesCell.textContent = project.domains.map(function (d) {
745				// 	return d.domainName;
746				// }).join(', ');
747
748				for (let i = 0; i < project.domains.length; i++) {
749					const websiteLink = document.createElement('a');
750					const domain = project.domains[i];
751					websiteLink.textContent = domain.domainName;
752					websiteLink.href = "website_settings.jsp?id=" + domain.id + "&pp=t";
753					websiteLink.classList.add("name-link", "align-middle");
754					websitesCell.appendChild(websiteLink);
755
756					if (i < project.domains.length - 1) {
757						const comma = document.createElement('span');
758						comma.textContent = ', ';
759						websitesCell.appendChild(comma);
760					}
761				}
762				row.appendChild(websitesCell);
763
764				const enabledCell = document.createElement('td');
765				enabledCell.classList.add('text-center');
766				const enabledIcon = document.createElement('i');
767				enabledIcon.classList.add('fa', project.cmpEnabled ? 'fa-check' : 'fa-times');
768				// enabledIcon.classList.add(project.enabled ? 'true' : 'false');
769				enabledIcon.classList.add(project.cmpEnabled ? 'text-success' : 'text-danger');
770				enabledCell.appendChild(enabledIcon);
771				//				enabledCell.classList.add("align-middle");
772				row.appendChild(enabledCell);
773
774				const actionCell = document.createElement('td');
775				actionCell.classList.add('text-center');
776
777				const settingsButton = document.createElement('button');
778				settingsButton.textContent = translations['settings'];
779				settingsButton.classList.add('btn', 'btn-outline-primary', 'btn-sm');
780				settingsButton.addEventListener('click', () => {
781					alog("Jump to project settings " + project.id + " 11");
782					window.location.href = "template_settings.jsp?id=" + project.id;
783				});
784				actionCell.appendChild(settingsButton);
785
786				const scriptButton = document.createElement('button');
787				setTextContentTranslationByElement(scriptButton, 'pageCode');
788				scriptButton.classList.add('btn', 'btn-outline-primary', 'btn-sm', 'ms-2');
789				scriptButton.setAttribute('data-bs-toggle', 'modal');
790				scriptButton.setAttribute('data-bs-target', '#scriptModal');
791				actionCell.appendChild(scriptButton);
792				scriptButton.addEventListener('click', async () => {
793					const contentElement = document.getElementById('script_content_area');
794					const copyButton = document.getElementById('copy_btn');
795					const copyInfo = document.getElementById('copy-info');
796					contentElement.style.display="none";
797					contentElement.textContent = "";
798					copyButton.style.display="none";
799					copyInfo.innerHTML = "";
800					if (project.domains.length){
801						copyInfo.innerHTML = "<div class='loader'></div>";
802						const data = { sid: getSid(), projectId: project.id };
803						const response = await postRequest(endpoint + '/scriptcode', data);
804						if (response && response.ok) {
805							const responseBody = await response.json();
806							if (responseBody.statusCode === 0) {
807								contentElement.textContent = responseBody.content;
808								contentElement.style.display="";
809								copyButton.style.display="";
810								copyInfo.innerHTML = translations["copyScriptLabel"];
811								
812							} else {
813								console.log('Script Error Code: ' + responseBody.statusCode);
814								copyInfo.innerHTML = translations["serverError"];
815							}
816						}else {
817							console.log('Script Error Code: ' + responseBody.statusCode);
818							copyInfo.innerHTML = translations["serverError"];
819						}
820					}
821					else 
822						copyInfo.innerHTML = translations["noWebsitesForTemplate"];
823				});
824
825				const statButton = document.createElement('button');
826				statButton.textContent = translations['stats'];
827				statButton.classList.add('btn', 'btn-outline-primary', 'btn-sm', 'ms-2');
828				actionCell.appendChild(statButton);
829				statButton.addEventListener('click', async () => {
830					alog("Jump to dashboard " + project.id + " 12");
831					window.location.href = "dashboard.jsp?id=" + project.id;
832				});
833
834				/*const cmButton = document.createElement('button');
835				setTextContentTranslationByElement(cmButton, 'cookieDisplayCode');
836				cmButton.classList.add('btn', 'btn-outline-primary', 'btn-sm', 'ms-2');
837				cmButton.setAttribute('data-bs-toggle', 'modal');
838				cmButton.setAttribute('data-bs-target', '#scriptModal');
839				actionCell.appendChild(cmButton);
840				cmButton.addEventListener('click', async () => {
841					const contentElement = document.getElementById('script_content_area');
842					const copyButton = document.getElementById('copy_btn');
843					const copyInfo = document.getElementById('copy-info');
844					contentElement.style.display="none";
845					contentElement.textContent = "";
846					copyButton.style.display="none";
847					copyInfo.innerHTML = "";
848					if (project.domains.length){
849						copyInfo.innerHTML = "<div class='loader'></div>";
850						const data = { sid: getSid(), projectId: project.id };
851						const response = await postRequest(endpoint + '/scriptcode', data);
852						if (response && response.ok) {
853							const responseBody = await response.json();
854							if (responseBody.statusCode === 0) {
855								contentElement.textContent = responseBody.cookiesScriptCode;
856								contentElement.style.display="";
857								copyButton.style.display="";
858								copyInfo.innerHTML = translations["copyScriptLabel"];
859								
860							} else {
861								console.log('Script Error Code: ' + responseBody.statusCode);
862								copyInfo.innerHTML = translations["serverError"];
863							}
864						}else {
865							console.log('Script Error Code: ' + responseBody.statusCode);
866							copyInfo.innerHTML = translations["serverError"];
867						}
868					}
869					else 
870						copyInfo.innerHTML = translations["noWebsitesForTemplate"];
871				});*/
872
873				if (responseBody.ul1) {
874					const deleteButton = document.createElement('button');
875					deleteButton.textContent = translations['delete'] || 'Delete';
876					deleteButton.classList.add('btn', 'btn-outline-primary', 'btn-sm', 'ms-2');
877					actionCell.appendChild(deleteButton);
878					deleteButton.addEventListener('click', async () => {
879						const data2 = { sid: getSid(), projectTypeId: 2, projectId: project.id };
880						const response2 = await postRequest(endpoint + '/project/delete', data2);
881						if (response2.ok) {
882							const responseBody2 = await response2.json();
883							if (responseBody2.statusCode === 0)
884								tableBody.deleteRow(row.rowIndex - 1);
885						}
886					});
887				}
888
889				//const paymentsButton = document.createElement('button');
890				//paymentsButton.textContent = 'Payments';
891				//paymentsButton.classList.add('btn', 'btn-outline-primary', 'btn-sm', 'ms-2');
892				//actionCell.appendChild(paymentsButton);
893				//paymentsButton.addEventListener('click', async () => {
894				//	alog("Jump to payments " + project.id + " 13");
895				//	window.location.href = "payments.jsp?id=" + project.id;
896				//});
897
898				row.appendChild(actionCell);
899
900				tableBody.appendChild(row);
901				//				}
902			});
903		} else if (responseBody.statusCode === 3) {
904			deleteCookie('sid');
905			deleteCookie('name');
906			deleteCookie('email');
907			clearLocalStorage();
908			alog("Jump to login 14");
909			window.location.href = '../views/login.jsp';
910		}
911
912	} // else {
913	//		showAlert("The page could not be loaded!", "error");
914	//	}
915
916	createCopyToClipboard('copy_btn', 'script_content_area');
917}
918
919function createCopyToClipboard(btnid, contentid, displayinfo, tooltip) {
920	const copyBtn = document.getElementById(btnid);
921	// let tooltip = displayinfo ? null : new bootstrap.Tooltip(copyBtn);
922	copyBtn.addEventListener('click', function() {
923		const contentElement = document.getElementById(contentid);
924		if (contentElement) {
925			if (displayinfo) {
926				navigator.clipboard.writeText(contentElement.textContent).then(() => {
927					console.log("Copied the script to clipboard!");
928				});
929				var di = document.getElementById(displayinfo);
930				if (di) {
931					di.innerHTML = translations["copyCompleted"];
932				}
933			} else if (tooltip) {
934				const btnText = copyBtn.getAttribute('data-bs-original-title');
935				navigator.clipboard.writeText(contentElement.textContent).then(() => {
936					tooltip.dispose();
937					copyBtn.setAttribute('title', "✓ " + (translations['copied'] || "Copied"));
938					tooltip = new bootstrap.Tooltip(copyBtn);
939					tooltip.show();
940
941					setTimeout(() => {
942						tooltip.dispose();
943						copyBtn.setAttribute('title', btnText);
944						tooltip = new bootstrap.Tooltip(copyBtn);
945					}, 2000);
946				});
947			} else {
948				const btnText = copyBtn.textContent;
949				navigator.clipboard.writeText(contentElement.textContent).then(() => {
950					copyBtn.textContent = "✓ " + (translations['copied'] || "Copied");
951
952					setTimeout(() => {
953						copyBtn.textContent = btnText;
954					}, 2000);
955				});
956			}
957
958			// contentElement.focus();
959			// contentElement.setSelectionRange(0, 99999);
960			// setTimeout(function() {
961			// 	contentElement.setSelectionRange(0, 0);
962			// }, 200);
963		}
964	});
965
966	// if (!displayinfo) {
967	// 	const tooltip = new bootstrap.Tooltip(copyBtn);
968	//
969	// 	copyBtn.addEventListener('click', () => {
970	// 		// Metni kopyala
971	// 		const copyText = document.getElementById('copyText');
972	// 		navigator.clipboard.writeText(copyText.value).then(() => {
973	// 			// Tooltip mesajını değiştir
974	// 			copyBtn.setAttribute('data-bs-original-title', 'Copied');
975	// 			tooltip.show();
976	//
977	// 			// Bir süre sonra tekrar "Copy" mesajına dön
978	// 			setTimeout(() => {
979	// 				copyBtn.setAttribute('data-bs-original-title', 'Copy');
980	// 			}, 1500);
981	// 		});
982	// 	});
983	// }
984}
985
986function addLanguageSelector(primaryLanguage, availableLanguages) {
987	const langselector = document.getElementById('select_language');
988	availableLanguages.forEach(option1 => {
989		const optionx = document.createElement('option');
990		optionx.value = option1.shortCode;
991		optionx.text = option1.name;
992		//console.dir(primaryLanguage + " " + option1.shortCode);
993		if (primaryLanguage == option1.shortCode) {
994			optionx.selected = "selected";
995			optionx.checked = true;
996			langselector.value = optionx.shortCode;
997		}
998		langselector.appendChild(optionx);
999	});
1000}
1001
1002
1003////////////////////////////////////////////////////////////////////////////////////////
1004////////////////////////////////////////////////////////////////////////////////////////
1005////////////////////////////////////////////////////////////////////////////////////////
1006////////////////////////////////////////////////////////////////////////////////////////
1007////////////////////////////////////////////////////////////////////////////////////////
1008////////////////////////////////////////////////////////////////////////////////////////
1009////////////////////////////////////////////////////////////////////////////////////////
1010////////////////////////////////////////////////////////////////////////////////////////
1011////////////////////////////////////////////////////////////////////////////////////////
1012////////////////////////////////////////////////////////////////////////////////////////
1013////////////////////////////////////////////////////////////////////////////////////////
1014////////////////////////////////////////////////////////////////////////////////////////
1015////////////////////////////////////////////////////////////////////////////////////////
1016////////////////////////////////////////////////////////////////////////////////////////
1017////////////////////////////////////////////////////////////////////////////////////////
1018////////////////////////////////////////////////////////////////////////////////////////
1019////////////////////////////////////////////////////////////////////////////////////////
1020////////////////////////////////////////////////////////////////////////////////////////
1021
1022
1023
1024/* TEMPLATE SETTINGS */
1025
1026async function loadTemplateSettings() {
1027	const projectSettingsForm = document.getElementById('project_settings_form');
1028	if (!projectSettingsForm || checkIfLoginRequired())
1029		return;
1030
1031	const advertisingForm = document.getElementById('advertising_settings_form');
1032	const layoutForm = document.getElementById('layout_properties_form');
1033	const allForms = [projectSettingsForm, advertisingForm, layoutForm];
1034	const creatingForms = [projectSettingsForm, advertisingForm];
1035
1036	const params = new URLSearchParams(window.location.search);
1037	const projectId = params.get("id");
1038	var projectAccessKey = '';
1039	var serverAvailableLanguages = null;
1040
1041	// Activate tab from URL hash (e.g. #advertising, #layout, #geopreview) or ?tab= param
1042	var initialTab = window.location.hash ? window.location.hash.substring(1) : params.get("tab");
1043	if (initialTab) {
1044		var tabTriggerEl = document.querySelector('#settings_tabs button[data-bs-target="#' + initialTab + '"]');
1045		if (tabTriggerEl) {
1046			var tab = new bootstrap.Tab(tabTriggerEl);
1047			tab.show();
1048		}
1049	}
1050
1051	// Update URL hash when tabs change
1052	document.querySelectorAll('#settings_tabs button[data-bs-toggle="tab"]').forEach(function(btn) {
1053		btn.addEventListener('shown.bs.tab', function() {
1054			var target = btn.getAttribute('data-bs-target');
1055			if (target) history.replaceState(null, '', target);
1056		});
1057	});
1058
1059	const enabledCheckbox = document.getElementById('consent_enabled');
1060	enabledCheckbox.addEventListener('change', function() {
1061		const statusElement = document.getElementById('status_text');
1062		if (enabledCheckbox.checked) {
1063			statusElement.classList.remove('text-danger');
1064			statusElement.classList.add('text-success');
1065			setTextContentTranslationByElement(statusElement, 'enabled');
1066		} else {
1067			statusElement.classList.remove('text-success');
1068			statusElement.classList.add('text-danger');
1069			setTextContentTranslationByElement(statusElement, 'projectDisabledMessage');
1070		}
1071	});
1072
1073
1074	const usCheckbox = document.getElementById('advertising_us');
1075
1076
1077	const layoutPropertiesEnabled = document.getElementById('layout_properties_enabled');
1078	layoutPropertiesEnabled?.addEventListener('change', function () {
1079		const expandableContainer = document.getElementById('layout_properties_container');
1080		// expandableContainer.classList.toggle('expanded');
1081		for (const layoutPropertyContainer of expandableContainer.children) {
1082			for (const layoutPropertyElement of layoutPropertyContainer.children) {
1083				layoutPropertyElement.disabled = !layoutPropertiesEnabled.checked;
1084				// layoutPropertyElement.classList.toggle("text-muted");
1085				if (layoutPropertiesEnabled.checked)
1086					layoutPropertyElement.classList.remove("text-muted");
1087				else
1088					layoutPropertyElement.classList.add("text-muted");
1089			}
1090		}
1091	});
1092
1093	const statusSection = document.getElementById('status_section');
1094	const gcmUrlPassthrough = document.getElementById('gcm_url_passthrough');
1095	const gcmAdsDataRedaction = document.getElementById('gcm_ads_data_redaction');
1096	const gcmGoogleSignals = document.getElementById('gcm_google_signals');
1097	const gcmAdPersonalizationSignals = document.getElementById('gcm_ad_personalization_signals');
1098	const gcmRestrictedDataProcessing = document.getElementById('gcm_restricted_data_processing');
1099	const gcmSendPageView = document.getElementById('gcm_send_page_view');
1100	const gcmWaitForUpdate = document.getElementById('gcm_wait_for_update');
1101	// const additionalSection = document.getElementById('additional_section');
1102	const contentElement = document.getElementById('page_script');
1103
1104	//console.dir(projectId);
1105	if (projectId == null) { //Create new project
1106		addLanguageSelector("en", availConsentLanguages);
1107		// layoutPropertiesEnabled?.dispatchEvent(new Event('change'));
1108
1109		for (let i = 0; i < creatingForms.length; i++) {
1110			const buttonsContainer = document.createElement('div');
1111			buttonsContainer.classList.add('mt-2', 'd-flex', 'justify-content-end');
1112			if (i < creatingForms.length - 1) {
1113				buttonsContainer.innerHTML = '<button type="button" class="btn btn-primary btn-next w-lg mx-2">' + (translations['next'] || "Next") + '</button>';
1114				// if (i > 0)
1115				// 	buttonsContainer.innerHTML += '<button type="button" class="btn btn-back w-sm" onclick="">' + (translations['back'] || "Back") + '</button>';
1116			} else {
1117				const createButton = document.createElement('button');
1118				createButton.type = 'submit';
1119				createButton.classList.add('btn', 'btn-success', 'w-lg', 'mx-2');
1120				createButton.textContent = translations['create'] || "Create";
1121				buttonsContainer.appendChild(createButton);
1122				createButton.addEventListener('click', async () => {
1123					createButton.disabled = true;
1124					createButton.textContent = (translations['creating'] || "Creating") + '...';
1125					await createTemplateSettings();
1126				});
1127				// buttonsContainer.innerHTML = '<button type="button" class="btn btn-success w-lg mx-2">' + (translations['create'] || "Create") + '</button>';
1128				// buttonsContainer.innerHTML += '<button type="button" class="btn btn-back w-sm" onclick="">' + (translations['back'] || "Back") + '</button>';
1129			}
1130			creatingForms[i].appendChild(buttonsContainer);
1131		}
1132	} else { //Edit an existing project
1133		document.getElementById('layout_properties_section').classList.remove('d-none');
1134
1135		const data = { sid: getSid(), project: projectId };
1136		const response = await postRequest(endpoint + '/project/get', data);
1137		if (response.ok) {
1138			const responseBody = await response.json();
1139			if (responseBody.statusCode === 0) {
1140				projectSettingsForm.elements['name'].value = responseBody.project.projectName;
1141				const config = responseBody.config;
1142				projectAccessKey = config.accessKey;
1143				projectSettingsForm.elements['consent_enabled'].checked = config.cmpEnabled;
1144				enabledCheckbox.dispatchEvent(new Event('change'));
1145				// projectSettingsForm.elements['gdpr_applies_globally'].checked = config.cmpGdprAppliesGlobally;
1146
1147				addLanguageSelector(config.primaryLanguage, responseBody.availableLanguages);
1148				if (responseBody.availableLanguages)
1149					serverAvailableLanguages = responseBody.availableLanguages;
1150				projectSettingsForm.elements['use_client_info_for_language'].checked = config.useClientLanguage;
1151
1152				//Google / Microsoft Consent Mode — enablement is derived from per-region
1153				//site-data toggles; the sub-settings live in a collapsible body that
1154				//defaults to collapsed and is toggled by the user on demand.
1155				gcmUrlPassthrough.checked = config.cmpConsentModeV2.urlPassthrough;
1156				gcmAdsDataRedaction.checked = config.cmpConsentModeV2.adsDataRedaction;
1157				gcmGoogleSignals.checked = config.cmpConsentModeV2.googleSignals;
1158				gcmAdPersonalizationSignals.checked = config.cmpConsentModeV2.adPersonalizationSignals;
1159				gcmRestrictedDataProcessing.checked = config.cmpConsentModeV2.restrictedDataProcessing;
1160				gcmSendPageView.value = config.cmpConsentModeV2.sendPageView;
1161				gcmWaitForUpdate.value = config.cmpConsentModeV2.waitForUpdate;
1162
1163				projectSettingsForm.elements['auto_blocker'].checked = config.cmpAutoBlockerEnabled === "1";
1164				projectSettingsForm.elements['privacy_button'].checked = config.cmpDisplayConsentButton;
1165				if (projectSettingsForm.elements['cmp_show_reject_all_home'])
1166					projectSettingsForm.elements['cmp_show_reject_all_home'].checked = config.cmpShowRejectAllHome !== false;
1167				if (projectSettingsForm.elements['cmp_hide_empty_cookie_categories'])
1168					projectSettingsForm.elements['cmp_hide_empty_cookie_categories'].checked = !!config.cmpHideEmptyCookieCategories;
1169				// Default ON — matches the CMP_EMBED_PLACEHOLDER backend default.
1170				if (projectSettingsForm.elements['cmp_embed_placeholder'])
1171					projectSettingsForm.elements['cmp_embed_placeholder'].checked = config.cmpEmbedPlaceholder !== false;
1172				// Opt-in (default OFF) — matches the CMP_REGATE_ON_WITHDRAWAL backend default.
1173				// Note the polarity: `!!` not `!== false`, or an absent value would read as ON.
1174				if (projectSettingsForm.elements['cmp_regate_on_withdrawal'])
1175					projectSettingsForm.elements['cmp_regate_on_withdrawal'].checked = !!config.cmpRegateO
1175nWithdrawal;
1176				// Opt-in (default OFF) — matches the CMP_ACCESSIBILITY_WIDGET backend
1177				// default. `!!` not `!== false`, same polarity reasoning as above.
1178				if (projectSettingsForm.elements['cmp_accessibility_widget'])
1179					projectSettingsForm.elements['cmp_accessibility_widget'].checked = !!config.cmpAccessibilityWidget;
1180
1181				// Banner copy experiment. The server always sends a NORMALIZED definition,
1182				// so anything it could not validate arrives as {} and the form simply shows
1183				// a stopped test — the panel never has to re-implement the whitelist.
1184				fcLoadBannerText(config.cmpBannerText);
1185				fcLoadExperiment(config.cmpExperiment);
1186				// Results for the last 30 days, alongside the editor: a merchant deciding
1187				// whether to stop a test should not have to go and find the numbers.
1188				if (projectId) {
1189					const expEnd = new Date();
1190					const expStart = new Date(expEnd.getTime() - 29 * 24 * 3600 * 1000);
1191					// Local date parts, NOT toISOString(): that returns UTC, so east of UTC
1192					// every boundary shifts a day back — the same bug fixed in the trend
1193					// chart (verified there under Auckland and Los Angeles).
1194					const pad = (v) => String(v).padStart(2, '0');
1195					const iso = (d) => d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate());
1196					loadExperimentReport(projectId, iso(expStart), iso(expEnd));
1197				}
1198				// Privacy requests: the lookup is on demand, so only the button is wired.
1199				const privacyBtn = document.getElementById('privacy_lookup_btn');
1200				if (privacyBtn && !privacyBtn.dataset.fcBound) {
1201					privacyBtn.dataset.fcBound = '1';
1202					privacyBtn.addEventListener('click', fcPrivacyLookup);
1203					const privacyInput = document.getElementById('privacy_client_key');
1204					if (privacyInput)
1205						privacyInput.addEventListener('keydown', function (e) {
1206							if (e.key === 'Enter') { e.preventDefault(); fcPrivacyLookup(); }
1207						});
1208				}
1209				// Opt-in (default off) — matches the CMP_MONTHLY_SCAN_REPORT backend default.
1210				if (projectSettingsForm.elements['cmp_monthly_scan_report'])
1211					projectSettingsForm.elements['cmp_monthly_scan_report'].checked = !!config.cmpMonthlyScanReport;
1212
1213				contentElement.textContent = responseBody.pageCode;
1214
1215
1216				advertisingForm.elements['advertising_eu'].checked = config.cmpAdvertisingSettings.eu;
1217				advertisingForm.elements['advertising_us'].checked = config.cmpAdvertisingSettings.us;
1218				usCheckbox.dispatchEvent(new Event('change'));
1219				advertisingForm.elements['advertising_ca'].checked = config.cmpAdvertisingSettings.ca;
1220				['tr','br','za','jp','ch','au','kr','th','in'].forEach(function(r){
1221					const el = advertisingForm.elements['advertising_' + r];
1222					if (el) el.checked = !!config.cmpAdvertisingSettings[r];
1223				});
1224				advertisingForm.elements['advertising_others'].checked = config.cmpAdvertisingSettings.others;
1225
1226				const regulations = (config.cmpAdvertisingSettings && config.cmpAdvertisingSettings.regulations) || {};
1227				const regUsToggle = document.getElementById('regulation_enable_us');
1228				if (regUsToggle) regUsToggle.checked = regulations.us !== 0;
1229
1230				loadSiteDataConsent(config.cmpAdvertisingSettings && config.cmpAdvertisingSettings.siteDataConsent);
1231				loadQuebecRows(config.cmpAdvertisingSettings);
1232
1233				loadUsSectionPreferences(config['cmpSectionPreferences']);
1234
1235
1236				const customCss = config.customCss;
1237				layoutPropertiesEnabled.checked = customCss.enabled;
1238				layoutPropertiesEnabled.dispatchEvent(new Event('change'));
1239				layoutForm.elements['font_family'].value = customCss.fontFamily || "";
1240				layoutForm.elements['font_size'].value = customCss.fontSize || "";
1241				layoutForm.elements['custom_css'].value = customCss.css || "";
1242				layoutForm.elements['custom_logo'].value = customCss.customLogo || "";
1243				layoutForm.elements['custom_icon'].value = customCss.customIcon || "";
1244				layoutForm.elements['home_modal_theme'].value = customCss.homeModalTheme || customCss.homeModalTheme || "default";
1245				layoutForm.elements['home_modal_location'].value = customCss.homeModalLocation || customCss.homeModalPlace || "center";
1246				layoutForm.elements['page_interaction'].checked = customCss.allowPageInteraction || false;
1247
1248				await previewChanged();
1249				const previewFrame = document.getElementById('preview_frame');
1250				// previewFrame.onload = function () {
1251				// 	const screenWidth = screen.width;
1252				// 	// const docWidth = document.documentElement.offsetWidth;
1253				// 	// const homeModal = previewFrame.contentWindow.document.querySelector('#rfCmpHomeModal');
1254				// 	// const homeModalDialog = homeModal.querySelector('.rf-cmp-modal-dialog');
1255				// 	// const homeModalContent = homeModal.querySelector('.rf-cmp-modal-content');
1256				// 	// const homeModalContentWidth = homeModalContent.offsetWidth;
1257				// 	// const maxWidth = 640;
1258				// 	// // const maxWidth = window.getComputedStyle(homeModalDialog).getPropertyValue('max-width');
1259				// 	const previewWidth = previewFrame.offsetWidth;
1260				// 	// const previewBodyWidth = previewFrame.contentWindow.document.body.offsetWidth;
1261				//
1262				// 	// const ratio = maxWidth / screenWidth;
1263				// 	// previewFrame.contentWindow.document.body.style.zoom = ratio;
1264				//
1265				// 	const ratio = previewWidth / screenWidth;
1266				// 	previewFrame.style.zoom = ratio;
1267				// }
1268
1269				// previewFrame.style.zoom = previewFrame.offsetWidth / screen.width;
1270				// Populate domain selects and load preview
1271				var projectDomains = [];
1272				var fallbackDomain = window.location.hostname;
1273
1274				function populateDomainSelect(selectId, domains) {
1275					var sel = document.getElementById(selectId);
1276					if (!sel) return;
1277					sel.innerHTML = '';
1278					if (!domains || domains.length === 0) {
1279						var opt = document.createElement('option');
1280						opt.value = '0';
1281						opt.textContent = fallbackDomain;
1282						sel.appendChild(opt);
1283					} else {
1284						domains.forEach(function(d) {
1285							var opt = document.createElement('option');
1286							opt.value = d.id || '0';
1287							opt.textContent = d.domainName;
1288							sel.appendChild(opt);
1289						});
1290					}
1291				}
1292
1293				function getPreviewUrl(domainId) {
1294					return "../views/consent_preview.jsp?pid=" + projectId + "&key=" + config.accessKey + "&domainId=" + domainId + "&rand=" + Math.random();
1295				}
1296
1297				// Load domains for this project from the domain list API
1298				postRequest(endpoint + '/project-domain/list', { sid: getSid(), projectTypeId: 2 })
1299					.then(function(r) { return r.json(); })
1300					.then(function(body) {
1301						if (body.statusCode === 0 && body.domains) {
1302							// Filter domains belonging to this project
1303							projectDomains = body.domains.filter(function(d) {
1304								return d.project && String(d.project.id) === String(projectId);
1305							});
1306							// If no domains for this project, use all user domains
1307							if (projectDomains.length === 0)
1308								projectDomains = body.domains;
1309						}
1310						populateDomainSelect('preview_domain_select', projectDomains);
1311						populateDomainSelect('geo_preview_domain_select', projectDomains);
1312						var sel = document.getElementById('preview_domain_select');
1313						if (sel) previewFrame.src = getPreviewUrl(sel.value);
1314					}).catch(function() {
1315						populateDomainSelect('preview_domain_select', []);
1316						populateDomainSelect('geo_preview_domain_select', []);
1317						previewFrame.src = getPreviewUrl('0');
1318					});
1319
1320				var previewDomainSelect = document.getElementById('preview_domain_select');
1321				if (previewDomainSelect) {
1322					previewDomainSelect.onchange = function() {
1323						previewFrame.src = getPreviewUrl(previewDomainSelect.value);
1324					};
1325				}
1326
1327				var geoDomainSelect = document.getElementById('geo_preview_domain_select');
1328				if (geoDomainSelect) {
1329					geoDomainSelect.onchange = function() {
1330						var geoFrame = document.getElementById('geo_preview_frame');
1331						if (geoFrame && geoFrame.src) {
1332							var url = new URL(geoFrame.src, window.location.href);
1333							url.searchParams.set('domainId', geoDomainSelect.value);
1334							url.searchParams.set('rand', Math.random());
1335							geoFrame.src = url.toString();
1336						}
1337					};
1338				}
1339
1340
1341				const previewNewTabBtn = document.getElementById('btn_preview_new_tab');
1342				previewNewTabBtn.onclick = function (event) {
1343					event.preventDefault();
1344					window.open(previewFrame.src, '_blank');
1345				};
1346
1347				allForms.forEach(form => {
1348					const buttonsContainer = document.createElement('div');
1349					buttonsContainer.classList.add('d-flex', 'justify-content-end', 'mb-5', 'mb-md-0');
1350					buttonsContainer.innerHTML = '<button type="submit" class="btn btn-success w-lg mx-2">' + (translations['save'] || "Save") + '</button>';
1351					buttonsContainer.innerHTML += '<button type="button" class="btn btn-cancel w-sm" onclick="history.back()">' + (translations['cancel'] || "Cancel") + '</button>';
1352					form.appendChild(buttonsContainer);
1353				});
1354			} else {
1355				console.log('Loading Error Code: ' + responseBody.statusCode);
1356				showAlert("The page could not be loaded!", "error");
1357			}
1358
1359			Array.prototype.forEach.call(document.getElementsByClassName('btn-success'), function(element) {
1360				element.removeAttribute('disabled');
1361			});
1362		}
1363	}
1364
1365	//General Tab
1366	// const gcmSubmenu = document.getElementById('gcm_submenu');
1367	// if (projectSettingsForm.elements['google_consent_mode'].checked)
1368	// 	gcmSubmenu.classList.add('expanded');
1369
1370	// Google/Microsoft Consent Mode sub-settings live inside a collapsible body;
1371	// the header toggles the body's `expanded` class. Default is collapsed — these
1372	// are advanced flags most publishers leave at their sensible defaults.
1373	const gcmSubmenuHeader = document.getElementById('gcm_submenu_header');
1374	const gcmSubmenuBody = document.getElementById('gcm_submenu_body');
1375	const gcmSubmenuChevron = document.getElementById('gcm_submenu_chevron');
1376	if (gcmSubmenuHeader && gcmSubmenuBody && gcmSubmenuChevron) {
1377		gcmSubmenuHeader.addEventListener('click', function() {
1378			gcmSubmenuBody.classList.toggle('expanded');
1379			gcmSubmenuChevron.innerHTML = gcmSubmenuBody.classList.contains('expanded')
1380				? '<i class="fa-solid fa-chevron-down"></i>'
1381				: '<i class="fa-solid fa-chevron-right"></i>';
1382		});
1383	}
1384
1385	const triggerCustomization = document.getElementById('trigger_customization');
1386	const chevronParent = triggerCustomization.lastElementChild;
1387	triggerCustomization.onclick = function() {
1388		const nextElement = this.nextElementSibling;
1389		nextElement.classList.toggle('expanded');
1390
1391		if (nextElement.classList.contains('expanded'))
1392			chevronParent.innerHTML = '<i class="fa-solid fa-chevron-down"></i>';
1393		else
1394			chevronParent.innerHTML = '<i class="fa-solid fa-chevron-right"></i>';
1395	}
1396
1397	const privacyHeader = document.getElementById('privacy_trigger_header');
1398	const privacyBody = document.getElementById('privacy_trigger_body');
1399	const privacyChevron = document.getElementById('privacy_trigger_chevron');
1400	if (privacyHeader && privacyBody && privacyChevron) {
1401		privacyHeader.addEventListener('click', function() {
1402			privacyBody.classList.toggle('expanded');
1403			privacyChevron.innerHTML = privacyBody.classList.contains('expanded')
1404				? '<i class="fa-solid fa-chevron-down"></i>'
1405				: '<i class="fa-solid fa-chevron-right"></i>';
1406		});
1407	}
1408
1409	function wireCollapsible(headerId, bodyId, chevronId) {
1410		const header = document.getElementById(headerId);
1411		const body = document.getElementById(bodyId);
1412		const chevron = document.getElementById(chevronId);
1413		if (!header || !body || !chevron) return;
1414		header.addEventListener('click', function() {
1415			body.classList.toggle('expanded');
1416			chevron.innerHTML = body.classList.contains('expanded')
1417				? '<i class="fa-solid fa-chevron-down"></i>'
1418				: '<i class="fa-solid fa-chevron-right"></i>';
1419		});
1420	}
1421
1422	wireCollapsible('section_preferences_header', 'section_preferences_body', 'section_preferences_chevron');
1423	wireCollapsible('gcm_section_header', 'gcm_section_body', 'gcm_section_chevron');
1424	wireCollapsible('advertising_section_header', 'advertising_section_body', 'advertising_section_chevron');
1425
1426
1427	//Advertising Tab
1428
1429
1430	//Layout Tab
1431	async function previewChanged(event) {
1432		const cmpDisplayPrivacyButton = document.getElementById('privacy_button').checked;
1433
1434		const previewSettings = {
1435			useClientLanguage: document.getElementById('use_client_info_for_language').checked,
1436			primaryLanguage: document.getElementById('select_language').value,
1437			cmpConsentModeV2: {
1438				enabled: isAnySiteDataEnabled(),
1439				urlPassthrough: document.getElementById('gcm_url_passthrough').checked,
1440				adsDataRedaction: document.getElementById('gcm_ads_data_redaction').checked,
1441				googleSignals: document.getElementById('gcm_google_signals').checked,
1442				adPersonalizationSignals: document.getElementById('gcm_ad_personalization_signals').checked,
1443				restrictedDataProcessing: document.getElementById('gcm_restricted_data_processing').checked,
1444				sendPageView: document.getElementById('gcm_send_page_view').value !== 'false',
1445				waitForUpdate: parseInt(document.getElementById('gcm_wait_for_update').value, 10) || 1000
1446			},
1447			cmpAutoBlockerEnabled: document.getElementById('auto_blocker').checked,
1448			cmpDisplayConsentButton: cmpDisplayPrivacyButton,
1449			cmpShowRejectAllHome: document.getElementById('cmp_show_reject_all_home')
1450				? document.getElementById('cmp_show_reject_all_home').checked : true,
1451			cmpHideEmptyCookieCategories: !!(document.getElementById('cmp_hide_empty_cookie_categories')
1452				&& document.getElementById('cmp_hide_empty_cookie_categories').checked),
1453
1454			enabled: document.getElementById('layout_properties_enabled').checked,
1455			fontFamily: document.getElementById('font_family').value,
1456			fontSize: document.getElementById('font_size').value,
1457			css: document.getElementById('custom_css').value,
1458			customLogo: document.getElementById('custom_logo').value,
1459			customIcon: document.getElementById('custom_icon').value,
1460			homeModalTheme: document.getElementById('home_modal_theme').value,
1461			homeModalLocation: document.getElementById('home_modal_location').value,
1462			allowPageInteraction: document.getElementById('page_interaction').checked
1463		};
1464
1465		const data = {sid: getSid(), project: projectId, previewSettings: previewSettings};
1466		const response = await postRequest(endpoint + '/project/preview', data);
1467		if (response.ok) {
1468			const responseBody = await response.json();
1469			if (responseBody.statusCode === 0 && event) {
1470				const previewFrame = document.getElementById('preview_frame');
1471				previewFrame.contentWindow.location.reload();
1472			}
1473		}
1474	}
1475
1476	if (projectId) {
1477		layoutPropertiesEnabled.addEventListener('change', previewChanged);
1478		document.getElementById('font_family').addEventListener('change', previewChanged);
1479		document.getElementById('font_size').addEventListener('change', previewChanged);
1480		document.getElementById('custom_css').addEventListener('change', previewChanged);
1481		document.getElementById('custom_logo').addEventListener('change', previewChanged);
1482		document.getElementById('custom_icon').addEventListener('change', previewChanged);
1483		document.getElementById('home_modal_theme').addEventListener('change', previewChanged);
1484		document.getElementById('home_modal_location').addEventListener('change', previewChanged);
1485		document.getElementById('page_interaction').addEventListener('change', previewChanged);
1486	}
1487
1488	// ===================== Geo Preview Tab =====================
1489	if (projectId) {
1490		const geoGroupSelect = document.getElementById('geo_preview_group');
1491		const geoCountrySelect = document.getElementById('geo_preview_country');
1492		const geoStateSelect = document.getElementById('geo_preview_state');
1493		const geoLangSelect = document.getElementById('geo_preview_lang');
1494		const geoCountryContainer = document.getElementById('geo_preview_country_container');
1495		const geoStateContainer = document.getElementById('geo_preview_state_container');
1496		const geoInfoCard = document.getElementById('geo_preview_info');
1497		const geoInfoContent = document.getElementById('geo_preview_info_content');
1498		const geoPreviewFrame = document.getElementById('geo_preview_frame');
1499		let geoPreviewLoaded = false;
1500
1501		// Available CMP languages (populated from server response)
1502		const GEO_LANGUAGES = [
1503			{code:'en',name:'English'},{code:'fr',name:'Français'},{code:'de',name:'Deutsch'},
1504			{code:'es',name:'Español'},{code:'it',name:'Italiano'},{code:'pt',name:'Português'},
1505			{code:'nl',name:'Nederlands'},{code:'pl',name:'Polski'},{code:'cs',name:'Čeština'},
1506			{code:'da',name:'Dansk'},{code:'sv',name:'Svenska'},{code:'fi',name:'Suomi'},
1507			{code:'el',name:'Ελληνικά'},{code:'hu',name:'Magyar'},{code:'ro',name:'Română'},
1508			{code:'sk',name:'Slovenčina'},{code:'sl',name:'Slovenščina'},{code:'bg',name:'Български'},
1509			{code:'hr',name:'Hrvatski'},{code:'et',name:'Eesti'},{code:'lv',name:'Latviešu'},
1510			{code:'lt',name:'Lietuvių'},{code:'mt',name:'Malti'},{code:'ga',name:'Gaeilge'},
1511			{code:'tr',name:'Türkçe'},{code:'ja',name:'日本語'},{code:'zh',name:'中文'},
1512			{code:'ko',name:'한국어'},{code:'ar',name:'العربية'},{code:'he',name:'עברית'},
1513			{code:'ru',name:'Русский'},{code:'uk',name:'Українська'},{code:'no',name:'Norsk'}
1514		];
1515
1516		function populateGeoLanguages() {
1517			var langs = serverAvailableLanguages || GEO_LANGUAGES;
1518			geoLangSelect.innerHTML = '<option value="">' + (translations['geoPreviewLangAuto'] || 'Auto (use browser default)') + '</option>';
1519			langs.forEach(function(l) {
1520				var code = l.code || l.shortCode || l;
1521				var name = l.name || code;
1522				var opt = document.createElement('option');
1523				opt.value = code;
1524				opt.textContent = name + ' (' + code + ')';
1525				geoLangSelect.appendChild(opt);
1526			});
1527		}
1528		populateGeoLanguages();
1529
1530		// Region/country definitions
1531		const EU_COUNTRIES = {
1532			'at':'Austria','be':'Belgium','bg':'Bulgaria','hr':'Croatia','cy':'Cyprus',
1533			'cz':'Czechia','dk':'Denmark','ee':'Estonia','fi':'Finland','fr':'France',
1534			'de':'Germany','gr':'Greece','hu':'Hungary','ie':'Ireland','it':'Italy',
1535			'lv':'Latvia','lt':'Lithuania','lu':'Luxembourg','mt':'Malta','nl':'Netherlands',
1536			'pl':'Poland','pt':'Portugal','ro':'Romania','sk':'Slovakia','si':'Slovenia',
1537			'es':'Spain','se':'Sweden','gb':'United Kingdom','no':'Norway','is':'Iceland',
1538			'li':'Liechtenstein','ch':'Switzerland'
1539		};
1540		const US_STATES = {
1541			'ca':'California','va':'Virginia','co':'Colorado','ct':'Connecticut',
1542			'ut':'Utah','tx':'Texas','or':'Oregon','mt':'Montana','de':'Delaware',
1543			'ia':'Iowa','nh':'New Hampshire','nj':'New Jersey','tn':'Tennessee',
1544			'mn':'Minnesota','md':'Maryland','ky':'Kentucky','ri':'Rhode Island',
1545			'fl':'Florida','ne':'Nebraska','in':'Indiana','ny':'New York','il':'Illinois',
1546			'pa':'Pennsylvania','oh':'Ohio','ga':'Georgia','nc':'North Carolina',
1547			'mi':'Michigan','wa':'Washington','az':'Arizona','ma':'Massachusetts'
1548		};
1549		// States with dedicated server-side implementation (state-specific generator
1550		// and/or GPP section). California gets CCPA-specific purposes (generateCcpaPurposes,
1551		// gppUsCa section); the rest share generateUsNatPurposes + gppUsNat. The
1552		// "Other" entry falls through to usnat on the server (stateCode doesn't match
1553		// any state flag → isUsNat=true).
1554		const US_STATES_IMPLEMENTED = {
1555			'':    'US National (usnat — MSPA catch-all)',
1556			'ca':  'California (CCPA)',
1557			'va':  'Virginia (VCDPA)',
1558			'co':  'Colorado (CPA)',
1559			'ut':  'Utah (UCPA)',
1560			'ct':  'Connecticut (CTDPA)',
1561			'tx':  'Texas (TDPSA)',
1562			'or':  'Oregon (OCPA)',
1563			'mt':  'Montana (MCDPA)',
1564			'de':  'Delaware (DPDPA)',
1565			'ia':  'Iowa (ICDPA)',
1566			'nh':  'New Hampshire (SB255)',
1567			'nj':  'New Jersey (NJDPA)',
1568			'tn':  'Tennessee (TIPA)',
1569			'mn':  'Minnesota (MCDPA)',
1570			'ne':  'Nebraska (NDPA)',
1571			'fl':  'Florida (FDBR)',
1572			'other': 'Other US state (falls through to usnat)'
1573		};
1574		const US_STATES_IMPLEMENTED_ORDER = ['', 'ca', 'va', 'co', 'ut', 'ct', 'tx', 'or', 'mt', 'de', 'ia', 'nh', 'nj', 'tn', 'mn', 'ne', 'fl', 'other'];
1575		const GPP_OPT_OUT_STATES = ['ca','va','co','ct','ut','tx','or','mt','de','ia','nh','nj','tn','mn','md','ky','ri','fl','ne','in'];
1576
1577		// Regions that produce identical TCF EU v2 encoding when advertising is
1578		// enabled. Canada is excluded — it adds the GPP CA section and gets its
1579		// own group. US is state-aware and stays separate too.
1580		const TCF_REGION_COUNTRIES = {
1581			'eu': EU_COUNTRIES,
1582			'br': {'br':'Brazil'},
1583			'za': {'za':'South Africa'},
1584			'jp': {'jp':'Japan'},
1585			'ch': {'ch':'Switzerland'},
1586			'au': {'au':'Australia'},
1587			'kr': {'kr':'South Korea'},
1588			'th': {'th':'Thailand'},
1589			'in': {'in':'India'},
1590			'tr': {'tr':'Turkey'}
1591		};
1592		const TCF_REGION_LABELS = {
1593			'eu':'EU/EEA','br':'Brazil','za':'South Africa','jp':'Japan',
1594			'ch':'Switzerland','au':'Australia','kr':'South Korea','th':'Thailand',
1595			'in':'India','tr':'Turkey'
1596		};
1597		// Sample countries for the "others" catch-all bucket.
1598		const OTHERS_SAMPLE_COUNTRIES = {
1599			'mx':'Mexico','sg':'Singapore','ng':'Nigeria','ar':'Argentina','id':'Indonesia'
1600		};
1601
1602		// Build regulation groups based on publisher's advertising settings.
1603		// Regions with identical regulation (same TCF EU v2 encoding) are merged
1604		// into a single group — the country dropdown picks a specific country.
1605		function buildGeoGroups() {
1606			function advChecked(code) {
1607				var el = document.getElementById('advertising_' + code);
1608				return !!(el && el.checked);
1609			}
1610			function siteDataChecked(code) {
1611				var el = document.getElementById('site_data_enable_' + code);
1612				return !!(el && el.checked);
1613			}
1614
1615			syncQuebecRows();
1616			const advUs = advChecked('us');
1617			const advCa = advChecked('ca');
1618			const advQc = advChecked('qc');
1619			const advOthers = advChecked('others');
1620			const gcmCa = siteDataChecked('ca');
1621			const gcmQc = siteDataChecked('qc');
1622			const gcmOthers = siteDataChecked('others');
1623
1624			const groups = [];
1625
1626			// Short translation lookup: panel translations live on `translations`
1627			// (populated from /consent/panel/tj/*.json). Every user-facing
1628			// geo-preview string goes through t() so localisation picks it up;
1629			// the hard-coded English fallback survives if the translation file
1630			// is missing a key.
1631			function t(key, fallback) {
1632				return (typeof translations !== "undefined" && translations && translations[key]) || fallback;
1633			}
1634
1635			// 1. TCF EU v2 groups, split by geography. All five produce identical
1636			// TCF EU v2 encoding on the server but a narrower country picker per
1637			// group keeps the preview usable — a combined group with 30+
1638			// countries was cumbersome and (when the publisher hadn't saved
1639			// advertising changes) masked which specific region's setting was
1640			// driving the missing purposes. Each sub-group shows only countries
1641			// whose advertising flag is actually on.
1642			function tcfSubgroup(id, label, description, countries, defaultCountry) {
1643				if (!Object.keys(countries).length) return;
1644				groups.push({
1645					id: id,
1646					label: label,
1647					countries: countries,
1648					info: '<b>' + t('geoPreviewTcfEuV2Header', 'TCF EU v2') + ' — ' + description + '</b><br>'
1649						+ t('geoPreviewTcfInfoBlockingModal', 'Blocking modal, explicit consent required.') + '<br>'
1650						+ t('geoPreviewTcfInfoIdentical', 'Produces a tcfeuv2 GPP section; every country in this group encodes identically.'),
1651					defaultCountry: countries[defaultCountry] ? defaultCountry : Object.keys(countries)[0],
1652					defaultState: ''
1653				});
1654			}
1655
1656			// 1a. Europe/EEA & Switzerland — GDPR + FADP. EU_COUNTRIES already
1657			// includes Switzerland, so advertising_eu=1 covers the whole bloc.
1658			// If only advertising_ch=1 (Switzerland standalone) we still show
1659			// just the CH row.
1660			var euGroupCountries = {};
1661			if (advChecked('eu')) Object.assign(euGroupCountries, EU_COUNTRIES);
1662			else if (advChecked('ch')) Object.assign(euGroupCountries, {'ch':'Switzerland'});
1663			tcfSubgroup('tcf_eu',
1664				t('geoPreviewGroupEu', 'Europe/EEA & Switzerland'),
1665				t('geoPreviewGroupEuDesc', 'GDPR / FADP'),
1666				euGroupCountries, 'de');
1667
1668			// 1b. Asia-Pacific — Japan, South Korea, Thailand, India, Australia.
1669			var apacCountries = {};
1670			if (advChecked('jp')) apacCountries['jp'] = 'Japan';
1671			if (advChecked('kr')) apacCountries['kr'] = 'South Korea';
1672			if (advChecked('th')) apacCountries['th'] = 'Thailand';
1673			if (advChecked('in')) apacCountries['in'] = 'India';
1674			if (advChecked('au')) apacCountries['au'] = 'Australia';
1675			tcfSubgroup('tcf_apac',
1676				t('geoPreviewGroupApac', 'Asia-Pacific'),
1677				t('geoPreviewGroupApacDesc', 'APPI / PIPA / PDPA / DPDP / Privacy Act'),
1678				apacCountries, 'jp');
1679
1680			// 1c. Americas — Brazil plus sample Latin-American catch-all
1681			// countries when advertising_others=1.
1682			var americasCountries = {};
1683			if (advChecked('br')) americasCountries['br'] = 'Brazil';
1684			if (advOthers) { americasCountries['mx'] = 'Mexico'; americasCountries['ar'] = 'Argentina'; }
1685			tcfSubgroup('tcf_americas',
1686				t('geoPreviewGroupAmericas', 'Americas'),
1687				t('geoPreviewGroupAmericasDesc', 'LGPD + publisher-enabled global TCF'),
1688				americasCountries, 'br');
1689
1690			// 1d. Africa, Middle East & Turkey — South Africa, Turkey, plus a
1691			// sample African catch-all when advertising_others=1.
1692			var meaCountries = {};
1693			if (advChecked('za')) meaCountries['za'] = 'South Africa';
1694			if (advChecked('tr')) meaCountries['tr'] = 'Turkey';
1695			if (advOthers) meaCountries['ng'] = 'Nigeria';
1696			tcfSubgroup('tcf_africa_me',
1697				t('geoPreviewGroupAfricaMe', 'Africa, Middle East & Turkey'),
1698				t('geoPreviewGroupAfricaMeDesc', 'POPIA / KVKK + publisher-enabled global TCF'),
1699				meaCountries, 'za');
1700
1701			// 1e. Rest of World — only the "others" catch-all countries that
1702			// aren't already covered by one of the regional buckets above.
1703			var rowCountries = {};
1704			if (advOthers) { rowCountries['sg'] = 'Singapore'; rowCountries['id'] = 'Indonesia'; }
1705			tcfSubgroup('tcf_row',
1706				t('geoPreviewGroupRow', 'Rest of World'),
1707				t('geoPreviewGroupRowDesc', 'Publisher-enabled global TCF'),
1708				rowCountries, 'sg');
1709
1710			// 2. United States — state-aware, distinct from every other region.
1711			groups.push({
1712				id: 'us',
1713				label: advUs
1714					? t('geoPreviewGroupUsDual', 'United States (TCF + GPP)')
1715					: t('geoPreviewGroupUsGpp', 'United States (GPP opt-outs)'),
1716				countries: {'us': t('geoPreviewCountryUs', 'United States')},
1717				states: US_STATES_IMPLEMENTED,
1718				stateOrder: US_STATES_IMPLEMENTED_ORDER,
1719				info: advUs
1720					? '<b>' + t('geoPreviewUsDualHeader', 'TCF EU v2 + GPP US sections') + '</b> — '
1721						+ t('geoPreviewUsDualInfo', 'Blocking TCF modal on auto-show plus GPP opt-outs accessible via Manage Privacy Settings.')
1722						+ '<br>' + t('geoPreviewUsRouting', 'California → gppUsCa. VA/CO/UT/CT/usnat/Other → gppUsNat.')
1723					: '<b>' + t('geoPreviewUsGppHeader', 'GPP US state opt-outs') + '</b> — '
1724						+ t('geoPreviewUsGppInfo', 'Non-blocking, opt-out model. California uses the gppUsCa section (CCPA); every other implemented state and the "Other" catch-all encode via gppUsNat.'),
1725				defaultCountry: 'us', defaultState: ''
1726			});
1727
1728			// 3. Canada — TCF + GPP CA only when advertising_ca=1 (unique regulation).
1729			if (advCa) {
1730				groups.push({
1731					id: 'ca_tcf',
1732					label: t('geoPreviewGroupCanada', 'Canada — TCF + GPP TCF-CA'),
1733					countries: {'ca': t('geoPreviewCountryCa', 'Canada')},
1734					info: '<b>' + t('geoPreviewCanadaHeader', 'TCF EU v2 + GPP TCF-CA (Section 5)') + '</b> — '
1735						+ t('geoPreviewCanadaInfo', 'Blocking modal.')
1736						+ '<br>' + t('geoPreviewCanadaInfo2', 'Both TCF string and GPP CA section generated.'),
1737					defaultCountry: 'ca', defaultState: ''
1738				});
1739			}
1740			// 3b. Quebec (J129): the same Canadian regime by its own rows; previewed as a visitor in Quebec (gs=qc), who also
1741			// gets French first when the browser lists it.
1742			const QUEBEC_STATES = {'qc': t('geoPreviewCountryQc', 'Quebec')};
1743			if (advQc) {
1744				groups.push({
1745					id: 'qc_tcf',
1746					label: t('geoPreviewGroupQuebec', 'Quebec — TCF + GPP TCF-CA'),
1747					countries: {'ca': t('geoPreviewCountryCa', 'Canada')},
1748					states: QUEBEC_STATES,
1749					info: '<b>' + t('geoPreviewCanadaHeader', 'TCF EU v2 + GPP TCF-CA (Section 5)') + '</b> — '
1750						+ t('geoPreviewCanadaInfo', 'Blocking modal.')
1751						+ '<br>' + t('geoPreviewCanadaInfo2', 'Both TCF string and GPP CA section generated.'),
1752					defaultCountry: 'ca', defaultState: 'qc'
1753				});
1754			}
1755
1756			// 4. Site-data-only regions — one dropdown entry per contributing
1757			// region, not a single group with every region concatenated into
1758			// the label. A <select> doesn't wrap option text, so a combined
1759			// label like "Site data consent only — EU/EEA, Brazil, South
1760			// Africa, Japan, Switzerland, Australia, South Korea, Thailand,
1761			// India, Turkey, Canada, Rest of World" used to overflow. Each
1762			// region gets its own short, readable option; clicking one loads
1763			// the preview for just that region's visitor.
1764			var siteDataRegions = [];
1765			var siteDataPrefix = t('geoPreviewSiteDataOnlyPrefix', 'Site data consent only — ');
1766			Object.keys(TCF_REGION_COUNTRIES).forEach(function(code) {
1767				if (!advChecked(code) && siteDataChecked(code)) {
1768					siteDataRegions.push({
1769						id: 'sitedata_only_' + code,
1770						label: siteDataPrefix + t('geoPreviewRegion_' + code, TCF_REGION_LABELS[code]),
1771						countries: TCF_REGION_COUNTRIES[code],
1772						defaultCountry: Object.keys(TCF_REGION_COUNTRIES[code])[0]
1773					});
1774				}
1775			});
1776			if (!advCa && gcmCa) {
1777				siteDataRegions.push({
1778					id: 'sitedata_only_ca',
1779					label: siteDataPrefix + t('geoPreviewCountryCa', 'Canada'),
1780					countries: {'ca': t('geoPreviewCountryCa', 'Canada')},
1781					defaultCountry: 'ca'
1782				});
1783			}
1784			if (!advQc && gcmQc) {
1785				siteDataRegions.push({
1786					id: 'sitedata_only_qc',
1787					label: siteDataPrefix + t('geoPreviewCountryQc', 'Quebec'),
1788					countries: {'ca': t('geoPreviewCountryCa', 'Canada')},
1789					states: QUEBEC_STATES, defaultState: 'qc',
1790					defaultCountry: 'ca'
1791				});
1792			}
1793			if (!advOthers && gcmOthers) {
1794				siteDataRegions.push({
1795					id: 'sitedata_only_others',
1796					label: siteDataPrefix + t('geoPreviewGroupRow', 'Rest of World'),
1797					countries: OTHERS_SAMPLE_COUNTRIES,
1798					defaultCountry: Object.keys(OTHERS_SAMPLE_COUNTRIES)[0]
1799				});
1800			}
1801			siteDataRegions.forEach(function(r) {
1802				groups.push({
1803					id: r.id,
1804					label: r.label,
1805					countries: r.countries,
1806					info: '<b>' + t('geoPreviewSiteDataHeader', 'Site data consent') + '</b> — '
1807						+ t('geoPreviewSiteDataInfo', 'Cookie categories (essential, analytics, marketing).')
1808						+ '<br>' + t('geoPreviewSiteDataInfo2', 'No IAB TCF/GPP. Only site-specific cookie management.'),
1809					states: r.states,
1810					defaultCountry: r.defaultCountry, defaultState: r.defaultState || ''
1811				});
1812			});
1813
1814			// 5. No-consent catch-all — same per-region-per-entry treatment.
1815			// Lets the publisher click "No consent popup — Japan" and see
1816			// that specific visitor without scanning a long compound label.
1817			var noConsentRegions = [];
1818			var noConsentPrefix = t('geoPreviewNoConsentPrefix', 'No consent popup — ');
1819			Object.keys(TCF_REGION_COUNTRIES).forEach(function(code) {
1820				if (!advChecked(code) && !siteDataChecked(code)) {
1821					noConsentRegions.push({
1822						id: 'no_consent_' + code,
1823						label: noConsentPrefix + t('geoPreviewRegion_' + code, TCF_REGION_LABELS[code]),
1824						countries: TCF_REGION_COUNTRIES[code],
1825						defaultCountry: Object.keys(TCF_REGION_COUNTRIES[code])[0]
1826					});
1827				}
1828			});
1829			if (!advCa && !gcmCa) {
1830				noConsentRegions.push({
1831					id: 'no_consent_ca',
1832					label: noConsentPrefix + t('geoPreviewCountryCa', 'Canada'),
1833					countries: {'ca': t('geoPreviewCountryCa', 'Canada')},
1834					defaultCountry: 'ca'
1835				});
1836			}
1837			if (!advQc && !gcmQc) {
1838				noConsentRegions.push({
1839					id: 'no_consent_qc',
1840					label: noConsentPrefix + t('geoPreviewCountryQc', 'Quebec'),
1841					countries: {'ca': t('geoPreviewCountryCa', 'Canada')},
1842					states: QUEBEC_STATES, defaultState: 'qc',
1843					defaultCountry: 'ca'
1844				});
1845			}
1846			if (!advOthers && !gcmOthers) {
1847				noConsentRegions.push({
1848					id: 'no_consent_others',
1849					label: noConsentPrefix + t('geoPreviewGroupRow', 'Rest of World'),
1850					countries: OTHERS_SAMPLE_COUNTRIES,
1851					defaultCountry: Object.keys(OTHERS_SAMPLE_COUNTRIES)[0]
1852				});
1853			}
1854			noConsentRegions.forEach(function(r) {
1855				groups.push({
1856					id: r.id,
1857					label: r.label,
1858					countries: r.countries,
1859					info: '<b>' + t('geoPreviewNoConsentHeader', 'No consent required') + '</b> — '
1860						+ t('geoPreviewNoConsentInfo', 'No popup shown.')
1861						+ '<br>' + t('geoPreviewNoConsentInfo2', 'Only the privacy button is visible (if enabled).'),
1862					states: r.states,
1863					defaultCountry: r.defaultCountry, defaultState: r.defaultState || ''
1864				});
1865			});
1866
1867			return groups;
1868		}
1869
1870		function populateGeoGroups() {
1871			const groups = buildGeoGroups();
1872			geoGroupSelect.innerHTML = '<option value="" disabled selected>' + (translations['geoPreviewSelectRegion'] || 'Select a region...') + '</option>';
1873			groups.forEach(function(g) {
1874				var opt = document.createElement('option');
1875				opt.value = g.id;
1876				opt.textContent = g.label;
1877				geoGroupSelect.appendChild(opt);
1878			});
1879			geoGroupSelect._groups = groups;
1880		}
1881
1882		function getSelectedGroup() {
1883			var groups = geoGroupSelect._groups || [];
1884			var val = geoGroupSelect.value;
1885			for (var i = 0; i < groups.length; i++) {
1886				if (groups[i].id === val) return groups[i];
1887			}
1888			return null;
1889		}
1890
1891		geoGroupSelect.addEventListener('change', function() {
1892			var group = getSelectedGroup();
1893			if (!group) return;
1894
1895			// Populate countries
1896			var countries = group.countries || {};
1897			var keys = Object.keys(countries);
1898			if (keys.length > 1) {
1899				geoCountryContainer.classList.remove('d-none');
1900				geoCountrySelect.innerHTML = '';
1901				keys.sort(function(a,b){ return countries[a].localeCompare(countries[b]); });
1902				keys.forEach(function(code) {
1903					var opt = document.createElement('option');
1904					opt.value = code;
1905					opt.textContent = countries[code];
1906					geoCountrySelect.appendChild(opt);
1907				});
1908				// Set the selection explicitly on the mounted element — setting
1909				// opt.selected before append is unreliable in some browsers, which
1910				// left select.value empty and the server without a gc override.
1911				if (group.defaultCountry && Object.prototype.hasOwnProperty.call(countries, group.defaultCountry)) {
1912					geoCountrySelect.value = group.defaultCountry;
1913				} else {
1914					geoCountrySelect.value = keys[0];
1915				}
1916			} else {
1917				geoCountryContainer.classList.add('d-none');
1918				geoCountrySelect.innerHTML = '<option value="' + keys[0] + '">' + countries[keys[0]] + '</option>';
1919			}
1920
1921			// Populate states. Respect group.stateOrder when supplied (US group
1922			// uses this to keep implemented states listed first in a specific
1923			// order instead of alphabetic); otherwise sort alphabetically by label.
1924			if (group.states && Object.keys(group.states).length > 0) {
1925				geoStateContainer.classList.remove('d-none');
1926				geoStateSelect.innerHTML = '';
1927				var stateKeys;
1928				if (group.stateOrder && group.stateOrder.length) {
1929					stateKeys = group.stateOrder.filter(function(k){ return Object.prototype.hasOwnProperty.call(group.states, k); });
1930				} else {
1931					stateKeys = Object.keys(group.states);
1932					stateKeys.sort(function(a,b){ return group.states[a].localeCompare(group.states[b]); });
1933				}
1934				stateKeys.forEach(function(code) {
1935					var opt = document.createElement('option');
1936					opt.value = code;
1937					opt.textContent = group.states[code];
1938					geoStateSelect.appendChild(opt);
1939				});
1940				// Same reason as the country select above — explicit value
1941				// assignment after mount.
1942				if (Object.prototype.hasOwnProperty.call(group.states, group.defaultState || '')) {
1943					geoStateSelect.value = group.defaultState || '';
1944				} else {
1945					geoStateSelect.value = stateKeys[0];
1946				}
1947			} else {
1948				geoStateContainer.classList.add('d-none');
1949				geoStateSelect.innerHTML = '';
1950			}
1951
1952			// Show info card
1953			geoInfoCard.classList.remove('d-none');
1954			geoInfoContent.innerHTML = group.info;
1955
1956			refreshGeoPreview();
1957		});
1958
1959		geoCountrySelect.addEventListener('change', refreshGeoPreview);
1960		geoStateSelect.addEventListener('change', refreshGeoPreview);
1961		geoLangSelect.addEventListener('change', refreshGeoPreview);
1962
1963		function refreshGeoPreview() {
1964			var country = geoCountrySelect.value || '';
1965			var state = geoStateSelect.value || '';
1966			var lang = geoLangSelect.value || '';
1967			if (!country) return;
1968
1969			// clear=1 tells consent_preview.jsp to wipe CMP/IAB storage before the
1970			// CMP boots. Every geo-group switch should render as a fresh visitor
1971			// — otherwise stored consent from the previous region's selection
1972			// suppresses the auto-popup (and the IAB state cache can keep
1973			// displaying the prior region's purposes).
1974			var src = "../views/consent_preview.jsp?pid=" + projectId
1975				+ "&key=" + projectAccessKey
1976				+ "&gc=" + encodeURIComponent(country)
1977				+ "&gs=" + encodeURIComponent(state)
1978				+ (lang ? "&gl=" + encodeURIComponent(lang) : "")
1979				+ "&clear=1"
1980				+ "&rand=" + Math.random();
1981
1982			if (geoPreviewLoaded) {
1983				geoPreviewFrame.src = src;
1984			} else {
1985				geoPreviewFrame.src = src;
1986				geoPreviewLoaded = true;
1987			}
1988		}
1989
1990		// Rebuild geo groups when advertising checkboxes or site-data toggles change
1991		CFG_TCF_REGIONS.concat(CFG_GCM_REGIONS).forEach(function(id) {
1992			var el = document.getElementById(id);
1993			if (el) el.addEventListener('change', populateGeoGroups);
1994		});
1995
1996		// Lazy-load: populate groups when tab is first shown
1997		var geoTab = document.getElementById('geopreview-tab');
1998		if (geoTab) {
1999			geoTab.addEventListener('shown.bs.tab', function() {
2000				if (!geoGroupSelect._groups) populateGeoGroups();
2001				// Re-populate languages with server data if it arrived after initial populate
2002				if (serverAvailableLanguages) populateGeoLanguages();
2003			});
2004			populateGeoGroups();
2005		}
2006
2007		// Mobile/Desktop toggle for geo preview
2008		var geoBrowserFrame = document.getElementById('geo_browser_frame');
2009		var geoBtnMobile = document.getElementById('geo_btn_mobile_toggle');
2010		var geoBtnDesktop = document.getElementById('geo_btn_desktop_ratio');
2011
2012		if (geoBtnMobile) {
2013			geoBtnMobile.onclick = function(e) {
2014				e.preventDefault();
2015				geoBtnMobile.classList.toggle('active');
2016				if (geoBtnMobile.classList.contains('active')) {
2017					geoBrowserFrame.classList.add('mobile');
2018					geoBtnDesktop.classList.remove('active');
2019					geoPreviewFrame.style.zoom = 1;
2020					geoBtnDesktop.style.display = 'none';
2021				} else {
2022					geoBrowserFrame.classList.remove('mobile');
2023					geoBtnDesktop.style.display = 'inline-block';
2024				}
2025			};
2026		}
2027
2028		if (geoBtnDesktop) {
2029			geoBtnDesktop.onclick = function(e) {
2030				e.preventDefault();
2031				geoBtnDesktop.classList.toggle('active');
2032				geoPreviewFrame.style.zoom = geoBtnDesktop.classList.contains('active')
2033					? geoPreviewFrame.offsetWidth / screen.width : 1;
2034			};
2035		}
2036
2037		var geoBtnNewTab = document.getElementById('geo_btn_preview_new_tab');
2038		if (geoBtnNewTab) {
2039			geoBtnNewTab.onclick = function(e) {
2040				e.preventDefault();
2041				if (geoPreviewFrame.src) window.open(geoPreviewFrame.src, '_blank');
2042			};
2043		}
2044	}
2045	// ===================== End Geo Preview Tab =====================
2046
2047
2048	// Same page as advertisingForm and registered first: a null here would take the whole
2049	// settings page's wiring down with it, not just its own tab.
2050	projectSettingsForm?.addEventListener('submit', function(event) {
2051		event.preventDefault();
2052
2053		// GCM site-data toggles, GCM sub-settings and the Manage Privacy Settings trigger
2054		// now live on the Consent & Privacy tab and are saved by advertisingForm.
2055		// The server reads `projectName` (RestCommands, /project/config); this handler posted
2056		// `name` for years, so a rename from the General tab was acknowledged with a success
2057		// toast and silently dropped. tests/panel/verify-settings-save.js round-trips it.
2058		const data = {
2059			projectName: projectSettingsForm.elements['name'].value.trim(),
2060			cmpEnabled: projectSettingsForm.elements['consent_enabled'].checked,
2061			primaryLanguage: projectSettingsForm.elements['select_language'].value,
2062			useClientLanguage: projectSettingsForm.elements['use_client_info_for_language'].checked,
2063			cmpAutoBlockerEnabled: projectSettingsForm.elements['auto_blocker'].checked,
2064			cmpDisplayConsentButton: projectSettingsForm.elements['privacy_button'].checked,
2065			cmpShowRejectAllHome: projectSettingsForm.elements['cmp_show_reject_all_home']
2066				? projectSettingsForm.elements['cmp_show_reject_all_home'].checked
2067				: true,
2068			cmpHideEmptyCookieCategories: !!(projectSettingsForm.elements['cmp_hide_empty_cookie_categories']
2069				&& projectSettingsForm.elements['cmp_hide_empty_cookie_categories'].checked),
2070			cmpMonthlyScanReport: !!(projectSettingsForm.elements['cmp_monthly_scan_report']
2071				&& projectSettingsForm.elements['cmp_monthly_scan_report'].checked),
2072			cmpEmbedPlaceholder: projectSettingsForm.elements['cmp_embed_placeholder']
2073				? projectSettingsForm.elements['cmp_embed_placeholder'].checked : true,
2074			cmpRegateOnWithdrawal: projectSettingsForm.elements['cmp_regate_on_withdrawal']
2075				? projectSettingsForm.elements['cmp_regate_on_withdrawal'].checked : false,
2076			cmpAccessibilityWidget: projectSettingsForm.elements['cmp_accessibility_widget']
2077				? projectSettingsForm.elements['cmp_accessibility_widget'].checked : false,
2078			cmpExperiment: fcCollectExperiment(),
2079			// §12. This is the payload the Save button actually sends. A merchant reported
2080			// on 2026-09-10 that the banner fields never reached the server: §12 had been
2081			// wired into saveTemplateSettings() instead, which reads identically and is
2082			// never called. The round-trip verifier's mustCarry list now names this field.
2083			cmpBannerText: fcCollectBannerText()
2084		};
2085		updateTemplateSettings(data, projectId);
2086	});
2087
2088	// "Apply US National settings to all states" — copies the personal_data, targeted_ads,
2089	// and sensitive_data values from US National into CA/VA/CO/UT/CT, so the publisher
2090	// doesn't have to configure each state block individually. California has no
2091	// targeted_ads field (CCPA/uscav1 folds it into the sale/share control), so that
2092	// field is skipped for CA.
2093	const applyUsNatToAllBtn = document.getElementById('us_nat_apply_to_all_states');
2094	if (applyUsNatToAllBtn) applyUsNatToAllBtn.addEventListener('click', function() {
2095		const personal = document.getElementById('us_nat_pref_personal_data').value;
2096		const targeted = document.getElementById('us_nat_pref_targeted_ads').value;
2097		const sensitive = document.getElementById('us_nat_pref_sensitive_data').value;
2098		US_SECTION_PREFS.forEach(function(sec) {
2099			if (sec.key === 'usNat') return;
2100			const p = document.getElementById(sec.prefix + '_personal_data');
2101			const s = document.getElementById(sec.prefix + '_sensitive_data');
2102			if (p) p.value = personal;
2103			if (s) s.value = sensitive;
2104			if (sec.withTargetedAds) {
2105				const t = document.getElementById(sec.prefix + '_targeted_ads');
2106				if (t) t.value = targeted;
2107			}
2108		});
2109	});
2110
2111	// Guarded like layoutForm below, and for a reason that is not hypothetical: this form is
2112	// called `advertising_settings_form` and holds 19 controls named `advertising_*`. Those are
2113	// exactly the names ad blockers strip with cosmetic filters. If the form is removed from the
2114	// DOM, an unguarded addEventListener here throws DURING SCRIPT LOAD — so every handler
2115	// registered AFTER this line never attaches, while the General tab above keeps working
2116	// because its handler was registered first. The visible symptom is a Save button that sends
2117	// no request at all plus a console error, on one tab only, for one user.
2118	//
2119	// The same reasoning applies to the four regions below: nine of the thirteen were already
2120	// written defensively, and eu/us/ca/others were not — a missing checkbox there throws inside
2121	// the handler and loses the save just as completely.
2122	advertisingForm?.addEventListener('submit', function (event) {
2123		event.preventDefault();
2124
2125		const checked = (name) => {
2126			const el = advertisingForm.elements[name];
2127			return el && el.checked ? 1 : 0;
2128		};
2129		const advertisingSettings = {
2130			eu: checked('advertising_eu'),
2131			us: checked('advertising_us'),
2132			ca: checked('advertising_ca'),
2133			tr: checked('advertising_tr'),
2134			br: checked('advertising_br'),
2135			za: checked('advertising_za'),
2136			jp: checked('advertising_jp'),
2137			ch: checked('advertising_ch'),
2138			au: checked('advertising_au'),
2139			kr: checked('advertising_kr'),
2140			th: checked('advertising_th'),
2141			in: checked('advertising_in'),
2142			others: checked('advertising_others'),
2143			regulations: {
2144				us: document.getElementById('regulation_enable_us') && document.getElementById('regulation_enable_us').checked ? 1 : 0,
2145				ca: 1,
2146				tr: 1
2147			},
2148			siteDataConsent: collectSiteDataConsent()
2149		};
2150		addQuebecAdvertising(advertisingSettings);
2151		const sectionPreferences = collectUsSectionPreferences();
2152		const data = {
2153			cmpAdvertisingSettings: advertisingSettings,
2154			cmpSectionPreferences: sectionPreferences,
2155			cmpConsentModeV2: {
2156				enabled: isAnySiteDataEnabled(),
2157				urlPassthrough: document.getElementById('gcm_url_passthrough').checked,
2158				adsDataRedaction: document.getElementById('gcm_ads_data_redaction').checked,
2159				googleSignals: document.getElementById('gcm_google_signals').checked,
2160				adPersonalizationSignals: document.getElementById('gcm_ad_personalization_signals').checked,
2161				restrictedDataProcessing: document.getElementById('gcm_restricted_data_processing').checked,
2162				sendPageView: document.getElementById('gcm_send_page_view').value !== 'false',
2163				waitForUpdate: parseInt(document.getElementById('gcm_wait_for_update').value, 10) || 1000
2164			}
2165		};
2166		updateTemplateSettings(data, projectId);
2167	});
2168
2169	setupConsentConfigureModal();
2170
2171	layoutForm?.addEventListener('submit', function (event) {
2172		event.preventDefault();
2173
2174		const data = {
2175			customCss: {
2176				enabled: layoutForm.elements['layout_properties_enabled'].checked,
2177				fontFamily: layoutForm.elements['font_family'].value,
2178				fontSize: layoutForm.elements['font_size'].value,
2179				css: layoutForm.elements['custom_css'].value,
2180				customLogo: layoutForm.elements['custom_logo'].value,
2181				customIcon: layoutForm.elements['custom_icon'].value,
2182				homeModalTheme: layoutForm.elements['home_modal_theme'].value,
2183				homeModalLocation: layoutForm.elements['home_modal_location'].value,
2184				allowPageInteraction: layoutForm.elements['page_interaction'].checked
2185			}
2186		};
2187		updateTemplateSettings(data, projectId);
2188	});
2189
2190
2191	const tabsUl = document.getElementById('settings_tabs');
2192	const tabContents = document.getElementById('tab_contents');
2193	const nextButtons = document.getElementsByClassName('btn-next');
2194	Array.prototype.forEach.call(nextButtons, function(button) {
2195		button.addEventListener('click', function() {
2196			const currentForm = this.closest('form');
2197			const currentIndex = creatingForms.indexOf(currentForm);
2198			if (currentIndex >= 0 && currentIndex < creatingForms.length - 1) {
2199				for (let form of creatingForms) {
2200					const elements = form.getElementsByTagName("*");
2201					for (let child of elements) {
2202						if (child.tagName === 'INPUT' && child.hasAttribute('required') && child.offsetParent !== null) {
2203							if (child.value.trim() === "") {
2204								showAlert((translations['fieldRequired'] || "This field is required") + ": " + (child.getAttribute('data-field-name') || child.id), "warning");
2205								child.focus();
2206								return;
2207							}
2208						}
2209					}
2210				}
2211
2212				// Deactivate current tab
2213				tabsUl.children[currentIndex].querySelector('button').classList.remove('active');
2214				tabContents.children[currentIndex].classList.remove('show', 'active');
2215				// Activate next tab
2216				tabsUl.children[currentIndex + 1].querySelector('button').classList.add('active');
2217				tabContents.children[currentIndex + 1].classList.add('show', 'active');
2218				window.scrollTo(0, 0);
2219			}
2220		});
2221	});
2222}
2223
2224// saveTemplateSettings() lived here and nothing ever called it: the settings page saves
2225// through the projectSettingsForm submit handler above. It was a complete, plausible second
2226// copy of the same payload — its own fetch, its own field list — and on 2026-09-10 a merchant
2227// reported that the §12 banner fields never reached the server because §12 had been wired into
2228// this copy instead of the live one. Deleted rather than left as a trap for whoever wires the
2229// next setting. The create path is createTemplateSettings() below, which IS called.
2230
2231async function createTemplateSettings() {
2232	const name = document.getElementById('name').value.trim();
2233	const cmpEnabled = document.getElementById('consent_enabled').checked;
2234	const primaryLanguage = document.getElementById('select_language').value;
2235	const useClientLanguage = document.getElementById('use_client_info_for_language').checked;
2236	const googleConsentModeV2 = {
2237		enabled: isAnySiteDataEnabled(),
2238		urlPassthrough: document.getElementById('gcm_url_passthrough').checked,
2239		adsDataRedaction: document.getElementById('gcm_ads_data_redaction').checked,
2240		googleSignals: document.getElementById('gcm_google_signals').checked,
2241		adPersonalizationSignals: document.getElementById('gcm_ad_personalization_signals').checked,
2242		restrictedDataProcessing: document.getElementById('gcm_restricted_data_processing').checked,
2243		sendPageView: document.getElementById('gcm_send_page_view').value !== 'false',
2244		waitForUpdate: parseInt(document.getElementById('gcm_wait_for_update').value, 10) || 1000
2245	};
2246	const autoBlockerEnabled = document.getElementById('auto_blocker').checked;
2247	const cmpDisplayPrivacyButton = document.getElementById('privacy_button').checked;
2248
2249	const advertisingSettings = {
2250		eu: document.getElementById('advertising_eu').checked ? 1 : 0,
2251		us: document.getElementById('advertising_us').checked ? 1 : 0,
2252		ca: document.getElementById('advertising_ca').checked ? 1 : 0,
2253		tr: document.getElementById('advertising_tr') && document.getElementById('advertising_tr').checked ? 1 : 0,
2254		br: document.getElementById('advertising_br') && document.getElementById('advertising_br').checked ? 1 : 0,
2255		za: document.getElementById('advertising_za') && document.getElementById('advertising_za').checked ? 1 : 0,
2256		jp: document.getElementById('advertising_jp') && document.getElementById('advertising_jp').checked ? 1 : 0,
2257		ch: document.getElementById('advertising_ch') && document.getElementById('advertising_ch').checked ? 1 : 0,
2258		au: document.getElementById('advertising_au') && document.getElementById('advertising_au').checked ? 1 : 0,
2259		kr: document.getElementById('advertising_kr') && document.getElementById('advertising_kr').checked ? 1 : 0,
2260		th: document.getElementById('advertising_th') && document.getElementById('advertising_th').checked ? 1 : 0,
2261		in: document.getElementById('advertising_in') && document.getElementById('advertising_in').checked ? 1 : 0,
2262		others: document.getElementById('advertising_others').checked ? 1 : 0,
2263		regulations: {
2264			us: document.getElementById('regulation_enable_us') && document.getElementById('regulation_enable_us').checked ? 1 : 0,
2265			ca: 1,
2266			tr: 1
2267		},
2268		siteDataConsent: collectSiteDataConsent()
2269	}
2270	addQuebecAdvertising(advertisingSettings);
2271	const sectionPreferences = collectUsSectionPreferences();
2272
2273	const data = {
2274		projectTypeId: 2,
2275		sid: getSid(),
2276
2277		projectName: name,
2278		cmpEnabled: cmpEnabled,
2279		useClientLanguage: useClientLanguage,
2280		primaryLanguage: primaryLanguage,
2281		cmpConsentModeV2: googleConsentModeV2,
2282		cmpAutoBlockerEnabled: autoBlockerEnabled,
2283		cmpDisplayConsentButton: cmpDisplayPrivacyButton,
2284		cmpShowRejectAllHome: document.getElementById('cmp_show_reject_all_home')
2285			? document.getElementById('cmp_show_reject_all_home').checked : true,
2286		cmpHideEmptyCookieCategories: !!(document.getElementById('cmp_hide_empty_cookie_categories')
2287			&& document.getElementById('cmp_hide_empty_cookie_categories').checked),
2288		cmpMonthlyScanReport: !!(document.getElementById('cmp_monthly_scan_report')
2289			&& document.getElementById('cmp_monthly_scan_report').checked),
2290		cmpEmbedPlaceholder: document.getElementById('cmp_embed_placeholder')
2291			? document.getElementById('cmp_embed_placeholder').checked : true,
2292		cmpRegateOnWithdrawal: document.getElementById('cmp_regate_on_withdrawal')
2293			? document.getElementById('cmp_regate_on_withdrawal').checked : false,
2294		cmpAccessibilityWidget: document.getElementById('cmp_accessibility_widget')
2295			? document.getElementById('cmp_accessibility_widget').checked : false,
2296		cmpBannerText: fcCollectBannerText(),
2297			cmpExperiment: fcCollectExperiment(),
2298
2299		cmpAdvertisingSettings: advertisingSettings,
2300		cmpSectionPreferences: sectionPreferences
2301	};
2302
2303	const websiteId = new URLSearchParams(window.location.search).get("wid");
2304	if (websiteId) {
2305		data.projectDomainId = websiteId;
2306	}
2307
2308	const response = await postRequest(endpoint + '/project/create', data);
2309	if (response.ok) {
2310		const responseBody = await response.json();
2311		if (responseBody.statusCode === 0) {
2312			displayAlert('templateSaveSuccess', 'success');
2313			if (responseBody.pageCode) {
2314				console.dir('show code');
2315				document.getElementById('settings_tabs').style.display = 'none';
2316				document.getElementById('tab_contents').style.display = 'none';
2317				var scriptc = document.getElementById('script_card');
2318				scriptc.classList.remove('d-none');
2319				scriptc.style.display = 'block';
2320				scriptc = document.getElementById('page_script');
2321				scriptc.textContent = responseBody.pageCode;
2322				createCopyToClipboard('copy_btn', 'page_script', 'copy-info');
2323			}
2324		} else {
2325			console.log('Saving Error Code: ' + responseBody.statusCode);
2326			displayAlert('Cannot create project: ' + responseBody.statusMessage);
2327		}
2328	} else {
2329		console.error('Saving failed: ' + JSON.stringify(response));
2330		displayAlert('Cannot create project. Please try later');
2331	}
2332}
2333
2334async function updateTemplateSettings(data, projectId) {
2335	data['projectTypeId'] = 2;
2336	data['sid'] = getSid();
2337	data['project'] = projectId;
2338	const response = await postRequest(endpoint + '/project/config', data);
2339	if (response.ok) {
2340		const responseBody = await response.json();
2341		if (responseBody.statusCode === 0) {
2342			displayAlert('templateSaveSuccess', 'success');
2343		} else {
2344			console.log('Saving Error Code: ' + responseBody.statusCode);
2345			displayAlert('Cant save project: ' + responseBody.statusMessage);
2346		}
2347	} else {
2348		console.error('Saving failed: ' + JSON.stringify(response));
2349		displayAlert('Cant save project: Please try later');
2350	}
2351}
2352
2353
2354/* WEBSITES */
2355
2356async function loadWebsites(includeRejected = false) {
2357	if (checkIfLoginRequired())
2358		return;
2359
2360	/*const scriptModal = document.getElementById('scriptModal');
2361	const scriptModalTitle = scriptModal.querySelector('h1');
2362	setTextContentTranslationByElement(scriptModalTitle, 'pageCode');
2363	setTextContentTranslationById('copy-info', 'copyScriptLabel');
2364	const btnCopy = document.getElementById('copy_btn');
2365	setTextContentTranslationByElement(btnCopy, 'copyCodeToClipboard');
2366	setTextContentTranslationByElement(btnCopy.nextElementSibling, 'close');*/
2367
2368	const websitesTable = document.getElementById('websites_table');
2369	// Reset rows so the deleted-domains toggle can reload cleanly.
2370	const existingBody = websitesTable.querySelector('tbody');
2371	if (existingBody) existingBody.innerHTML = '';
2372
2373	const data = { sid: getSid(), projectTypeId: 2, includeRejected: includeRejected };
2374	const response = await postRequest(endpoint + '/project-domain/list', data);
2375	if (!response.ok) {
2376		console.error('Websites could not be loaded: ' + JSON.stringify(response));
2377		return;
2378	}
2379
2380	const responseBody = await response.json();
2381	if (responseBody.statusCode === 0) {
2382		// Toggle ON = deleted-only view; toggle OFF = active-only (backend already
2383		// excludes rejected when includeRejected is false, filter is a belt-and-braces).
2384		const websites = (responseBody.domains || []).filter(w => includeRejected ? w.deleted : !w.deleted);
2385		if (!websites.length) {
2386			websitesTable.classList.add('d-none');
2387			if (includeRejected)
2388				displayAlert(translations['noDeletedDomains'] || 'You have no deleted domains.', 'info');
2389			else
2390				showWebsiteCreateLink('websites_table_container');
2391			return;
2392		}
2393
2394		const tableBody = websitesTable.querySelector('tbody');
2395		websites.sort((a, b) => a.domainName.localeCompare(b.domainName));
2396		// websites.sort((a, b) => a.id.localeCompare(b.id));
2397		websites.forEach(website => addWebsiteRow(tableBody, website));
2398		websitesTable.classList.remove('d-none');
2399	} else if (responseBody.statusCode === 3) {
2400		deleteCookie('sid');
2401		deleteCookie('name');
2402		deleteCookie('email');
2403		clearLocalStorage();
2404		alog("Jump to login 14");
2405		window.location.href = '../views/login.jsp';
2406	}
2407
2408	//createCopyToClipboard('copy_btn', 'script_content_area');
2409
2410	function addWebsiteRow(tableBody, website) {
2411		const row = document.createElement('tr');
2412
2413		// Previously deleted (rejected) domain: render a muted row whose only
2414		// action is to restore it. It does not count toward the plan until restored.
2415		if (website.deleted) {
2416			row.classList.add('table-secondary');
2417
2418			const delDomainCell = document.createElement('td');
2419			delDomainCell.textContent = website.domainName;
2420			const badge = document.createElement('span');
2421			badge.className = 'badge bg-secondary ms-2';
2422			badge.textContent = translations['deletedLabel'] || 'Deleted';
2423			delDomainCell.appendChild(badge);
2424			row.appendChild(delDomainCell);
2425
2426			const delTemplateCell = document.createElement('td');
2427			delTemplateCell.textContent = website.project.name;
2428			delTemplateCell.classList.add('align-middle', 'text-muted');
2429			row.appendChild(delTemplateCell);
2430
2431			if (!isReseller)
2432				row.appendChild(document.createElement('td')); // empty plan cell
2433
2434			const delActionCell = document.createElement('td');
2435			delActionCell.classList.add('text-center');
2436			const reacceptBtn = document.createElement('button');
2437			reacceptBtn.textContent = translations['reAcceptDomain'] || 'Re-accept';
2438			reacceptBtn.classList.add('btn', 'btn-outline-success', 'btn-sm');
2439			reacceptBtn.addEventListener('click', async () => {
2440				if (!await showConfirm(translations['reAcceptDomainConfirm'] || 'Re-accept this domain? It will count toward your plan again.', translations['reAcceptDomain'] || 'Re-accept'))
2441					return;
2442				const rdata = { sid: getSid(), projectTypeId: 2, domainId: website.id };
2443				const rresp = await postRequest(endpoint + '/project-domain/reaccept', rdata);
2444				if (rresp.ok) {
2445					const rbody = await rresp.json();
2446					if (rbody.statusCode === 0) {
2447						displayAlert(translations['reAcceptDomainSuccessMsg'] || 'The domain was restored.', 'success');
2448						setTimeout(() => window.location.reload(), 1500);
2449					} else {
2450						displayAlert('Error: ' + rbody.statusMessage);
2451					}
2452				} else {
2453					displayAlert('Domain could not be re-accepted: Please try later');
2454				}
2455			});
2456			delActionCell.appendChild(reacceptBtn);
2457			row.appendChild(delActionCell);
2458			tableBody.appendChild(row);
2459			return;
2460		}
2461
2462		const domainCell = document.createElement('td');
2463		domainCell.textContent = website.domainName;
2464		row.appendChild(domainCell);
2465
2466		const templateCell = document.createElement('td');
2467		const templateLink = document.createElement('a');
2468		templateLink.textContent = website.project.name;
2469		templateLink.href = "template_settings.jsp?id=" + website.project.id + "&pp=w";
2470		templateLink.classList.add("name-link", "align-middle");
2471		templateCell.appendChild(templateLink);
2472		row.appendChild(templateCell);
2473
2474		if (!isReseller) {
2475			const subscriptionCell = document.createElement('td');
2476			subscriptionCell.classList.add('text-center');
2477			row.appendChild(subscriptionCell);
2478			if (website.st) {
2479				// const subscriptionLink = document.createElement('a');
2480				// const st = responseBody.packages.find(p => p.id === website.st);
2481				// // if (st.name.includes('(Month)'))
2482				// // 	st.name = st.name.replace('(Month)', '');
2483				//
2484				// if (st.durationMonth % 12 === 0)
2485				// 	subscriptionLink.textContent = st.name + ' (' + translations['annual'] + ')';
2486				// else
2487				// 	subscriptionLink.textContent = st.name + ' (' + translations['monthly'] + ')';
2488				//
2489				// subscriptionLink.href = "subscriptions.jsp?id=" + project.id;
2490				// subscriptionLink.classList.add("name-link", "align-middle");
2491				// subscriptionCell.appendChild(subscriptionLink);
2492
2493				const st = responseBody.packages.find(p => p.id === website.st);
2494				if (st.durationMonth % 12 === 0)
2495					subscriptionCell.textContent = st.name + ' (' + translations['annual'] + ')';
2496				else
2497					subscriptionCell.textContent = st.name + ' (' + translations['monthly'] + ')';
2498			}
2499		}
2500		const actionCell = document.createElement('td');
2501		actionCell.classList.add('text-center');
2502
2503		const settingsButton = document.createElement('button');
2504		settingsButton.textContent = translations['settings'];
2505		settingsButton.classList.add('btn', 'btn-outline-primary', 'btn-sm');
2506		settingsButton.addEventListener('click', () => {
2507			alog("Jump to website settings " + website.id + " 11");
2508			window.location.href = "website_settings.jsp?id=" + website.id;
2509		});
2510		actionCell.appendChild(settingsButton);
2511
2512		// const trashIcon = "<i class='fa fa-trash text-danger'></i>";//<i class="fa-solid fa-trash-can"></i>
2513
2514
2515		const deleteBtn = document.createElement('button');
2516		// deleteBtn.textContent = translations['delete'];
2517		// deleteBtn.innerHTML = "<i class='fa-solid fa-trash'></i>";
2518		deleteBtn.innerHTML = "<i class='fa-regular fa-trash-can'></i>";
2519		deleteBtn.classList.add('btn', 'btn-delete', /*'btn-outline-danger',*/ 'btn-sm', 'ms-2');
2520		// deleteBtn.setAttribute('data-bs-toggle', 'tooltip');
2521		// deleteBtn.setAttribute('data-bs-placement', 'top');
2522		// deleteBtn.setAttribute('title', translations['delete'] || "Delete");
2523		actionCell.appendChild(deleteBtn);
2524		deleteBtn.addEventListener('click', async () => {
2525			if (!await showConfirm(translations['deleteWebsiteConfirm'] || "Are you sure you want to delete this website?", translations['delete'] || "Delete"))
2526				return;
2527
2528			const data = {sid: getSid(), projectTypeId: 2, domainId: website.id};
2529			const response = await postRequest(endpoint + '/project-domain/delete', data);
2530			if (response.ok) {
2531				const responseBody = await response.json();
2532				if (responseBody.statusCode === 0) {
2533					displayAlert(translations['deleteWebsiteSuccessMsg'] ||"The website was deleted.", 'success');
2534					tableBody.deleteRow(row.rowIndex - 1);
2535					// if (tableBody.rows.length === 0) {
2536					// 	window.location.reload();
2537					// }
2538
2539					setTimeout(() => {
2540						window.location.reload();
2541					}, 3000);
2542				} else {
2543					displayAlert("Error: " + responseBody.statusMessage);
2544				}
2545			} else {
2546				console.error('Website could not be deleted: ' + JSON.stringify(response));
2547				displayAlert('Website could not be deleted: Please try later');
2548			}
2549		});
2550
2551		/*const scriptButton = document.createElement('button');
2552		setTextContentTranslationByElement(scriptButton, 'pageCode');
2553		scriptButton.classList.add('btn', 'btn-outline-primary', 'btn-sm', 'ms-2');
2554		scriptButton.setAttribute('data-bs-toggle', 'modal');
2555		scriptButton.setAttribute('data-bs-target', '#scriptModal');
2556		actionCell.appendChild(scriptButton);
2557		scriptButton.addEventListener('click', async () => {
2558			const data = { sid: getSid(), domainId: domain.id };
2559			const response = await postRequest(endpoint + '/scriptcode', data);
2560			if (response.ok) {
2561				const responseBody = await response.json();
2562				if (responseBody.statusCode === 0) {
2563					const contentElement = document.getElementById('script_content_area');
2564					contentElement.textContent = responseBody.content;
2565				} else {
2566					console.log('Script Error Code: ' + responseBody.statusCode);
2567				}
2568			}
2569		});*/
2570
2571		if (responseBody.ul1) {
2572			const deleteButton = document.createElement('button');
2573			deleteButton.textContent = translations['delete'] || 'Delete';
2574			deleteButton.classList.add('btn', 'btn-outline-primary', 'btn-sm', 'ms-2');
2575			actionCell.appendChild(deleteButton);
2576			deleteButton.addEventListener('click', async () => {
2577				const data2 = { sid: getSid(), projectTypeId: 2, projectId: website.project.id };
2578				const response2 = await postRequest(endpoint + '/project/delete', data2);
2579				if (response2.ok) {
2580					const responseBody2 = await response2.json();
2581					if (responseBody2.statusCode === 0)
2582						tableBody.deleteRow(row.rowIndex - 1);
2583				}
2584			});
2585		}
2586
2587		row.appendChild(actionCell);
2588		tableBody.appendChild(row);
2589	}
2590}
2591
2592// Toggle button on the websites page: reloads the list with previously deleted
2593// (rejected) domains shown, so the user can restore them.
2594function setupDeletedDomainsToggle() {
2595	const btn = document.getElementById('btn_toggle_deleted');
2596	if (!btn)
2597		return;
2598	let showing = false;
2599	btn.addEventListener('click', async () => {
2600		showing = !showing;
2601		await loadWebsites(showing);
2602		btn.textContent = showing
2603			? (translations['hideDeletedDomains'] || 'Hide deleted domains')
2604			: (translations['showDeletedDomains'] || 'Show deleted domains');
2605	});
2606}
2607
2608// Website-settings page banner for a rejected (deleted) domain: persistent warning
2609// with an Accept button that restores the domain via /project-domain/reaccept.
2610function showRejectedDomainBanner(domainId) {
2611	const container = document.getElementById('alert_container');
2612	if (!container)
2613		return;
2614
2615	const banner = document.createElement('div');
2616	banner.classList.add('alert', 'alert-warning', 'd-flex', 'justify-content-between', 'align-items-center', 'mb-2');
2617	banner.setAttribute('role', 'alert');
2618
2619	const text = document.createElement('span');
2620	text.textContent = translations['domainRejectedMsg']
2621		|| 'This domain is currently deleted (rejected). It is not served and does not count toward your plan.';
2622	banner.appendChild(text);
2623
2624	const acceptBtn = document.createElement('button');
2625	acceptBtn.type = 'button';
2626	acceptBtn.classList.add('btn', 'btn-success', 'btn-sm', 'ms-3');
2627	acceptBtn.textContent = translations['reAcceptDomain'] || 'Accept';
2628	acceptBtn.addEventListener('click', async () => {
2629		if (!await showConfirm(translations['reAcceptDomainConfirm'] || 'Re-accept this domain? It will count toward your plan again.', translations['reAcceptDomain'] || 'Accept'))
2630			return;
2631		const rdata = { sid: getSid(), projectTypeId: 2, domainId: domainId };
2632		const rresp = await postRequest(endpoint + '/project-domain/reaccept', rdata);
2633		if (rresp.ok) {
2634			const rbody = await rresp.json();
2635			if (rbody.statusCode === 0) {
2636				displayAlert(translations['reAcceptDomainSuccessMsg'] || 'The domain was restored.', 'success');
2637				setTimeout(() => window.location.reload(), 1500);
2638			} else {
2639				displayAlert('Error: ' + rbody.statusMessage);
2640			}
2641		} else {
2642			displayAlert('Domain could not be re-accepted: Please try later');
2643		}
2644	});
2645	banner.appendChild(acceptBtn);
2646
2647	// Sibling of alert_container, not a child: displayAlert() clears the container
2648	// (innerHTML = '') and would otherwise wipe this banner.
2649	container.parentNode.insertBefore(banner, container);
2650}
2651
2652/* TEMPLATE SETTINGS: select-all masters for the region checkbox groups
2653   (IAB TCF advertising + browser storage consents). The master reflects the
2654   group (checked = all on, indeterminate = mixed) and toggling it dispatches
2655   a change event per checkbox so dependent UI (e.g. the US-preferences
2656   section tied to advertising_us) reacts exactly as with a manual click. */
2657function setupSelectAllToggles() {
2658	wireSelectAll('advertising_select_all', 'advertising_section_body');
2659	wireSelectAll('site_data_select_all', 'site_data_consent_section');
2660
2661	function wireSelectAll(masterId, containerId) {
2662		const master = document.getElementById(masterId);
2663		const container = document.getElementById(containerId);
2664		if (!master || !container)
2665			return;
2666		const boxes = () => Array.from(container.querySelectorAll('input[type="checkbox"]'))
2667			.filter(cb => cb.id !== masterId);
2668		const refresh = () => {
2669			const bs = boxes();
2670			const on = bs.filter(cb => cb.checked).length;
2671			master.checked = bs.length > 0 && on === bs.length;
2672			master.indeterminate = on > 0 && on < bs.length;
2673		};
2674		master.addEventListener('change', () => {
2675			const target = master.checked;
2676			boxes().forEach(cb => {
2677				if (cb.checked !== target) {
2678					cb.checked = target;
2679					cb.dispatchEvent(new Event('change', { bubbles: true }));
2680				}
2681			});
2682			refresh();
2683		});
2684		container.addEventListener('change', e => {
2685			if (e.target !== master)
2686				refresh();
2687		});
2688		refresh();
2689	}
2690}
2691
2692/* ADMIN: USER SEARCH + IMPERSONATE (admin_users.jsp) */
2693function loadAdminUsers() {
2694	if (checkIfLoginRequired())
2695		return;
2696	const form = document.getElementById('admin_user_search_form');
2697	if (!form)
2698		return;
2699	form.addEventListener('submit', async (e) => {
2700		e.preventDefault();
2701		await searchAdminUsers();
2702	});
2703	// Live search: re-query as the admin types (debounced), no button click needed.
2704	let debounceTimer = null;
2705	form.addEventListener('input', () => {
2706		clearTimeout(debounceTimer);
2707		debounceTimer = setTimeout(() => searchAdminUsers(true), 400);
2708	});
2709}
2710
2711async function searchAdminUsers(silent) {
2712	// ids go as strings — the backend partial-matches them (LIKE), so "12" finds 123.
2713	const data = {
2714		sid: getSid(),
2715		domain: (document.getElementById('search_domain').value || '').trim(),
2716		projectId: (document.getElementById('search_projectId').value || '').trim(),
2717		email: (document.getElementById('search_email').value || '').trim(),
2718		userId: (document.getElementById('search_userId').value || '').trim(),
2719	};
2720	if (!data.domain && !data.projectId && !data.email && !data.userId) {
2721		if (!silent)
2722			displayAlert(translations['adminSearchNoCriteria'] || 'Enter at least one search criterion.', 'warning');
2723		return;
2724	}
2725	// Sequence guard: with debounced live search, an older (slower) response
2726	// must not overwrite the results of a newer query.
2727	const seq = (searchAdminUsers._seq = (searchAdminUsers._seq || 0) + 1);
2728	const response = await postRequest(endpoint + '/user/search', data);
2729	if (seq !== searchAdminUsers._seq)
2730		return;
2731	if (!response.ok) {
2732		displayAlert('Search failed. Please try again.');
2733		return;
2734	}
2735	const body = await response.json();
2736	if (body.statusCode !== 0) {
2737		displayAlert('Error: ' + (body.statusMessage || 'not authorized'));
2738		return;
2739	}
2740	renderAdminUsers(body.users || []);
2741}
2742
2743function renderAdminUsers(users) {
2744	const table = document.getElementById('admin_users_table');
2745	const empty = document.getElementById('admin_users_empty');
2746	const tbody = table.querySelector('tbody');
2747	tbody.innerHTML = '';
2748	if (!users.length) {
2749		table.classList.add('d-none');
2750		empty.classList.remove('d-none');
2751		return;
2752	}
2753	empty.classList.add('d-none');
2754	// Build cells with textContent (never innerHTML) — email/name are user-controlled.
2755	users.forEach(u => {
2756		const row = document.createElement('tr');
2757		[u.id, u.email || '', u.fullName || ''].forEach(v => {
2758			const td = document.createElement('td');
2759			td.textContent = v;
2760			row.appendChild(td);
2761		});
2762		[u.numProjects || 0, u.numDomains || 0].forEach(v => {
2763			const td = document.createElement('td');
2764			td.className = 'text-center';
2765			td.textContent = v;
2766			row.appendChild(td);
2767		});
2768		const actionCell = document.createElement('td');
2769		actionCell.className = 'text-center';
2770		const btn = document.createElement('button');
2771		btn.className = 'btn btn-outline-primary btn-sm';
2772		btn.textContent = translations['impersonate'] || 'Impersonate';
2773		if (u.enabled === false)
2774			btn.disabled = true;
2775		btn.addEventListener('click', () => impersonateUser(u));
2776		actionCell.appendChild(btn);
2777		row.appendChild(actionCell);
2778		tbody.appendChild(row);
2779	});
2780	table.classList.remove('d-none');
2781}
2782
2783async function impersonateUser(u) {
2784	const msg = (translations['impersonateConfirm'] || 'Sign in as [email]? You will be logged in as this user.').replace('[email]', u.email);
2785	if (!await showConfirm(msg, translations['impersonate'] || 'Impersonate'))
2786		return;
2787	const response = await postRequest(endpoint + '/user/impersonate', { sid: getSid(), uid: u.id });
2788	if (!response.ok) {
2789		displayAlert('Impersonation failed. Please try again.');
2790		return;
2791	}
2792	const body = await response.json();
2793	if (body.statusCode !== 0 || !body.auth) {
2794		displayAlert('Error: ' + (body.statusMessage || 'could not impersonate'));
2795		return;
2796	}
2797	// Switch the session to the target user's new sid, then load their panel.
2798	createCookie('sid', body.auth);
2799	if (body.content) {
2800		createCookie('name', body.content.fullName || '');
2801		createCookie('email', body.content.email || '');
2802	}
2803	clearLocalStorage();
2804	window.location.href = 'dashboard.jsp';
2805}
2806
2807/* WEBSITE SETTINGS */
2808
2809async function loadWebsiteSettings() {
2810	if (checkIfLoginRequired() )
2811		return;
2812
2813	const websiteSettingsForm = document.getElementById('website_settings_form');
2814	const subscriptionSection = document.getElementById('subscription_section');
2815
2816	let packages = null, templates = null, isFirst = false;
2817	const params = new URLSearchParams(window.location.search);
2818	const domainId = params.get("id");
2819	if (domainId == null) { //Create new website
2820		setTextContentTranslationById('subtitle', 'newWebsite');
2821
2822		const scriptCard = document.getElementById('script_card');
2823		scriptCard.classList.add('d-none');
2824		//projectSettingsForm.addEventListener('submit', createProject);
2825
2826		const data = { sid: getSid(), projectTypeId: 2 };
2827		const response = await postRequest(endpoint + '/project-domain/start', data);
2828		if (response.ok) {
2829			const responseBody = await response.json();
2830			if (responseBody.statusCode === 0) {
2831				templates = responseBody.projects;
2832				if (templates.length === 0) {
2833					isFirst = true;
2834					document.getElementById('template_section').classList.add('d-none');
2835				} else {
2836					fillTemplateSelector(null);
2837				}
2838
2839				//if (!isReseller) {
2840				// packages = responseBody.packages;
2841				//}
2842
2843				subscriptionSection.remove();
2844			}
2845		}
2846		document.getElementById('save-button').removeAttribute('disabled');
2847	} else { //Edit an existing project
2848		document.querySelector('label[for="domain"]').hidden = true;
2849		document.getElementById('domain').setAttribute('disabled', 'disabled');
2850		setTextContentTranslationById('subtitle', 'websiteSettings');
2851
2852		const data = { sid: getSid(), domainId: domainId, projectType: 2 };
2853		const response = await postRequest(endpoint + '/project-domain/get', data);
2854		if (response.ok) {
2855			const responseBody = await response.json();
2856			if (responseBody.statusCode === 0) {
2857				const website = responseBody.domain;
2858				if (website.deleted)
2859					showRejectedDomainBanner(website.id);
2860				websiteSettingsForm.elements['domain'].value = website.domainName;
2861				websiteSettingsForm.elements['cookie_policy'].value = website.settings.cookiePolicyUrl;
2862				websiteSettingsForm.elements['privacy_policy'].value = website.settings.privacyPolicyUrl;
2863
2864				templates = responseBody.projects;
2865				fillTemplateSelector(website.projectId);
2866
2867				//if (!isReseller) {
2868				packages = responseBody.packages;
2869				if (subscriptionSection) {
2870					loadSubscriptionPlan(website);
2871				}
2872				//	}
2873			} else {
2874				console.log('Loading Error Code: ' + responseBody.statusCode);
2875				showAlert("The page could not be loaded!", "error");
2876			}
2877		}
2878		document.getElementById('save-button').removeAttribute('disabled');
2879	}
2880
2881	websiteSettingsForm.addEventListener('submit', function(event) {
2882		event.preventDefault();
2883		saveWebsiteSettings(domainId, isFirst);
2884	}, false);
2885
2886
2887	function fillTemplateSelector(selectedValue) {
2888		if (!templates)
2889			return;
2890
2891		const templateSelector = document.getElementById('template');
2892		templates.forEach(template => {
2893			const option = document.createElement('option');
2894			option.value = template.id;
2895			if (selectedValue && selectedValue === template.id)
2896				option.selected = true;
2897			option.textContent = template.name;
2898			templateSelector.appendChild(option);
2899		});
2900
2901		// templateSelector.dispatchEvent(new Event('change'));
2902	}
2903
2904	function loadSubscriptionPlan(website) {
2905		const st = packages.find(p => p.id === website.st);
2906		const planContainer = document.getElementById('plan_container');
2907		if (website.st > 0) {
2908			planContainer.innerHTML = '';
2909			const planTitle = document.createElement('div');
2910			planTitle.classList.add('mb-2');
2911			planTitle.textContent = st.name;
2912			// planTitle.role = 'button';
2913			planContainer.appendChild(planTitle);
2914
2915			// const chevron = document.createElement('i');
2916			// chevron.classList.add('fa-solid', 'fa-chevron-right', 'ms-1');
2917			// planTitle.appendChild(chevron);
2918
2919			// if (st.description) {
2920			// 	const planDescription = document.createElement('div');
2921			// 	planDescription.classList.add('text-muted', 'small');
2922			// 	planDescription.innerHTML = st.description;
2923			// 	planContainer.appendChild(planDescription);
2924			// }
2925
2926			const updateLink = subscriptionSection.querySelector('.btn-link');
2927			updateLink.textContent = translations['update'];
2928		} else {
2929			planContainer.textContent = translations['notSelected'] || "Not selected";
2930		}
2931	}
2932}
2933
2934async function saveWebsiteSettings(domainId, isFirst) {
2935	const domain = document.getElementById('domain').value.trim();
2936	const cookiePolicyUrl = document.getElementById('cookie_policy').value.trim();
2937	const privacyPolicyUrl = document.getElementById('privacy_policy').value.trim();
2938	const templateId = document.getElementById('template').value || 0;
2939
2940	// let subscriptionTypeId = 0;
2941	// const planSelector = document.getElementById('subscription_plan');
2942	// if (planSelector)
2943	// 	subscriptionTypeId = planSelector.value;
2944
2945	if (domain === '') {
2946		displayAlert('Please enter a domain!');
2947		return;
2948	}
2949
2950	const data = {
2951		sid: getSid(),
2952		domainId: domainId,
2953		domainName: domain,
2954		cookiePolicyUrl: cookiePolicyUrl,
2955		privacyPolicyUrl: privacyPolicyUrl,
2956		projectId: templateId,
2957		// st: subscriptionTypeId
2958	};
2959
2960	const url = endpoint + (domainId == null ? '/project-domain/create' : '/project-domain/update');
2961	const response = await postRequest(url, data);
2962	if (response.ok) {
2963		const responseBody = await response.json();
2964		if (responseBody.statusCode === 0) {
2965			displayAlert('Website settings saved successfully!', 'success');
2966
2967			if (isFirst) {
2968				window.location.href = "template_settings.jsp?wid=" + responseBody.domainId;
2969			} else {
2970				window.location.href = "websites.jsp";
2971			}
2972		} else if (responseBody.statusCode === 2) {
2973			displayAlert(translations['websiteAlreadyExistsError'] || 'Website already exists! Please enter a different domain name.');
2974		} else {
2975			console.log('Saving Error Code: ' + responseBody.statusCode);
2976			displayAlert("Can't save website: " + responseBody.statusMessage);
2977		}
2978	} else {
2979		console.error('Saving failed: ' + JSON.stringify(response));
2980		displayAlert("Can't save website: Please try later");
2981	}
2982}
2983
2984function loadInitialWizard() {
2985
2986}
2987
2988/* DASHBOARD */
2989
2990// The Shopify app's dashboard (FlexNewStatsPanel) with the template picker kept: a header card (range, template,
2991// export), the KPI tiles, consent over time, consent overview, visits by consent, pageviews, then the region and
2992// source cards. The page state lives in its URL (?id=&range=[&start=&end=]): picking a template reloads the page on
2993// that URL, a range change redraws in place and rewrites it, so a reload or the language menu keeps both.
2994const DASHBOARD_RANGES = [
2995	['today', 'Today', () => [moment(), moment()]],
2996	['yesterday', 'Yesterday', () => [moment().subtract(1, 'days'), moment().subtract(1, 'days')]],
2997	// Monday, like the calendar (firstDay: 1) — moment's startOf('week') is Sunday in its default locale.
2998	['thisWeek', 'This Week', () => [moment().startOf('isoWeek'), moment()]],
2999	['last7Days', 'Last 7 Days', () => [moment().subtract(6, 'days'), moment()]],
3000	['thisMonth', 'This Month', () => [moment().startOf('month'), moment()]],
3001	['lastMonth', 'Last Month', () => [moment().subtract(1, 'month').startOf('month'), moment().subtract(1, 'month').endOf('month')]],
3002	['last30Days', 'Last 30 Days', () => [moment().subtract(29, 'days'), moment()]],
3003	['thisYear', 'This Year', () => [moment().startOf('year'), moment()]],
3004	['lastYear', 'Last Year', () => [moment().subtract(1, 'year').startOf('year'), moment().subtract(1, 'year').endOf('year')]],
3005	['last365Days', 'Last 365 Days', () => [moment().subtract(364, 'days'), moment()]],
3006];
3007const DASHBOARD_DEFAULT_RANGE = 'last30Days';
3008// Longest custom range, in days (two years; Last 365 Days and This/Last Year fit). A link with a longer one gets the
3009// default: every day is a chart column and a table row.
3010const DASHBOARD_MAX_DAYS = 731;
3011const dashboardState = { projectId: '0', range: DASHBOARD_DEFAULT_RANGE, start: '', end: '' };
3012
3013async function loadDashboard() {
3014	if (checkIfLoginRequired())
3015		return;
3016
3017	const params = new URLSearchParams(window.location.search);
3018	const rangePreset = key => DASHBOARD_RANGES.find(r => r[0] === key);
3019	const urlDay = value => {
3020		const m = moment(value || '', 'YYYY-MM-DD', true);
3021		return m.isValid() ? m : null;
3022	};
3023
3024	// The range from the URL: a preset, or "custom" with two valid days; anything else is the default.
3025	let range = params.get('range');
3026	let start = urlDay(params.get('start'));
3027	let end = urlDay(params.get('end'));
3028	if (range === 'custom' && start && end && start.isAfter(end))
3029		[start, end] = [end, start];
3030	if (range !== 'custom' || !start || !end || end.diff(start, 'days') >= DASHBOARD_MAX_DAYS) {
3031		if (!rangePreset(range))
3032			range = DASHBOARD_DEFAULT_RANGE;
3033		[start, end] = rangePreset(range)[2]();
3034	}
3035	dashboardState.range = range;
3036	dashboardState.start = start.format('YYYY-MM-DD');
3037	dashboardState.end = end.format('YYYY-MM-DD');
3038	// Until the template list confirms it, the URL's template stands: a rewrite of the URL before that (the range
3039	// line below) must not drop ?id=, or a refresh after a failed list load would open All.
3040	dashboardState.projectId = params.get('id') || '0';
3041
3042	const rangeSelect = document.getElementById('date_range_select');
3043	for (const [key, label] of DASHBOARD_RANGES)
3044		rangeSelect.appendChild(new Option(translations[key] || label, key));
3045	rangeSelect.appendChild(new Option(translations['customRange'] || 'Custom Range', 'custom'));
3046	rangeSelect.value = range;
3047	rangeSelect.setAttribute('aria-label', translations['dateRange'] || 'Date range');
3048
3049	// The calendar opens from the range line, which turns into a button for a custom range (as in the Shopify app).
3050	const picker = $('#dashboard_range_button');
3051	picker.daterangepicker({
3052		opens: 'right',
3053		startDate: start,
3054		endDate: end,
3055		maxSpan: { days: DASHBOARD_MAX_DAYS - 1 },
3056		locale: {
3057			format: 'YYYY-MM-DD',
3058			applyLabel: translations['apply'],
3059			cancelLabel: translations['cancel'],
3060			daysOfWeek: [translations['sundayShort'], translations['mondayShort'], translations['tuesdayS
3060hort'], translations['wednesdayShort'], translations['thursdayShort'], translations['fridayShort'], translations['saturdayShort']],
3061			monthNames: [translations['january'], translations['february'], translations['march'], translations['april'], translations['may'], translations['june'], translations['july'], translations['august'], translations['september'], translations['october'], translations['november'], translations['december']],
3062			firstDay: 1,
3063		}
3064	}, function(s, e) {
3065		dashboardState.start = s.format('YYYY-MM-DD');
3066		dashboardState.end = e.format('YYYY-MM-DD');
3067		refreshDashboard();
3068	});
3069	const pickerData = picker.data('daterangepicker');
3070
3071	rangeSelect.addEventListener('change', function() {
3072		if (rangeSelect.value === 'custom') {
3073			// The days on screen stay until the calendar's Apply picks new ones.
3074			dashboardState.range = 'custom';
3075			showDashboardRange();
3076			pickerData.show();
3077			return;
3078		}
3079		// A preset picked while the calendar is still open (keyboard: Custom Range, then another option) closes it
3080		// without applying its days; hide() would apply half-picked ones.
3081		if (pickerData.isShowing)
3082			pickerData.clickCancel();
3083		const [s, e] = rangePreset(rangeSelect.value)[2]();
3084		dashboardState.range = rangeSelect.value;
3085		dashboardState.start = s.format('YYYY-MM-DD');
3086		dashboardState.end = e.format('YYYY-MM-DD');
3087		pickerData.setStartDate(s);
3088		pickerData.setEndDate(e);
3089		refreshDashboard();
3090	});
3091	showDashboardRange();
3092
3093	let projects = null;
3094	try {
3095		projects = await getProjects();
3096	} catch (e) {
3097		console.error('project/list', e);
3098	}
3099	if (!projects) {
3100		rangeSelect.disabled = true;
3101		document.getElementById('dashboard_kpis').replaceChildren(dashboardBanner(translations['statsLoadFailed']
3102			|| 'Unable to load statistics. Please try refreshing the page.', true));
3103		return;
3104	}
3105	if (!projects.length) {
3106		document.getElementById('dashboard_controls').classList.add('d-none');
3107		document.getElementById('date_range_text').classList.add('d-none');
3108		document.getElementById('dashboard_range_button').classList.add('d-none');
3109		const card = dashboardEl('section', 'fcd-card');
3110		const link = dashboardEl('div');
3111		link.id = 'dashboard_no_templates';
3112		card.appendChild(link);
3113		document.getElementById('dashboard_kpis').replaceChildren(card);
3114		return showTemplateCreateLink('dashboard_no_templates', !0);
3115	}
3116
3117	const projectDropdown = document.getElementById('select_project');
3118	projectDropdown.appendChild(new Option(translations['all'] || 'All', '0'));
3119	projects.forEach(project => projectDropdown.appendChild(new Option(project.projectName, String(project.id))));
3120	// ?id= comes from the templates list's Stats button, or from this page's own reload; an id that is not in the
3121	// list (a stale link, a deleted template) shows All instead of a page that cannot load.
3122	const selectedProjectId = params.get('id');
3123	projectDropdown.value = [...projectDropdown.options].some(o => o.value === selectedProjectId) ? selectedProjectId : '0';
3124	dashboardState.projectId = projectDropdown.value;
3125	// A pointer pick reloads at once. Arrow keys (Windows, Linux) and type-ahead fire 'change' on every step of a
3126	// closed select, so after a key press the reload waits for Enter or for the focus to leave.
3127	let keyboardChange = false;
3128	const openTemplate = () => {
3129		if (projectDropdown.value !== dashboardState.projectId)
3130			window.location.assign(dashboardUrl(projectDropdown.value));
3131	};
3132	projectDropdown.addEventListener('pointerdown', () => { keyboardChange = false; });
3133	projectDropdown.addEventListener('keydown', e => {
3134		if (e.key === 'Enter')
3135			openTemplate();
3136		else if (e.key !== 'Tab')
3137			keyboardChange = true;
3138	});
3139	projectDropdown.addEventListener('change', () => { if (!keyboardChange) openTemplate(); });
3140	projectDropdown.addEventListener('blur', openTemplate);
3141	// Back after a template switch can restore this page from the back/forward cache with the picker still on the
3142	// template the user left for; put the controls back to what this page shows.
3143	window.addEventListener('pageshow', e => {
3144		if (e.persisted) {
3145			projectDropdown.value = dashboardState.projectId;
3146			rangeSelect.value = dashboardState.range;
3147		}
3148	});
3149
3150	const exportDataBtn = document.getElementById('export_data');
3151	exportDataBtn.addEventListener('click', exportDashboardData);
3152	exportDataBtn.disabled = false;
3153
3154	refreshDashboard();
3155}
3156
3157// This page's URL for a template, carrying the range; other parameters (?lang=) are kept.
3158function dashboardUrl(projectId) {
3159	const params = new URLSearchParams(window.location.search);
3160	['id', 'range', 'start', 'end'].forEach(k => params.delete(k));
3161	if (projectId && projectId !== '0')
3162		params.set('id', projectId);
3163	params.set('range', dashboardState.range);
3164	if (dashboardState.range === 'custom') {
3165		params.set('start', dashboardState.start);
3166		params.set('end', dashboardState.end);
3167	}
3168	return window.location.pathname + '?' + params.toString();
3169}
3170
3171// The range under the title: plain text for a preset, the calendar button for a custom range.
3172function showDashboardRange() {
3173	const custom = dashboardState.range === 'custom';
3174	const start = dashboardDateStr(dashboardState.start);
3175	const end = dashboardDateStr(dashboardState.end);
3176	const text = document.getElementById('date_range_text');
3177	const button = document.getElementById('dashboard_range_button');
3178	text.querySelector('span').textContent = start + ' – ' + end;
3179	button.querySelector('span').textContent = start + ' - ' + end;
3180	text.classList.toggle('d-none', custom);
3181	button.classList.toggle('d-none', !custom);
3182	history.replaceState(null, '', dashboardUrl(dashboardState.projectId));
3183}
3184
3185function refreshDashboard() {
3186	showDashboardRange();
3187	const { projectId, start, end } = dashboardState;
3188	loadProjectStats(projectId, start, end);
3189	loadTrendStats(projectId, start, end);
3190	loadRegionStats(projectId, start, end);
3191	loadSourceStats(projectId, start, end);
3192}
3193
3194// The panel language as a BCP 47 tag (pt_br -> pt-BR) for Intl; <html> carries no lang attribute.
3195function dashboardLocale() {
3196	let tag = (typeof lang === 'string' && lang ? lang : 'en').replace('_', '-');
3197	const parts = tag.split('-');
3198	if (parts.length > 1)
3199		tag = parts[0] + '-' + parts[1].toUpperCase();
3200	try {
3201		return Intl.getCanonicalLocales(tag)[0] || 'en';
3202	} catch (e) {
3203		return 'en';
3204	}
3205}
3206
3207function dashboardFormat(value, options) {
3208	try {
3209		return new Intl.NumberFormat(dashboardLocale(), options).format(value);
3210	} catch (e) {
3211		return String(value);
3212	}
3213}
3214
3215function dashboardNumber(value) {
3216	return dashboardFormat(value);
3217}
3218
3219// part / total as a percentage with `digits` decimals; "0%" when there is nothing to divide by.
3220function dashboardPercent(part, total, digits = 1) {
3221	const d = total > 0 ? digits : 0;
3222	return dashboardFormat(total > 0 ? part / total : 0, { style: 'percent', minimumFractionDigits: d, maximumFractionDigits: d });
3223}
3224
3225// A YYYY-MM-DD day in the panel language. Parsed by moment in local time: new Date('2026-09-24') is UTC
3226// midnight, the previous day anywhere west of UTC.
3227function dashboardDateStr(day) {
3228	const m = moment(day, 'YYYY-MM-DD');
3229	try {
3230		return m.toDate().toLocaleDateString(dashboardLocale(), { year: 'numeric', month: 'short', day: 'numeric' });
3231	} catch (e) {
3232		return m.format('YYYY-MM-DD');
3233	}
3234}
3235
3236// createElement with a class and a text; the text always goes through textContent (house XSS rule).
3237function dashboardEl(tag, className, text) {
3238	const el = document.createElement(tag);
3239	if (className)
3240		el.className = className;
3241	if (text !== undefined && text !== null)
3242		el.textContent = text;
3243	return el;
3244}
3245
3246function dashboardBanner(text, critical) {
3247	const banner = dashboardEl('div', 'fcd-banner' + (critical ? ' fcd-critical' : ''));
3248	banner.setAttribute('role', critical ? 'alert' : 'status');
3249	const icon = dashboardEl('i', 'fcd-banner-icon fa-solid ' + (critical ? 'fa-circle-exclamation' : 'fa-circle-info'));
3250	icon.setAttribute('aria-hidden', 'true');
3251	banner.append(icon, dashboardEl('span', null, text));
3252	return banner;
3253}
3254
3255function dashboardLoadingCard() {
3256	const card = dashboardEl('section', 'fcd-card');
3257	const inner = dashboardEl('div', 'fcd-loading');
3258	inner.setAttribute('role', 'status');
3259	const spinner = dashboardEl('div', 'spinner-border text-secondary');
3260	spinner.setAttribute('aria-hidden', 'true');
3261	inner.append(spinner, dashboardEl('div', null, translations['loading'] || 'Loading...'));
3262	card.appendChild(inner);
3263	return card;
3264}
3265
3266function dashboardCardHead(title, badge) {
3267	const head = dashboardEl('div', 'fcd-between');
3268	head.appendChild(dashboardEl('h3', 'fcd-h-md', title));
3269	if (badge)
3270		head.appendChild(dashboardEl('span', 'fcd-badge fcd-badge-info', badge));
3271	return head;
3272}
3273
3274// rows: [label, cells[], indent]
3275function dashboardTable(heads, rows) {
3276	const table = dashboardEl('table', 'fcd-table');
3277	const thead = document.createElement('thead');
3278	const headRow = document.createElement('tr');
3279	heads.forEach(h => headRow.appendChild(dashboardEl('th', null, h)));
3280	thead.appendChild(headRow);
3281	const tbody = document.createElement('tbody');
3282	rows.forEach(([label, cells, indent]) => {
3283		const tr = document.createElement('tr');
3284		tr.appendChild(dashboardEl('td', indent ? 'fcd-indent' : null, label));
3285		cells.forEach(c => tr.appendChild(dashboardEl('td', null, c)));
3286		tbody.appendChild(tr);
3287	});
3288	table.append(thead, tbody);
3289	const wrap = dashboardEl('div', 'fcd-table-wrap');
3290	wrap.appendChild(table);
3291	return wrap;
3292}
3293
3294// The region and source tables' number columns: visitors or decisions, accepts, rejects, partials, accept rate.
3295// The rate is accepts over decisions — a partial accept is not an accept here (unlike the trend chart).
3296function dashboardDecisionCells(count, r) {
3297	const accepts = r.accepts || 0, rejects = r.rejects || 0, partials = r.partials || 0;
3298	const decisions = accepts + rejects + partials;
3299	return [
3300		dashboardNumber(count || 0),
3301		dashboardNumber(accepts),
3302		dashboardNumber(rejects),
3303		dashboardNumber(partials),
3304		decisions > 0 ? dashboardPercent(accepts, decisions, 0) : '—',
3305	];
3306}
3307
3308// Consent decisions over time (data-stats Days[]). The answer is keyed by template NAME with one entry per
3309// template ("All" = every template of the account), so the days are summed across entries.
3310let trendStatsSeq = 0;
3311// The chart is drawn at the card's width (see renderConsentTrend), so a new width — a rotated phone, the sidebar
3312// collapsed or opened — redraws it; the sidebar toggle fires no window resize, hence the observer.
3313let trendRedraw = null, trendResizeTimer = 0;
3314const trendResizeObserver = typeof ResizeObserver === 'function' ? new ResizeObserver(() => {
3315	clearTimeout(trendResizeTimer);
3316	trendResizeTimer = setTimeout(() => { if (trendRedraw) trendRedraw(); }, 150);
3317}) : null;
3318async function loadTrendStats(projectId, startDate, endDate) {
3319	const container = document.getElementById('trend_stats');
3320	if (!container) return;
3321	const seq = ++trendStatsSeq;
3322	trendRedraw = null;
3323	container.replaceChildren();
3324
3325	let body = null;
3326	try {
3327		const response = await postRequest(endpoint + '/cmp/data-stats',
3328			{ sid: getSid(), projectId: projectId, startDate: startDate, endDate: endDate });
3329		if (response && response.ok)
3330			body = await response.json();
3331	} catch (e) {
3332		console.error('data-stats', e);
3333	}
3334	if (seq !== trendStatsSeq) return;
3335	if (!body || !body.Projects) return;
3336
3337	const series = { days: [], accepts: [], partials: [], rejects: [] };
3338	const add = (target, metric) =>
3338 ((metric && metric.Days) || []).forEach((v, i) => {
3339		target[i] = (target[i] || 0) + (Number(v) || 0);
3340	});
3341	for (const name in body.Projects) {
3342		const p = body.Projects[name];
3343		add(series.accepts, p.consentAccepts);
3344		add(series.partials, p.consentPartiallyAccepts);
3345		add(series.rejects, p.consentRejects);
3346	}
3347	const n = Math.max(series.accepts.length, series.partials.length, series.rejects.length);
3348	if (!n) return;
3349	const first = moment(body.startDate || startDate, 'YYYY-MM-DD');
3350	for (let i = 0; i < n; i++)
3351		series.days.push(first.clone().add(i, 'days').format('YYYY-MM-DD'));
3352	const dashboard = document.getElementById('dashboard');
3353	let drawnWidth = dashboard.clientWidth;
3354	container.appendChild(renderConsentTrend(series, startDate, endDate, false));
3355	trendRedraw = () => {
3356		if (Math.abs(dashboard.clientWidth - drawnWidth) < 8) return;
3357		drawnWidth = dashboard.clientWidth;
3358		const table = container.querySelector('.fcd-table-wrap');
3359		container.replaceChildren(renderConsentTrend(series, startDate, endDate, !!table && !table.hidden));
3360	};
3361	if (trendResizeObserver)
3362		trendResizeObserver.observe(dashboard);
3363}
3364
3365// A port of the Shopify app's FlexConsentTrend: an accept RATE chart and a decision VOLUME chart — never one chart
3366// with a second y-axis — and a table view, so no reading depends on colour alone.
3367function renderConsentTrend(series, startDate, endDate, showTable) {
3368	const NS = 'http://www.w3.org/2000/svg';
3369	// Drawn at the card's own width, so the 11px axis labels stay 11px instead of shrinking with the viewBox on a
3370	// phone or growing on a wide screen.
3371	const cardWidth = (document.getElementById('dashboard') || {}).clientWidth - 32;
3372	const W = Math.round(Math.max(320, cardWidth || 720)), H = W >= 600 ? 210 : 170, PAD = { l: 44, r: 14, t: 12, b: 26 };
3373	const plotW = W - PAD.l - PAD.r, plotH = H - PAD.t - PAD.b;
3374	const svgEl = (tag, attrs, text) => {
3375		const el = document.createElementNS(NS, tag);
3376		for (const k in attrs)
3377			el.setAttribute(k, attrs[k]);
3378		if (text !== undefined)
3379			el.textContent = text;
3380		return el;
3381	};
3382
3383	// A partial accept counts as an accept here: the visitor allowed something. A day with no decisions has no
3384	// rate (null), not 0% — a 0 would read as a collapse in consent rather than an absence of traffic.
3385	const rows = series.days.map((day, i) => {
3386		const a = series.accepts[i] || 0, p = series.partials[i] || 0, r = series.rejects[i] || 0;
3387		const total = a + p + r;
3388		return { day, a, p, r, total, rate: total > 0 ? (a + p) / total * 100 : null };
3389	});
3390	const peak = Math.max(1, ...rows.map(x => x.total));
3391	const magnitude = Math.pow(10, Math.floor(Math.log10(peak)));
3392	let maxTotal = Math.ceil(peak / magnitude) * magnitude;
3393	// The middle gridline is maxTotal / 2, labelled as a whole number: keep maxTotal even.
3394	if (maxTotal % 2)
3395		maxTotal += 1;
3396	const n = rows.length;
3397	const bandW = Math.max(1, Math.min(28, plotW / n * 0.7));
3398	// Day centres are inset by half a bar, so the first and last bars stay off the y-axis labels.
3399	const innerW = plotW - bandW;
3400	const xOf = i => PAD.l + bandW / 2 + (n === 1 ? innerW / 2 : i * innerW / (n - 1));
3401	// One hit slot per day, ending halfway to the neighbouring days and kept inside the plot, so the pointer always
3402	// lands on the nearest day.
3403	const slotW = n > 1 ? innerW / (n - 1) : plotW;
3404	const hitL = i => Math.max(PAD.l, xOf(i) - slotW / 2), hitR = i => Math.min(W - PAD.r, xOf(i) + slotW / 2);
3405	// One label every ceil(n/7) days, so the ticks never collide whatever the range.
3406	const tickEvery = Math.max(1, Math.ceil(n / 7));
3407	let tickFormat = null;
3408	try {
3409		tickFormat = new Intl.DateTimeFormat(dashboardLocale(), { day: 'numeric', month: 'numeric' });
3410	} catch (e) { /* very old browser: DD/MM below */ }
3411	const tickLabel = day => {
3412		const [, mm, dd] = day.split('-');
3413		return tickFormat ? tickFormat.format(moment(day, 'YYYY-MM-DD').toDate()) : dd + '/' + mm;
3414	};
3415	const addTicks = svg => rows.forEach((r, i) => {
3416		if (i % tickEvery === 0)
3417			svg.appendChild(svgEl('text', { class: 'ax', x: xOf(i), y: H - 8, 'text-anchor': 'middle' }, tickLabel(r.day)));
3418	});
3419	const legend = [
3420		['a', translations['accepts'] || 'Accepted', 'var(--fcd-s1)'],
3421		['p', translations['partiallyAccepts'] || 'Partially accepted', 'var(--fcd-s2)'],
3422		['r', translations['rejects'] || 'Rejected', 'var(--fcd-s3)'],
3423	];
3424	const swatch = color => {
3425		const sw = dashboardEl('span', 'fcd-swatch');
3426		sw.style.background = color;
3427		return sw;
3428	};
3429
3430	const card = dashboardEl('section', 'fcd-card fcd-stack fcd-trend');
3431	const head = dashboardEl('div', 'fcd-between');
3432	const toggle = dashboardEl('button', 'fcd-link', translations['trendShowTable'] || 'Show table');
3433	toggle.type = 'button';
3434	head.append(dashboardEl('h3', 'fcd-h-md', translations['trendTitle'] || 'Consent over time'), toggle);
3435	card.appendChild(head);
3436
3437	if (!rows.some(x => x.total > 0)) {
3438		toggle.hidden = true;
3439		card.appendChild(dashboardEl('p', 'fcd-muted',
3440			translations['trendNoDecisions'] || 'No consent decisions were recorded in this period.'));
3441	} else {
3442		const chart = dashboardEl('div', 'fcd-stack');
3443		chart.style.gap = '20px';
3444
3445		// Accept rate: one series, so the heading names it instead of a legend.
3446		const rateBox = dashboardEl('div');
3447		rateBox.appendChild(dashboardEl('h4', 'fcd-h-sm fcd-muted', translations['acceptRate'] || 'Accept rate'));
3448		const rateSvg = svgEl('svg', { viewBox: '0 0 ' + W + ' ' + H, role: 'img',
3449			'aria-label': translations['trendAcceptRateAria'] || 'Accept rate over time' });
3450		[0, 25, 50, 75, 100].forEach(g => {
3451			const y = PAD.t + plotH - g / 100 * plotH;
3452			rateSvg.append(svgEl('line', { class: 'g', x1: PAD.l, x2: W - PAD.r, y1: y, y2: y }),
3453				svgEl('text', { class: 'ax', x: PAD.l - 8, y: y + 4, 'text-anchor': 'end' }, dashboardPercent(g, 100, 0)));
3454		});
3455		// The line breaks across days with no decisions instead of drawing through them; a day on its own
3456		// between two gaps is a dot, since a one-point line draws nothing.
3457		const points = rows.map((r, i) => ({ r, x: xOf(i), y: r.rate === null ? null : PAD.t + plotH - r.rate / 100 * plotH }));
3458		let segment = [];
3459		const flush = () => {
3460			if (segment.length === 1)
3461				rateSvg.appendChild(svgEl('circle', { cx: segment[0].x, cy: segment[0].y, r: 2, fill: 'var(--fcd-s1)' }));
3462			else if (segment.length > 1)
3463				rateSvg.appendChild(svgEl('polyline', { fill: 'none', stroke: 'var(--fcd-s1)', 'stroke-width': 2,
3464					'stroke-linecap': 'round', 'stroke-linejoin': 'round', points: segment.map(p => p.x + ',' + p.y).join(' ') }));
3465			segment = [];
3466		};
3467		points.forEach(p => p.y === null ? flush() : segment.push(p));
3468		flush();
3469		const last = points.slice().reverse().find(p => p.y !== null);
3470		if (last) {
3471			rateSvg.appendChild(svgEl('circle', { cx: last.x, cy: last.y, r: 4, fill: 'var(--fcd-s1)',
3472				stroke: 'var(--fcd-surface)', 'stroke-width': 2 }));
3473			const label = svgEl('text', { class: 'ax', x: Math.min(last.x + 8, W - PAD.r), y: last.y - 8, 'text-anchor': 'end' },
3474				dashboardPercent(last.r.rate, 100, 0));
3475			label.style.fontWeight = '600';
3476			rateSvg.appendChild(label);
3477		}
3478		addTicks(rateSvg);
3479		rateBox.appendChild(rateSvg);
3480
3481		// Decision volume: three series, so a legend is always present.
3482		const volumeBox = dashboardEl('div');
3483		const volumeHead = dashboardEl('div', 'fcd-between');
3484		const legendEl = dashboardEl('div', 'fcd-legend');
3485		legend.forEach(([, label, color]) => {
3486			const item = dashboardEl('span', 'fcd-inline fcd-sm fcd-muted');
3487			item.style.gap = '6px';
3488			item.append(swatch(color), document.createTextNode(label));
3489			legendEl.appendChild(item);
3490		});
3491		volumeHead.append(dashboardEl('h4', 'fcd-h-sm fcd-muted', translations['trendDecisions'] || 'Decisions per day'), legendEl);
3492		const plot = dashboardEl('div');
3493		plot.style.position = 'relative';
3494		const volumeSvg = svgEl('svg', { viewBox: '0 0 ' + W + ' ' + H, role: 'img',
3495			'aria-label': translations['trendDecisionsAria'] || 'Consent decisions per day' });
3496		[0, 0.5, 1].forEach(f => {
3497			const y = PAD.t + plotH - f * plotH;
3498			volumeSvg.append(svgEl('line', { class: 'g', x1: PAD.l, x2: W - PAD.r, y1: y, y2: y }),
3499				svgEl('text', { class: 'ax', x: PAD.l - 8, y: y + 4, 'text-anchor': 'end' }, dashboardNumber(Math.round(maxTotal * f))));
3500		});
3501		const tip = dashboardEl('div', 'fcd-tip');
3502		tip.hidden = true;
3503		const showTip = i => {
3504			const r = rows[i], at = xOf(i) / W;
3505			const day = dashboardEl('div', 'fcd-strong', dashboardDateStr(r.day));
3506			day.style.marginBottom = '4px';
3507			tip.replaceChildren(day);
3508			legend.forEach(([k, label, color]) => {
3509				const line = dashboardEl('div', 'fcd-inline');
3510				line.style.gap = '6px';
3511				const text = dashboardEl('span', null, label + ': ');
3512				text.appendChild(dashboardEl('b', null, dashboardNumber(r[k])));
3513				line.append(swatch(color), text);
3514				tip.appendChild(line);
3515			});
3516			// Kept inside the card at both ends: a tip centred over the first or last day would hang off the page.
3517			tip.style.left = (at * 100) + '%';
3518			tip.style.transform = at < .15 ? 'translate(0,-100%)' : at > .85 ? 'translate(-100%,-100%)' : 'translate(-50%,-100%)';
3519			tip.hidden = false;
3520		};
3521		rows.forEach((r, i) => {
3522			const x = xOf(i) - bandW / 2;
3523			let y = PAD.t + plotH;
3524			// Bottom to top in the legend's left-to-right order; a 2px gap between segments instead of a border.
3525			[[r.a, 'var(--fcd-s1)'], [r.p, 'var(--fcd-s2)'], [r.r, 'var(--fcd-s3)']].forEach(([v, color]) => {
3526				if (v <= 0) return;
3527				const h = Math.max(1, v / maxTotal * plotH - 2);
3528				y -= h;
3529				volumeSvg.appendChild(svgEl('rect', { x: x, y: y, width: bandW, height: h, fill: color, rx: 2 }));
3530				y -= 2;
3531			});
3532			const hit = svgEl('rect', { class: 'hit', x: hitL(i), y: PAD.t, width: hitR(i) - hitL(i), height: plotH });
3533			hit.addEventListener('mouseenter', () => showTip(i));
3534			hit.addEventListener('click', () => showTip(i));
3535			volumeSvg.appendChild(hit);
3536		});
3537		addTicks(volumeSvg);
3538		volumeSvg.addEventListener('mouseleave', () => { tip.hidden = true; });
3539		plot.append(volumeSvg, tip);
3540		volumeBox.append(volumeHead, plot);
3541		chart.append(rateBox, volumeBox);
3542
3543		const tableRows = rows.map(r => [dashboardDateStr(r.day), [dashboardNumber(r.a), dashboardNumber(r.p),
3544			dashboardNumber(r.r), r.rate === null ? '—' : dashboardPercent(r.rate, 100, 0)]]);
3545		const table = dashboardTable([translations['date'] || 'Date', legend[0][1], legend[1][1], legend[2][1],
3546			translations['acceptRate'] || 'Accept rate'], tableRows);
3547		table.hidden = !showTable;
3548		chart.hidden = !!showTable;
3549		if (showTable)
3550			toggle.textContent = translations['trendShowChart'] || 'Show chart';
3551		toggle.addEventListener('click', () => {
3552			const showTable = table.hidden;
3553			table.hidden = !showTable;
3554			chart.hidden = showTable;
3555			tip.hidden = true;
3556			toggle.textContent = showTable ? (translations['trendShowChart'] || 'Show chart')
3557				: (translations['trendShowTable'] || 'Show table');
3558		});
3559		card.append(chart, table);
3560	}
3561
3562	card.appendChild(dashboardEl('p', 'fcd-sm fcd-muted', dashboardDateStr(startDate) + ' – ' + dashboardDateStr(endDate)));
3563	return card;
3564}
3565
3566// Export: the server's per-visitor-day CSV for the template (or all templates) and the range on screen.
3567async function exportDashboardData() {
3568	const button = document.getElementById('export_data');
3569	button.disabled = true;
3570	try {
3571		const data = { sid: getSid(), projectId: dashboardState.projectId, startDate: dashboardState.start, endDate: dashboardState.end };
3572		const response = await postRequest(endpoint + '/cmp/export-data-stats', data);
3573		if (response === undefined)
3574			return; // 401: postRequest is already logging out
3575		const blob = response && response.ok ? await response.blob() : null;
3576		// text/csv;charset=UTF-8: doGetPost sets the charset before the command sets the type, so the blob type
3577		// carries it and an exact 'text/csv' comparison refused every file.
3578		if (!blob || !blob.type.startsWith('text/csv')) {
3579			showAlert(translations['unknownError'] || 'Unknown error, please try later', 'error');
3580			return;
3581		}
3582		const disposition = response.headers.get('Content-Disposition');
3583		const match = disposition && disposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/);
3584		const a = document.createElement('a');
3585		a.href = URL.createObjectURL(blob);
3586		a.download = match && match[1] ? match[1].replace(/['"]/g, '') : 'ConsentStats.csv';
3587		document.body.appendChild(a);
3588		a.click();
3589		document.body.removeChild(a);
3590		setTimeout(() => URL.revokeObjectURL(a.href), 1000);
3591	} catch (e) {
3592		console.error('export-data-stats', e);
3593		showAlert(translations['unknownError'] || 'Unknown error, please try later', 'error');
3594	} finally {
3595		button.disabled = false;
3596	}
3597}
3598
3599function showTemplateCreateLink(container, addLink) {
3600	var pc = document.getElementById(container);
3601	if (pc) {
3602		const noProjectsFound = translations['noTemplatesFound'] || 'No templates found.';
3603		const createProject = translations['clickHereToCreateATemplate'] || 'Click here to create a template';
3604		pc.innerHTML = noProjectsFound + (addLink ? ' <a href="template_settings.jsp?create=1">' + createProject + '</a>' : '');
3605		// pc.innerHTML = 'No projects found.' + (addLink ? ' <a href="template_settings.jsp?create=1">Click here to create a project</a>' : '');
3606		pc.className = 'no-projects-link';
3607	}
3608}
3609
3610function showWebsiteCreateLink(containerId) {
3611	var tc = document.getElementById(containerId);
3612	if (tc) {
3613		const noWsTxt = translations['noWebsitesFound'] || 'No websites found.';
3614		const createWsTxt = translations['clickHereToCreateAWebsite'] || 'Click here to create a website';
3615		tc.innerHTML = noWsTxt + ' <a href="website_settings.jsp">' + createWsTxt + '</a>';
3616		tc.className = 'no-projects-link';
3617	}
3618}
3619
3620
3621// Consent-by-region table (data-stats-regions). Renders nothing for the "All"
3622// selector — the endpoint is per-project, matching its auth check. All cell
3623// text goes through textContent (house XSS rule); country names come from the
3624// browser's own Intl.DisplayNames in the panel language, so they need no
3625// entries in the translation sheet.
3626let regionStatsSeq = 0;
3627async function loadRegionStats(projectId, startDate, endDate) {
3628	const container = document.getElementById('region_stats');
3629	if (!container) return;
3630	// Sequence guard: rapid project/date switches leave earlier requests in
3631	// flight, and a stale response must neither append a second table nor
3632	// fill the card after a switch to "All". Every call — including the "All"
3633	// early return — claims the sequence, so anything older is discarded when
3634	// it lands. The card hides while empty (.fcd > :empty).
3635	const seq = ++regionStatsSeq;
3636	container.replaceChildren();
3637	if (!projectId || projectId === '0') return;
3638
3639	const response = await postRequest(endpoint + '/cmp/data-stats-regions',
3640		{ sid: getSid(), projectId: projectId, startDate: startDate, endDate: endDate });
3641	if (seq !== regionStatsSeq) return;
3642	// postRequest resolves null on non-2xx (and undefined on 401) — there is
3643	// no .ok to read on the failure it signals.
3644	if (!response || !response.ok) return;
3645	const body = await response.json();
3646	if (seq !== regionStatsSeq) return;
3647	if (body.statusCode !== 0) return;
3648
3649	const regions = body.regions || [];
3650	const hasAny = regions.length > 0 || body.other || body.unknown;
3651
3652	const card = dashboardEl('section', 'fcd-card fcd-stack-8');
3653	card.appendChild(dashboardEl('h3', 'fcd-h-sm', translations['regionStatsTitle'] || 'Consent by region'));
3654
3655	if (!hasAny) {
3656		card.appendChild(dashboardEl('p', 'fcd-muted', translations['regionNoData']
3657			|| 'No regional data for this range yet. Locations are recorded from new visits going forward.'));
3658		container.replaceChildren(card);
3659		return;
3660	}
3661
3662	let regionName = (code) => code.toUpperCase();
3663	try {
3664		const dn = new Intl.DisplayNames([dashboardLocale()], { type: 'region' });
3665		regionName = (code) => { try { return dn.of(code.toUpperCase()) || code.toUpperCase(); } catch (e) { return code.toUpperCase(); } };
3666	} catch (e) { /* very old browser: fall back to the bare code */ }
3667
3668	const other = translations['otherRegions'] || 'Other';
3669	const rows = [];
3670	for (const r of regions) {
3671		rows.push([regionName(r.country), dashboardDecisionCells(r.clients, r), false]);
3672		for (const st of (r.states || []))
3673			rows.push([st.state === 'other' ? other + ' (' + dashboardNumber(st.statesFolded) + ')' : st.state.toUpperCase(),
3674				dashboardDecisionCells(st.clients, st), true]);
3675	}
3676	if (body.other)
3677		rows.push([other + ' (' + dashboardNumber(body.other.countries) + ')', dashboardDecisionCells(body.other.clients, body.other), false]);
3678	if (body.unknown)
3679		rows.push([translations['unknownRegion'] || 'Unknown', dashboardDecisionCells(body.unknown.clients, body.unknown), false]);
3680
3681	card.appendChild(dashboardTable([
3682		translations['country'] || 'Country',
3683		translations['visitors'] || 'Visitors',
3684		translations['consentAccepts'] || 'Accepts',
3685		translations['consentRejects'] || 'Rejects',
3686		translations['consentPartiallyAccepts'] || 'Partial',
3687		translations['acceptRate'] || 'Accept rate',
3688	], rows));
3689	card.appendChild(dashboardEl('p', 'fcd-sm fcd-muted', translations['regionFoldNote']
3690		|| 'Countries and states with only a few visitors are grouped as "Other", so no single visitor can be identified from this table.'));
3691	container.replaceChildren(card);
3692}
3693
3694// ---------------------------------------------------------------------------------------------------------
3695// J90/J91: where each consent decision came from (the J86 `source`: banner / preferences / embed / page /
3696// automatic, "" = not recorded). The panel is the reference front end (J92): the Shopify app and the
3697// WordPress plugin use the same labels, columns and order.
3698function consentSourceLabel(source) {
3699	switch (source) {
3700		case 'banner': return translations['sourceBanner'] || 'Banner';
3701		case 'preferences': return translations['sourcePreferences'] || 'Preferences screen';
3702		case 'embed': return translations['sourceEmbed'] || 'Blocked video or embed';
3703		case 'page': return translations['sourcePage'] || "Your site's own link or button";
3704		case 'automatic': return translations['sourceAutomatic'] || 'Automatic (Global Privacy Control)';
3705		default: return translations['sourceNotRecorded'] || 'Not recorded';
3706	}
3707}
3708
3709// J109/J110: whether the US Global Privacy Control opt-out was in force for the decision. null = not recorded (every row
3710// before 2026-09-25, and saves from older bundles).
3711function consentGpcLabel(gpc) {
3712	if (gpc === true) return translations['gpcApplied'] || 'Applied';
3713	if (gpc === false) return translations['gpcNotApplied'] || 'Not applied';
3714	return translations['sourceNotRecorded'] || 'Not recorded';
3715}
3716
3717function consentStateLabel(state) {
3718	switch (state) {
3719		case 'ALL_ACCEPTED': return translations['consentStateAllAccepted'] || 'All Accepted';
3720		case 'ALL_REJECTED': return translations['consentStateAllRejected'] || 'All Rejected';
3721		case 'PARTIALLY_ACCEPTED': return translations['consentStatePartiallyAccepted'] || 'Partially Accepted';
3722		case 'NOT_REQUIRED': return translations['consentStateNotRequired'] || 'Not Required';
3723		case 'PENDING': return translations['consentStatePending'] || 'Pending';
3724		default: return state || '';
3725	}
3726}
3727
3728// Decisions-by-source card (data-stats-sources), beside the region card. Per project, like regions.
3729let sourceStatsSeq = 0;
3730async function loadSourceStats(projectId, startDate, endDate) {
3731	const container = document.getElementById('source_stats');
3732	if (!container) return;
3733	const seq = ++sourceStatsSeq;
3734	container.replaceChildren();
3735	if (!projectId || projectId === '0') return;
3736
3737	const response = await postRequest(endpoint + '/cmp/data-stats-sources',
3738		{ sid: getSid(), projectId: projectId, startDate: startDate, endDate: endDate });
3739	if (seq !== sourceStatsSeq) return;
3740	if (!response || !response.ok) return;
3741	const body = await response.json();
3742	if (seq !== sourceStatsSeq) return;
3743	if (body.statusCode !== 0) return;
3744
3745	const sources = body.sources || [];
3746	const card = dashboardEl('section', 'fcd-card fcd-stack-8');
3747	card.appendChild(dashboardEl('h3', 'fcd-h-sm', translations['sourceStatsTitle'] || 'Decisions by source'));
3748
3749	if (!sources.length) {
3750		card.appendChild(dashboardEl('p', 'fcd-muted', translations['sourceStatsNoData'] || 'No consent decisions in this date range.'));
3751		container.replaceChildren(card);
3752		return;
3753	}
3754
3755	card.appendChild(dashboardTable([
3756		translations['source'] || 'Source',
3757		translations['decisions'] || 'Decisions',
3758		translations['consentAccepts'] || 'Accepts',
3759		translations['consentRejects'] || 'Rejects',
3760		translations['consentPartiallyAccepts'] || 'Partial',
3761		translations['acceptRate'] || 'Accept rate',
3762	], sources.map(r => [consentSourceLabel(r.source), dashboardDecisionCells(r.decisions, r), false])));
3763	card.appendChild(dashboardEl('p', 'fcd-sm fcd-muted', translations['sourceStatsNote']
3764		|| 'Each visitor counts once per day, under the source of their last decision that day. "Not recorded" means the decision was made before 24 September 2026 or through an older cached version of the banner.'));
3765	container.replaceChildren(card);
3766}
3767
3768// Consent log page (consent-logs): the newest decisions first, 40 at a time, one row per visitor-day with the
3769// last decision of that day. "Load more" pages back with idLimit; the CSV holds the rows loaded so far.
3770const CONSENT_LOG_PAGE = 40;
3771const consentLogView = { rows: [], lastId: 0, seq: 0 };
3772
3773async function loadConsentLogs() {
3774	const select = document.getElementById('select_project');
3775	const projects = await getProjects();
3776	if (!projects || !projects.length) {
3777		showTemplateCreateLink('consent_logs', !0);
3778		return;
3779	}
3780	projects.forEach(project => {
3781		const option = document.createElement('option');
3782		option.value = project.id;
3783		option.text = project.projectName;
3784		select.appendChild(option);
3785	});
3786	const wanted = new URLSearchParams(window.location.search).get('id');
3787	if (wanted && projects.some(p => String(p.id) === wanted)) select.value = wanted;
3788	select.classList.remove('d-none');
3789	select.addEventListener('change', () => fetchConsentLogs(true));
3790	document.getElementById('consent_logs_more').addEventListener('click', () => fetchConsentLogs(false));
3791	document.getElementById('consent_logs_export').addEventListener('click', exportConsentLogsCsv);
3792	await fetchConsentLogs(true);
3793}
3794
3795// The API sends the report day as a bare "YYYY-MM-DD". new Date() reads that as UTC midnight, which is the
3796// PREVIOUS day anywhere west of UTC — so a calendar date is built from its parts, in local time. Formatted in
3797// the panel language, like the dashboard's range picker.
3798function consentLogDate(value) {
3799	const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(String(value || ''));
3800	const d = m ? new Date(+m[1], +m[2] - 1, +m[3]) : new Date(value);
3801	if (isNaN(d.getTime())) return String(value || '');
3802	let locale;
3803	if (typeof lang === 'string' && lang) {
3804		const p = lang.replace('-', '_').split('_');
3805		locale = p.length > 1 ? p[0] + '-' + p[1].toUpperCase() : p[0];
3806	}
3807	try {
3808		return d.toLocaleDateString(locale, { year: 'numeric', month: 'short', day: '2-digit' });
3809	} catch (e) {
3810		return d.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: '2-digit' });
3811	}
3812}
3813
3814async function fetchConsentLogs(reset) {
3815	const select = document.getElementById('select_project');
3816	const tbody = document.querySelector('#consent_logs_table tbody');
3817	const more = document.getElementById('consent_logs_more');
3818	const exportBtn = document.getElementById('consent_logs_export');
3819	const empty = document.getElementById('consent_logs_empty');
3820	const seq = ++consentLogView.seq;
3821	if (reset) {
3822		// A new project starts from nothing: no rows, and no buttons that belong to the previous one.
3823		consentLogView.rows = [];
3824		consentLogView.lastId = 0;
3825		tbody.innerHTML = '';
3826		more.classList.add('d-none');
3827		exportBtn.classList.add('d-none');
3828		empty.classList.add('d-none');
3829	}
3830	more.disabled = true;
3831	const data = { sid: getSid(), projectId: select.value, limit: CONSENT_LOG_PAGE };
3832	if (!reset && consentLogView.lastId) data.idLimit = consentLogView.lastId;
3833	const failed = (message) => {
3834		more.disabled = false;
3835		showAlert(message || translations['unknownError'] || 'Unknown error, please try later', 'error');
3836	};
3837	const response = await postRequest(endpoint + '/cmp/consent-logs', data);
3838	if (seq !== consentLogView.seq) return;
3839	if (!response) return failed();
3840	let body;
3841	try {
3842		body = await response.json();
3843	} catch (e) {
3844		body = null;
3845	}
3846	if (seq !== consentLogView.seq) return;
3847	if (!body || body.statusCode !== 0) return failed(body && body.statusMessage);
3848	more.disabled = false;
3849	const logs = body.logs || [];
3850	for (const log of logs) {
3851		consentLogView.rows.push(log);
3852		const tr = document.createElement('tr');
3853		const cells = [consentLogDate(log.date), log.domain || '', log.clientId || '', consentStateLabel(log.consentState),
3854			consentSourceLabel(log.source), consentGpcLabel(log.gpcApplied), String(log.pageViews || 0)];
3855		cells.forEach((text, i) => {
3856			const td = document.createElement('td');
3857			td.textContent = text;
3858			if (i === 2) td.classList.add('text-break', 'small');
3859			if (i === cells.length - 1) td.classList.add('text-end');
3860			tr.appendChild(td);
3861		});
3862		tbody.appendChild(tr);
3863		if (!consentLogView.lastId || log.id < consentLogView.lastId) consentLogView.lastId = log.id;
3864	}
3865	more.classList.toggle('d-none', logs.length < CONSENT_LOG_PAGE);
3866	exportBtn.classList.toggle('d-none', consentLogView.rows.length === 0);
3867	empty.classList.toggle('d-none', consentLogView.rows.length > 0);
3868}
3869
3870function exportConsentLogsCsv() {
3871	const rows = consentLogView.rows;
3872	if (!rows.length) return;
3873	const q = (v) => {
3874		const t = String(v == null ? '' : v);
3875		// Leading = + - @ would run as a formula in a spreadsheet; a quote in front keeps it text.
3876		const safe = /^[=+\-@]/.test(t) ? "'" + t : t;
3877		return '"' + safe.replace(/"/g, '""') + '"';
3878	};
3879	const head = [translations['date'] || 'Date', translations['domain'] || 'Domain', translations['clientId'] || 'Client ID',
3880		translations['consentState'] || 'Consent State', translations['source'] || 'Source',
3881		translations['gpcOptOutColumn'] || 'GPC opt-out', translations['pageViewsColumn'] || 'Page Views'];
3882	const lines = [head.map(q).join(',')];
3883	// The raw ISO day in the file: unambiguous for any spreadsheet, whatever its locale.
3884	for (const log of rows)
3885		lines.push([String(log.date || ''), log.domain || '', log.clientId || '', consentStateLabel(log.consentState),
3886			consentSourceLabel(log.source), consentGpcLabel(log.gpcApplied), log.pageViews || 0].map(q).join(','));
3887	const blob = new Blob(['\ufeff' + lines.join('\r\n')], { type: 'text/csv;charset=utf-8' });
3888	const a = document.createElement('a');
3889	a.href = URL.createObjectURL(blob);
3890	const project = document.getElementById('select_project');
3891	const name = (project.options[project.selectedIndex] || {}).text || 'consent-log';
3892	a.download = 'consent-log_' + name.replace(/[^A-Za-z0-9._-]+/g, '_') + '_' + new Date().toISOString().slice(0, 10) + '.csv';
3893	document.body.appendChild(a);
3894	a.click();
3895	document.body.removeChild(a);
3896	setTimeout(() => URL.revokeObjectURL(a.href), 1000);
3897}
3898
3899// KPI tiles, consent overview, visits by consent and pageviews (data-stats-simple), as in the Shopify app.
3900let projectStatsSeq = 0;
3901async function loadProjectStats(projectId, startDate, endDate) {
3902	const kpis = document.getElementById('dashboard_kpis');
3903	const consent = document.getElementById('dashboard_consent');
3904	const notice = document.getElementById('dashboard_notice');
3905	// A slow answer for an earlier range must not overwrite the one on screen.
3906	const seq = ++projectStatsSeq;
3907	consent.replaceChildren();
3908	notice.replaceChildren();
3909	kpis.replaceChildren(dashboardLoadingCard());
3910
3911	let body = null;
3912	try {
3913		const response = await postRequest(endpoint + '/cmp/data-stats-simple',
3914			{ sid: getSid(), projectId: projectId, startDate: startDate, endDate: endDate });
3915		if (response && response.ok)
3916			body = await response.json();
3917	} catch (e) {
3918		// a failed command answers 200 with an empty body, which json() rejects
3919		console.error('data-stats-simple', e);
3920	}
3921	if (seq !== projectStatsSeq) return;
3922	if (!body || body.statusCode !== 0) {
3923		kpis.replaceChildren(dashboardBanner(translations['statsLoadFailed']
3924			|| 'Unable to load statistics. Please try refreshing the page.', true));
3925		return;
3926	}
3927
3928	// An admin may pick a template of another account: data-stats-simple only covers the caller's own templates, so
3929	// the answer has no entry for it. Say so rather than show zeros and "No data yet".
3930	const stats = body.Projects && body.Projects['' + projectId];
3931	if (!stats) {
3932		kpis.replaceChildren(dashboardBanner(translations['statsNotAvailable'] || 'Statistics are not available for this template.'));
3933		return;
3934	}
3935	const n = key => Number(stats[key]) || 0;
3936	const visits = n('visitsTotal'), pageViews = n('pageViewsTotal'), requests = n('consentRequests');
3937
3938	if (visits === 0)
3939		notice.appendChild(dashboardBanner(translations['noDataYet'] || 'No data yet for the selected period.'));
3940
3941	const tiles = dashboardEl('div', 'fcd-grid fcd-grid-4');
3942	const perVisit = visits > 0 ? pageViews / visits : 0;
3943	[
3944		[translations['visitsTotal'] || 'Total visitors', visits, translations['visitsTotalSub'] || 'All visitors'],
3945		[translations['visitsNewClient'] || 'New visitors', n('visitsNewClient'), translations['visitsNewClientSub'] || 'First-time'],
3946		[translations['visitsExistingClient'] || 'Returning visitors', n('visitsExistingClient'), translations['visitsExistingClientSub'] || 'Repeat visitors'],
3947		[translations['pageViewsTotal'] || 'Total pageviews', pageViews,
3948			dashboardFormat(perVisit, { minimumFractionDigits: 1, maximumFractionDigits: 1 }) + ' ' + (translations['avgPerVisitShort'] || 'avg')],
3949	].forEach(([title, value, sub]) => {
3950		const card = dashboardEl('section', 'fcd-card fcd-stack');
3951		card.append(dashboardEl('div', 'fcd-muted', title), dashboardEl('div', 'fcd-2xl', dashboardNumber(value)),
3952			dashboardEl('div', 'fcd-sm fcd-muted', sub));
3953		tiles.appendChild(card);
3954	});
3955	kpis.replaceChildren(tiles);
3956
3957	// Shares of the decisions, not of consentRequests: that counts the visitor-days the banner was shown, and decisions
3958	// made without it that day (the preferences link, an embed) push those shares past 100%. The Shopify app does the
3959	// same (consentDecisionShares).
3960	const decided = n('consentAccepts') + n('consentRejects') + n('consentPartiallyAccepts');
3961	const overview = dashboardEl('section', 'fcd-card fcd-stack');
3962	overview.appendChild(dashboardCardHead(translations['consentOverview'] || 'Consent overview',
3963		dashboardNumber(requests) + ' ' + (translations['requestsBadge'] || 'requests')));
3964	const decisions = dashboardEl('div', 'fcd-stack-12');
3965	[
3966		[translations['accepts'] || 'Accepted', 'success', n('consentAccepts')],
3967		[translations['rejects'] || 'Rejected', 'attention', n('consentRejects')],
3968		[translations['partiallyAccepts'] || 'Partially accepted', 'warning', n('consentPartiallyAccepts')],
3969	].forEach(([label, tone, value]) => {
3970		const row = dashboardEl('div', 'fcd-stack-8');
3971		const line = dashboardEl('div', 'fcd-between');
3972		const left = dashboardEl('div', 'fcd-inline');
3973		left.append(dashboardEl('span', 'fcd-badge fcd-badge-' + tone, label), dashboardEl('span', 'fcd-strong', dashboardNumber(value)));
3974		line.append(left, dashboardEl('span', 'fcd-muted', dashboardPercent(value, decided)));
3975		const share = decided > 0 ? value / decided * 100 : 0;
3976		const bar = dashboardEl('div', 'fcd-progress');
3977		bar.setAttribute('role', 'progressbar');
3978		bar.setAttribute('aria-label', label);
3979		bar.setAttribute('aria-valuemin', '0');
3980		bar.setAttribute('aria-valuemax', '100');
3981		bar.setAttribute('aria-valuenow', String(Math.round(share)));
3982		const fill = dashboardEl('div');
3983		fill.style.width = share + '%';
3984		bar.appendChild(fill);
3985		row.append(line, bar);
3986		decisions.appendChild(row);
3987	});
3988	overview.appendChild(decisions);
3989
3990	const shareItem = (className, label, value, total, valueClass) => {
3991		const item = dashboardEl('div', className);
3992		item.append(dashboardEl('div', 'fcd-sm fcd-muted', label), dashboardEl('div', valueClass, dashboardNumber(value)),
3993			dashboardEl('div', 'fcd-sm fcd-muted', dashboardPercent(value, total)));
3994		return item;
3995	};
3996	const withConsent = translations['withConsent'] || 'With consent';
3997	const withoutConsent = translations['withoutConsent'] || 'Without consent';
3998	const noPreference = translations['noPreference'] || 'No preference';
3999
4000	const byConsent = dashboardEl('section', 'fcd-card fcd-stack');
4001	byConsent.appendChild(dashboardCardHead(translations['visitsByConsent'] || 'Visits by consent'));
4002	// notRequired counts visitor-days (CmpCreateReports adds one per row), so it is a share of visits, as in the Shopify
4003	// app's Visits by Consent card.
4004	const visitsGrid = dashboardEl('div', 'fcd-grid fcd-grid-4');
4005	visitsGrid.append(
4006		shareItem('fcd-box fcd-stack-8', withConsent, n('visitsWithConsent'), visits, 'fcd-xl'),
4007		shareItem('fcd-box fcd-stack-8', withoutConsent, n('visitsWithoutConsent'), visits, 'fcd-xl'),
4008		shareItem('fcd-box fcd-stack-8', noPreference, n('visitsNoPreference'), visits, 'fcd-xl'),
4009		shareItem('fcd-box fcd-stack-8', translations['notRequired'] || 'Consent not required', n('notRequired'), visits, 'fcd-xl'));
4010	byConsent.appendChild(visitsGrid);
4011
4012	const pageViewsCard = dashboardEl('section', 'fcd-card fcd-stack');
4013	pageViewsCard.appendChild(dashboardCardHead(translations['pageviewsAnalysis'] || 'Pageviews',
4014		dashboardNumber(pageViews) + ' ' + (translations['totalBadge'] || 'total')));
4015	const pageViewsGrid = dashboardEl('div', 'fcd-grid fcd-grid-3');
4016	pageViewsGrid.append(
4017		shareItem('fcd-stack-8', withConsent, n('pageViewsWithConsent'), pageViews, 'fcd-lg'),
4018		shareItem('fcd-stack-8', withoutConsent, n('pageViewsWithoutConsent'), pageViews, 'fcd-lg'),
4019		shareItem('fcd-stack-8', noPreference, n('pageViewsWithNoPreference'), pageViews, 'fcd-lg'));
4020	pageViewsCard.appendChild(pageViewsGrid);
4021
4022	consent.replaceChildren(overview, byConsent, pageViewsCard);
4023}
4024
4025async function loadProjectStatsPrev(dashboardForm, projectId, startDate, endDate) {
4026	const data = { sid: getSid(), projectId: projectId, startDate: startDate, endDate: endDate };
4027	const responseDataStats = await postRequest(endpoint + '/cmp/data-stats', data);
4028	// const responseDataStats = await fetch(endpoint + '/cmp/data-stats?sid=' + getSid());
4029
4030	if (responseDataStats.ok) {
4031		const responseBodyDataStats = await responseDataStats.json();
4032		if (responseBodyDataStats.statusCode === 0) {
4033			const firstKey = Object.keys(responseBodyDataStats.Projects)[0];
4034			const projectStats = responseBodyDataStats.Projects[firstKey];
4035
4036			const stats = {
4037				visitsTotal: {
4038					visitsTotal: projectStats['visitsTotal'].Total
4039				},
4040				visitorVisits: {
4041					visitsNewClient: projectStats['visitsNewClient'].Total,
4042					visitsExistingClient: projectStats['visitsExistingClient'].Total,
4043				},
4044				visitorConsentState: {
4045					visitsWithConsent: projectStats['visitsWithConsent'].Total,
4046					visitsWithoutConsent: projectStats['visitsWithoutConsent'].Total,
4047					visitsNoPreference: projectStats['visitsNoPreference'].Total,
4048				},
4049				pageViews: {
4050					pageViewsWithNoPreference: projectStats['pageViewsWithNoPreference'].Total,
4051					pageViewsWithConsent: projectStats['pageViewsWithConsent'].Total,
4052					pageViewsWithoutConsent: projectStats['pageViewsWithoutConsent'].Total,
4053					pageViewsTotal: projectStats['pageViewsTotal'].Total,
4054				},
4055				visitorPreferences: {
4056					notRequired: projectStats['notRequired'].Total,
4057					requests: projectStats['consentRequests'].Total,
4058					accepts: projectStats['consentAccepts'].Total,
4059					rejects: projectStats['consentRejects'].Total,
4060					partiallyAccepts: projectStats['consentPartiallyAccepts'].Total,
4061					cancels: projectStats['consentCancels'].Total,
4062				},
4063			};
4064
4065			dashboardForm.innerHTML = '';
4066			for (const groupKey in stats) {
4067				const groupValue = stats[groupKey];
4068				const div1 = document.createElement('div');
4069				div1.classList.add('row');
4070				dashboardForm.appendChild(div1);
4071				let count = Object.keys(groupValue).length;
4072				const width = 12 / count;
4073				const borderColor = 'border-color' + count;
4074
4075				for (const key in groupValue) {
4076					let value = groupValue[key];
4077
4078					const div2 = document.createElement('div');
4079					div2.classList.add('col-lg-' + width, 'px-3', 'py-2');
4080					div1.appendChild(div2);
4081
4082					const card = document.createElement('div');
4083					card.classList.add('card', 'card-std', borderColor);
4084					div2.appendChild(card);
4085
4086					const div3 = document.createElement('div');
4087					div3.classList.add('stat-box', 'd-flex', 'flex-column', 'flex-wrap');
4088					card.appendChild(div3);
4089
4090					const spanTitle = document.createElement('span');
4091					spanTitle.classList.add('title');
4092					// spanTitle.innerText = rfCmpData[key];
4093					spanTitle.innerText = keyToTitle(key);
4094					div3.appendChild(spanTitle);
4095
4096					const div4 = document.createElement('div');
4097					div4.classList.add('d-flex', 'justify-content-center');
4098					div3.appendChild(div4);
4099
4100					const [val, ending, color] = numberToText(value);
4101
4102					const spanValue = document.createElement('span');
4103					spanValue.classList.add('value');
4104					spanValue.innerText = val;
4105					div4.appendChild(spanValue);
4106
4107					const spanValue2 = document.createElement('span');
4108					spanValue2.classList.add('value');
4109					if (color.length > 0)
4110						spanValue2.classList.add(color);
4111					spanValue2.innerText = ending;
4112
4113					div4.appendChild(spanValue2);
4114				}
4115			}
4116			// console.log("Page loaded");
4117		} else {
4118			// showAlert("The page could not be loaded!", "error");
4119		}
4120	} else {
4121		showAlert("The page could not be loaded!", "error");
4122	}
4123
4124	// var rfCmpData = {a: "Page Display", b: "Visitor Count", c: "Deneme", d: "Test"};
4125	//
4126	// var stats = {
4127	//     grp1: {
4128	//         a: 12005436750, b: 240506
4129	//     },
4130	//     grp2: {
4131	//         c: 1500400, d: 5600, a: 10460
4132	//     },
4133	//     grp3: {
4134	//         c: 1500400, d: 600, a: 10460, b: 980778032
4135	//     }
4136	// };
4137}
4138
4139function getDateRange() {
4140	const dateRangeDropDown = document.getElementById('select_date_range');
4141	const dateRangeValue = dateRangeDropDown.value;
4142	// let date = new Date();
4143	const now = new Date();
4144	const dayOfWeek = now.getDay();
4145	const dayOfMonth = now.getDate();
4146	// let startDate, endDate;
4147	let startDate = new Date();
4148	let endDate = new Date()
4149	switch (dateRangeValue) {
4150		case 'today':
4151			startDate = now;
4152			endDate = now;
4153			break;
4154
4155		case 'yesterday':
4156			startDate.setDate(now.getDate() - 1);
4157			endDate.setDate(now.getDate() - 1);
4158			break;
4159
4160		case 'thisWeek':
4161			startDate.setDate(dayOfMonth - dayOfWeek);
4162			break;
4163
4164		case 'last7Days':
4165			startDate.setDate(now.getDate() - 6);
4166			break;
4167
4168		case 'thisMonth':
4169			startDate.setDate(1);
4170			break;
4171
4172		case 'last30Days':
4173			startDate.setDate(now.getDate() - 29);
4174			break;
4175
4176		case 'thisYear':
4177			startDate.setDate(1);
4178			startDate.setMonth(0);
4179			break;
4180
4181		case 'last365Days':
4182			startDate.setDate(now.getDate() - 364);
4183			break;
4184
4185		case 'allTime':
4186			startDate = null;
4187			endDate = null;
4188			break;
4189
4190		case 'custom':
4191			startDate = null;
4192			endDate = null;
4193			break;
4194
4195		default:
4196			startDate = null;
4197			endDate = null;
4198			break;
4199	}
4200
4201	console.log('Date Range: ' + dateRangeValue);
4202	console.log('Start: ' + startDate);
4203	console.log('End: ' + endDate);
4204	console.log('');
4205	return [startDate, endDate];
4206}
4207
4208
4209/* USER */
4210
4211async function loadUserSettings(userSettingsForm) {
4212	if (checkIfLoginRequired() )
4213		return;
4214
4215	// document.querySelectorAll('.btn-success').forEach(btn => {
4216	//     // btn.textContent = translations['save'];
4217	//     setTextContentTranslationByElement(btn, 'save');
4218	// });
4219	//
4220	// document.querySelectorAll('.btn-cancel').forEach(btn => {
4221	//     // btn.textContent = translations['cancel'];
4222	//     setTextContentTranslationByElement(btn, 'cancel');
4223	// });
4224
4225
4226	const languagePicker = document.getElementById('languages');
4227	languages.forEach(function (language) {
4228		languagePicker.add(new Option(language.name, language.id));
4229	});
4230
4231	document.querySelectorAll('input[type="password"]').forEach(input => {
4232		const btnEye = input.nextElementSibling;
4233		btnEye.addEventListener('click', (event) => {
4234			event.preventDefault();
4235			const passwordIcon = btnEye.children[0];
4236			if (input.type === "password") {
4237				input.type = "text";
4238				passwordIcon.classList.remove("fa-eye");
4239				passwordIcon.classList.add("fa-eye-slash");
4240			} else {
4241				input.type = "password";
4242				passwordIcon.classList.remove("fa-eye-slash");
4243				passwordIcon.classList.add("fa-eye");
4244			}
4245		});
4246	});
4247
4248	const data = { sid: getSid() };
4249	const response = await postRequest(endpoint + '/user/get', data);
4250	if (response.ok) {
4251		const responseBody = await response.json();
4252		if (responseBody.statusCode === 0) {
4253			document.getElementById('full_name').value = responseBody.fullName;
4254			document.getElementById('email').value = responseBody.email;
4255			document.getElementById('company').value = responseBody.companyName;
4256			// language
4257
4258			const invoiceInfo = responseBody.invoiceInfo;
4259			document.getElementById('nameOnInvoice').value = invoiceInfo.nameOnInvoice;
4260			document.getElementById('accountType').value = invoiceInfo.business ? 'business' : 'personal';
4261			document.getElementById('accountType')[0].style.display = 'none';
4262			document.getElementById('address1').value = invoiceInfo.addressLine1;
4263			document.getElementById('address2').value = invoiceInfo.addressLine2;
4264			document.getElementById('city').value = invoiceInfo.city;
4265			document.getElementById('postCode').value = invoiceInfo.postCode;
4266
4267			const countryPicker = document.getElementById('countries');
4268			fetch(endpoint + '/countries')
4269				.then((response) => response.json())
4270				.then((responseBody) => {
4271					if (responseBody.statusCode === 0) {
4272						responseBody.countries.forEach(async function(country) {
4273							countryPicker.add(new Option(country.name, country.id));
4274						});
4275						countryPicker.value = invoiceInfo.countryId;
4276						countryPicker[0].style.display = 'none';
4277					} else
4278						console.log("Countries Error: " + responseBody.statusCode);
4279				});
4280			document.getElementById('vatNumber').value = invoiceInfo.vatNumber;
4281
4282
4283			////			const languagesResponse = await fetch(endpoint + '/languages');
4284			////			if(languagesResponse.ok){ }
4285			// const languageId = responseBody.languageId;
4286			// const languagePicker = document.getElementById('languages');
4287			// fetch(endpoint + '/languages')
4288			// 	.then((response) => response.json())
4289			// 	.then((responseBody) => {
4290			// 		if (responseBody.statusCode === 0) {
4291			// 			languages = responseBody.languages;
4292			// 			responseBody.languages.forEach(async function(language) {
4293			// 				languagePicker.add(new Option(language.name, language.id));
4294			// 			});
4295			// 			languagePicker.value = languageId;
4296			// 			// languagePicker[0].style.display = 'none';
4297			// 		} else
4298			// 			console.log("Languages Error: " + responseBody.statusCode);
4299			// 	});
4300
4301			languagePicker.value = responseBody.languageId;
4302
4303			userSettingsForm.addEventListener('submit', async (event) => {
4304				event.preventDefault();
4305				saveUserSettings(responseBody.email);
4306			});
4307		} else {
4308			console.log('User Get Error Code: ' + responseBody.errorCode + '\nMessage: ' + responseBody.errorMessage);
4309		}
4310	} else {
4311		console.error('User Get failed: ' + JSON.stringify(response));
4312	}
4313}
4314
4315function saveUserSettings(currentEmail) {
4316	const name = document.getElementById('full_name').value.trim();
4317	// const email = document.getElementById('email').value.trim();
4318	const company = document.getElementById('company').value.trim();
4319	const languageId = parseInt(document.getElementById('languages').value);
4320	const nameOnInvoice = document.getElementById('nameOnInvoice').value.trim();
4321	const accountType = document.getElementById('accountType').value.trim();
4322	const address1 = document.getElementById('address1').value.trim();
4323	const address2 = document.getElementById('address2').value.trim();
4324	const city = document.getElementById('city').value.trim();
4325	const postCode = document.getElementById('postCode').value.trim();
4326	const countryId = parseInt(document.getElementById('countries').value);
4327	const vatNumber = document.getElementById('vatNumber').value.trim();
4328
4329	const data = {
4330		sid: getSid(),
4331		fullName: name,
4332		companyName: company,
4333		languageId: languageId,
4334		invoiceInfo: {
4335			nameOnInvoice: nameOnInvoice,
4336			business: accountType === 'business',
4337			addressLine1: address1,
4338			addressLine2: address2,
4339			city: city,
4340			postCode: postCode,
4341			countryId: countryId,
4342			vatNumber: vatNumber
4343		}
4344	};
4345
4346	// if (email !== currentEmail)
4347	// 	data.updatedEmail = email;
4348
4349	postRequest(endpoint + '/user/update', data)
4350		.then((response) => {
4351			if (response.ok) {
4352				response.json().then((responseBody) => {
4353					if (responseBody.statusCode === 0) {
4354						createCookie("name", removeAfterLast(name, ' '));
4355						const bundle = languages.find(l => l.id === languageId).bundle;
4356						createCookie("lang", bundle);
4357						let url = new URL(window.location.href);
4358						url.searchParams.delete('lang');
4359						window.location.href = url.toString();
4360
4361						// window.location.reload();
4362					} else {
4363						showAlert("Error: " + responseBody.errorMessage, "error");
4364					}
4365				});
4366			} else {
4367				console.error('User Save failed: ' + JSON.stringify(response));
4368			}
4369		});
4370}
4371
4372async function updatePassword() {
4373	event.preventDefault();
4374
4375	const currentPwd = document.getElementById('currentPassword').value.trim();
4376	const newPwd = document.getElementById('newPassword').value.trim();
4377	const confirmPwd = document.getElementById('confirmPassword').value.trim();
4378	if (!currentPwd || !newPwd || !confirmPwd) {
4379		showAlert("Password cannot be empty!", "warning");
4380	} else if (newPwd === currentPwd) {
4381		showAlert("Your new password cannot be the same as your current one!", "warning");
4382	} else if (newPwd === confirmPwd) {
4383		//		const data = { sid: getSid(), oldPassword: currentPwd, newPassword: newPwd };
4384		//		const response = await postRequest(endpoint + '/updatepassword', data);
4385		const response = await fetch(endpoint + '/updatepassword/' + getSid() + '/' + currentPwd + '/' + newPwd);
4386		if (response.ok) {
4387			const responseBody = await response.json();
4388			if (responseBody.errorCode === 0) {
4389				logout();
4390			} else if (responseBody.errorCode === 4) {
4391				showAlert("Current password is wrong!", "error");
4392			} else if (responseBody.errorCode === 9) {
4393				showAlert("Your password must be at least 6 characters!", "warning");
4394			} else if (responseBody.errorCode === 10) {
4395				//			document.getElementById('password_req').style.border = "1px solid red";
4396				showAlert("The new password does not comply with password requirements!", "warning");
4397			} else {
4398				console.log('Uodate Password Error Code: ' + responseBody.errorCode);
4399			}
4400		} else {
4401			console.error('Uodate Password failed: ' + JSON.stringify(response));
4402		}
4403	} else if (newPwd !== confirmPwd) {
4404		showAlert("Password confirmation doesn't match the new password!", "warning");
4405	}
4406}
4407
4408
4409/* SUBSCRIPTIONS */
4410
4411async function loadSubscriptions() {
4412	if (checkIfLoginRequired())
4413		return;
4414
4415	const subscriptionsElement = document.getElementById('subscriptions');
4416
4417	const modal = document.getElementById('modal_cancel_subscription');
4418	const modalTitle = modal.getElementsByClassName('modal-title')[0];
4419	setTextContentTranslationByElement(modalTitle, 'cancelSubscription');
4420	// modal.addEventListener('show.bs.modal', function (event) { });
4421
4422	setTextContentTranslationById('btn_cancel_subscription', 'cancelSubscription');
4423	const btnClose = modal.getElementsByClassName('btn-secondary')[0];
4424	setTextContentTranslationByElement(btnClose, 'close');
4425
4426	const data = { sid: getSid(), projectTypeId: 2 };
4427	const response = await postRequest(endpoint + '/package/list', data);
4428	if (response.ok) {
4429		const responseBody = await response.json();
4430		if (responseBody.statusCode === 0) {
4431			/*if (isReseller) {
4432				window.location.href = "payments.jsp";
4433				return;
4434			}*/
4435
4436			const subscriptionsContainer = subscriptionsElement.parentElement;
4437			subscriptionsContainer.classList.remove('d-none');
4438
4439			const packages = responseBody['packages'];
4440			const domains = responseBody['domains'];
4441
4442			// let freeSubscriptionExists = false, paidSubscriptionExists = false;
4443			const subscriptions = responseBody['subscriptions'];
4444			const trialSubscription = subscriptions.find(s => s.type === "Trial" && new Date(s.end) >= new Date());
4445			if (trialSubscription) {
4446				subscriptions.splice(subscriptions.indexOf(trialSubscription), 1);
4447				subscriptions.unshift(trialSubscription);
4448			}
4449
4450			const freeSubscription = subscriptions.find(s => s.type === "Free");
4451			if (freeSubscription) {
4452				subscriptions.splice(subscriptions.indexOf(freeSubscription), 1);
4453				subscriptions.unshift(freeSubscription);
4454			}
4455			subscriptions?.forEach(subscription => {
4456				if (subscription.state === "0")
4457					return;
4458
4459				const plan = packages.find(p => p.id === subscription.typeId);
4460				if (plan.price > 0 && new Date(subscription.end) < new Date())
4461					return;
4462				if (subscription.type === "Trial" && new Date(subscription.end) < new Date())
4463					return;
4464
4465				if (subscriptionsElement.children.length > 0) {
4466					const lineDivider = document.createElement('hr');
4467					lineDivider.classList.add('my-3');
4468					subscriptionsElement.appendChild(lineDivider);
4469				}
4470
4471				const tempDomains = domains.slice();
4472				if (trialSubscription && subscription.type !== "Trial") {
4473					const domainTrial = domains.find(d => d.subscriptionId === trialSubscription.id);
4474					if (domainTrial) {
4475						const index = tempDomains.indexOf(domainTrial);
4476						tempDomains.splice(index, 1);
4477					}
4478				}
4479				addSubscriptionElement(subscriptionsElement, subscription, packages, subscriptions.length, tempDomains);
4480			});
4481
4482			const btnSave = document.getElementById('btn_save');
4483			btnSave.addEventListener('click', async (event) => {
4484				event.preventDefault();
4485				const selectedWebsites = {};
4486				domains.forEach(domain => {
4487					selectedWebsites[domain.id] = 0;
4488				});
4489				const websiteSelectors = subscriptionsElement.querySelectorAll('select[id^="websites_"]');
4490				const websiteSelectorsKeys = Object.keys(websiteSelectors);
4491				for (let i = 0; i < websiteSelectorsKeys.length; i++) {
4492					const websiteSelector = websiteSelectors[websiteSelectorsKeys[i]];
4493					const subscriptionId = parseInt(websiteSelector.id.split('_')[1]);
4494					// const subscriptionId = websiteSelector.subscriptionId;
4495					const selectedWebsiteId = parseInt(websiteSelector.value);
4496					if (selectedWebsiteId > 0) {
4497						if (selectedWebsites[selectedWebsiteId] > 0) {
4498							showAlert(translations['websiteAlreadySelected'] || 'You can only assign one website to one subscription!', "warning");
4499							return;
4500						} else
4501							// selectedWebsites[selectedWebsiteId] = typeId;
4502							selectedWebsites[selectedWebsiteId] = subscriptionId;
4503					}
4504				}
4505
4506				// const selectedWebsiteIds = Object.values(selectedWebsites);
4507				// const uniqueWebsiteIds = new Set(selectedWebsiteIds);
4508				// if (selectedWebsiteIds.length !== uniqueWebsiteIds.size) {
4509				// 	alert(translations['websiteAlreadySelected'] || 'Website already selected');
4510				// 	return;
4511				// }
4512
4513				const data = { sid: getSid(), domains: selectedWebsites, projectTypeId: 2 };
4514				const response = await postRequest(endpoint + '/package/update', data);
4515				if (response.ok) {
4516					const responseBody = await response.json();
4517					if (responseBody.statusCode === 0) {
4518						window.location.reload();
4519					} else if (responseBody.statusCode === 27) { //ER_DOMAIN_TRIAL_EXPIRED
4520						showAlert(translations['domainTrialExpiredMsg'] || 'The trial period for this website has already expired. Please select another website.', "warning");
4521					} else {
4522						showAlert("Error: " + responseBody.statusMessage, "error");
4523					}
4524				}
4525			});
4526			btnSave.disabled = false;
4527
4528			const packagesContainer = document.getElementById('plans_container');
4529			const subscription = { id: 0, typeId: -1 };
4530			showBuyPackages(subscription, packages, packagesContainer);
4531
4532			const buyNewPlanBtn = document.getElementById('btn_buy_new_plan');
4533			buyNewPlanBtn.addEventListener('click', (event) => {
4534				event.preventDefault();
4535
4536				if (packagesContainer.classList.contains('expanded')) {
4537					packagesContainer.classList.remove('expanded');
4538					setTimeout(() => {
4539						setTextContentTranslationByElement(buyNewPlanBtn.firstElementChild, 'buyPlan');
4540						buyNewPlanBtn.lastElementChild.innerHTML = '<i class="fa-solid fa-chevron-right"></i>';
4541						buyNewPlanBtn.classList.replace('btn-outline-secondary', 'btn-success');
4542					}, 500);
4543				} else {
4544					packagesContainer.classList.add('expanded');
4545					setTextContentTranslationByElement(buyNewPlanBtn.firstElementChild, 'hidePlans');
4546					buyNewPlanBtn.lastElementChild.innerHTML = '<i class="fa-solid fa-chevron-down"></i>';
4547					buyNewPlanBtn.classList.replace('btn-success', 'btn-outline-secondary');
4548
4549					setTimeout(() => {
4550						const pageBody = document.getElementsByTagName('body')[0];
4551						const scrollOptions = {
4552							top: pageBody.scrollHeight,
4553							behavior: 'smooth'
4554						};
4555						pageBody.scroll(scrollOptions);
4556					}, 500);
4557				}
4558			});
4559		}
4560	}
4561}
4562
4563function addSubscriptionElement(subscriptionsElement, subscription, packages, numSubscriptions, domains) {
4564	const subscriptionElement = document.createElement('div');
4565	subscriptionElement.setAttribute('id', 'subscription_' + subscription.id);
4566	subscriptionElement.classList.add('p-2');
4567	subscriptionsElement.appendChild(subscriptionElement);
4568
4569	const plan = packages.find(p => p.id === subscription.typeId);
4570	const duration = "(" + (plan.durationMonth === 1 ? translations['monthly'] : translations['annual']) + ")";
4571
4572	const titleRow = document.createElement('div');
4573	titleRow.classList.add('d-flex', 'justify-content-between', 'align-items-center');
4574	subscriptionElement.appendChild(titleRow);
4575
4576	const title = document.createElement('h6');
4577	title.classList.add('fw-bold');
4578	title.textContent = subscription.type + " " + duration;
4579	titleRow.appendChild(title);
4580
4581	const websiteContainer = document.createElement('div');
4582	websiteContainer.classList.add('d-flex', 'align-items-center', 'mb-2');
4583	subscriptionElement.appendChild(websiteContainer);
4584
4585	const websiteIcon = document.createElement('i');
4586	websiteIcon.classList.add('fa-solid', 'fa-globe', 'me-2');
4587	websiteContainer.appendChild(websiteIcon);
4588
4589	const websiteLabel = document.createElement('span');
4590	websiteLabel.classList.add('card-text');
4591	websiteLabel.textContent = translations['website'] || 'Website';
4592	websiteContainer.appendChild(websiteLabel);
4593
4594	const websiteSelector = document.createElement('select');
4595	websiteSelector.classList.add('form-select', 'form-select-sm', 'w-auto', 'ms-3');
4596	websiteSelector.style.minWidth = '200px';
4597	websiteSelector.setAttribute('aria-label', translations['websites'] || 'Websites');
4598	// websiteSelector.setAttribute('id', 'websites_' + subscription.typeId);
4599	websiteSelector.setAttribute('id', 'websites_' + subscription.id);
4600	// websiteSelector.subscriptionId = subscription.id;
4601	websiteContainer.appendChild(websiteSelector);
4602
4603	const optionSelect = document.createElement('option');
4604	optionSelect.value = "0";
4605	optionSelect.textContent = translations['select'] || 'Select';
4606	// websiteSelector.insertBefore(option, websiteSelector.firstChild);
4607	websiteSelector.appendChild(optionSelect);
4608
4609	const domain = domains.find(d => d.subscriptionId === subscription.id);
4610	if (domain) {
4611		for (let i = 0; i < domains.length; i++) {
4612			const option = document.createElement('option');
4613			option.value = domains[i].id;
4614			option.textContent = domains[i].domainName;
4615			websiteSelector.appendChild(option);
4616		}
4617		websiteSelector.value = domain.id;
4618
4619		if (subscription.type === 'Trial')
4620			optionSelect.remove();
4621	} else {
4622		let isSelected = false;
4623		for (let i = 0; i < domains.length; i++) {
4624			const option = document.createElement('option');
4625			option.value = domains[i].id;
4626			option.textContent = domains[i].domainName;
4627
4628			if (!isSelected && !domains[i].isSelected && domains[i].st === subscription.typeId) {
4629				option.selected = true;
4630				isSelected = true;
4631				domains[i].isSelected = true;
4632			}
4633			websiteSelector.appendChild(option);
4634		}
4635
4636		if (isSelected && subscription.type === 'Trial')
4637			optionSelect.remove();
4638	}
4639
4640	if (subscriptionsPageViewExceededList.includes(subscription.id)) {
4641		// if (translations['pageviewLimitMsgForDomain']) {
4642			let msg = translations['pageviewLimitMsgForDomain'] || "Your website [domain] has reached the monthly page view limit on the [planName] plan. To ensure uninterrupted service and accurate consent tracking, we recommend upgrading your plan.";
4643			const domainLink = document.createElement('a');
4644			// domainLink.href = "#" + websiteSelector.getAttribute('id');
4645			domainLink.href = "#" + subscriptionElement.getAttribute('id');
4646			domainLink.textContent = domain.domainName;
4647			domainLink.classList.add('alert-link');
4648			// domainLink.onclick = function() {
4649			// 	const websiteSelector = document.getElementById('websites_' + subscription.id);
4650			// 	websiteSelector.scrollIntoView({ behavior: 'smooth', block: 'center' });
4651			// 	websiteSelector.focus();
4652			// }
4653
4654			const planLink = document.createElement('a');
4655			// planLink.href = "#" + websiteSelector.getAttribute('id');
4656			planLink.href = "#" + subscriptionElement.getAttribute('id');
4657			// planLink.textContent = plan.name;
4658			planLink.textContent = subscription.type;
4659			planLink.classList.add('alert-link');
4660			// planLink.onclick = function() {
4661			// 	subscriptionElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
4662			// 	subscriptionElement.focus();
4663			// }
4664
4665			msg = msg.replace('[domain]', domainLink.outerHTML).replace('[planName]', planLink.outerHTML);
4666			displayAlert(msg);
4667		// } else {
4668		// 	displayAlert(translations['pageviewLimitMsg'] + "<br>Plan: " + subscription.type + "<br>Website: " + domain.domainName);
4669		// }
4670	}
4671
4672	const periodContainer = document.createElement('div');
4673	periodContainer.classList.add('d-flex', 'align-items-center', 'mb-2');
4674	subscriptionElement.appendChild(periodContainer);
4675
4676	const periodIcon = document.createElement('i');
4677	periodIcon.classList.add('fa-regular', 'fa-calendar', 'me-2');
4678	periodContainer.appendChild(periodIcon);
4679
4680	const periodLabel = document.createElement('span');
4681	periodLabel.classList.add('card-text');
4682	periodLabel.textContent = (translations['subscriptionPeriod'] || 'Subscription period') + ":";
4683	periodContainer.appendChild(periodLabel);
4684
4685	const periodText = document.createElement('span');
4686	periodText.classList.add('card-text', 'ms-2');
4687	periodText.textContent = subscription.start + " - " + subscription.end;
4688	periodContainer.appendChild(periodText);
4689
4690
4691	const remainingContainer = document.createElement('div');
4692	remainingContainer.classList.add('d-flex', 'align-items-center', 'mb-2');
4693	subscriptionElement.appendChild(remainingContainer);
4694
4695	const remainingIcon = document.createElement('i');
4696	remainingIcon.classList.add('fa-regular', 'fa-clock', 'me-2');
4697	remainingContainer.appendChild(remainingIcon);
4698
4699	let remainTime = new Date(subscription.end) - new Date(), remainStr;
4700	if (remainTime < 0)
4701		remainTime = 0;
4702	const remainDays = Math.floor(remainTime / (1000 * 60 * 60 * 24));
4703	if (remainDays > 30) {
4704		const remainMonths = Math.floor(remainDays / 30);
4705		remainStr = translations[remainMonths > 1 ? 'remainMonthsCount' : 'remainMonthCount'];
4706		remainStr = remainStr.replace('[monthCount]', remainMonths.toString());
4707	} else {
4708		remainStr = translations[remainDays > 1 ? 'remainDaysCount' : 'remainDayCount'];
4709		remainStr = remainStr.replace('[dayCount]', remainDays.toString());
4710	}
4711
4712	const text2 = document.createElement('span');
4713	text2.classList.add('card-text');
4714	if (subscription.nextTypeId === 0) {
4715		setTextContentTranslationByElement(text2, 'subscriptionCancelMessage');
4716		text2.classList.add('text-danger');
4717	} else if (subscription.nextTypeId === subscription.typeId) {
4718		setTextContentTranslationByElement(text2, 'subscriptionRenewMessage');
4719		text2.classList.add('text-muted');
4720	}
4721	text2.textContent += " " + remainStr;
4722	remainingContainer.appendChild(text2);
4723
4724
4725	const descriptionContainer = document.createElement('div');
4726	subscriptionElement.appendChild(descriptionContainer);
4727
4728	const descriptionTitle = document.createElement('div');
4729	descriptionTitle.textContent = translations['planFeatures'] || 'Plan Features';
4730	descriptionTitle.role = 'button';
4731	descriptionTitle.classList.add('mt-2');
4732	descriptionContainer.appendChild(descriptionTitle);
4733
4734	const chevronParent = document.createElement('span');
4735	chevronParent.innerHTML = '<i class="fa-solid fa-chevron-right"></i>';
4736	chevronParent.classList.add('ms-1');
4737	descriptionTitle.appendChild(chevronParent);
4738
4739	const descriptionTextContainer = document.createElement('div');
4740	descriptionTextContainer.classList.add('expandable');
4741	descriptionContainer.appendChild(descriptionTextContainer);
4742
4743	const descriptionText = document.createElement('p');
4744	descriptionText.classList.add('card-text', 'bg-light', 'p-2', 'rounded-3', 'text-muted');
4745	// const planDescription = plan.description.replace(/<br\s*\/?>/gi, '<br>');
4746	// const planDescription = translations[plan.name + 'PlanDescription'] || plan.description;
4747	let planName = (plan.name.split(' ')[0]).toLowerCase();
4748	planName = planName.replace('trial', 'plus');
4749	descriptionText.innerHTML = translations[planName + 'PlanDescription'] || plan.description;
4750	descriptionTextContainer.appendChild(descriptionText);
4751
4752	descriptionTitle.onclick = function() {
4753		descriptionTextContainer.classList.toggle('expanded');
4754
4755		if (descriptionTextContainer.classList.contains('expanded'))
4756			chevronParent.innerHTML = '<i class="fa-solid fa-chevron-down"></i>';
4757		else
4758			chevronParent.innerHTML = '<i class="fa-solid fa-chevron-right"></i>';
4759	}
4760
4761	const isFreePlan = Math.floor(plan.price) === 0;
4762	if (isFreePlan)
4763		return;
4764
4765	if (subscription.id > 0 && subscription.nextTypeId > 0) {
4766		// const btnCancelContainer = document.createElement('div');
4767		// btnCancelContainer.classList.add('d-flex', 'justify-content-start', 'mt-1');
4768		// subscriptionElement.appendChild(btnCancelContainer);
4769
4770		const btnCancel = document.createElement('a');
4771		btnCancel.classList.add('danger-cancel-link');
4772		// btnCancel.classList.add('link-secondary', 'link-offset-3-hover', 'link-underline', 'link-underline-opacity-0', 'link-underline-opacity-75-hover');
4772 //link-offset-3-hover link-underline link-underline-opacity-0 link-underline-opacity-75-hover
4773		setTextContentTranslationByElement(btnCancel, 'cancelSubscription');
4774		btnCancel.setAttribute('data-bs-toggle', 'modal');
4775		btnCancel.setAttribute('data-bs-target', '#modal_cancel_subscription');
4776		// btnCancelContainer.appendChild(btnCancel);
4777		titleRow.appendChild(btnCancel);
4778		btnCancel.addEventListener('click', event => {
4779			event.preventDefault();
4780
4781			const subscriptionInfo = document.getElementById('modal_subscription_info');
4782			subscriptionInfo.textContent = translations['subscription'] + ": " + subscription.type + ' (' + subscription.start + ' - ' + subscription.end + ')';
4783
4784			const modalBodyText1 = subscriptionInfo.nextElementSibling;
4785			setTextContentTranslationByElement(modalBodyText1, 'cancelSubscriptionText1');
4786
4787			const modalBodyText2 = modalBodyText1.nextElementSibling;
4788			setTextContentTranslationByElement(modalBodyText2, 'cancelSubscriptionText2');
4789
4790			const btnConfirm = document.getElementById('btn_cancel_subscription');
4791			btnConfirm.textContent = "Cancel Subscription";
4792			setTextContentTranslationByElement(btnConfirm, 'cancelSubscription');
4793			btnConfirm.onclick = async function () {
4794				event.preventDefault();
4795
4796				btnConfirm.textContent = 'Cancelling...';
4797				setTextContentTranslationByElement(btnConfirm, 'cancelling');
4798				const spinner = generateSpinner();
4799				btnConfirm.appendChild(spinner);
4800
4801				const data = {sid: getSid(), subscriptionId: subscription.id};
4802				const response = await postRequest(endpoint + '/payment/unsubscribe', data);
4803				if (response.ok) {
4804					const responseBody = await response.json();
4805					if (responseBody.statusCode === 0) {
4806						// window.location.reload();
4807						// if (!packagescontainer.classList.contains('d-none')) {
4808						// 	btnChangePlan.click();
4809						// }
4810
4811						// const packageCards = packagescontainer.querySelectorAll('.card-body');
4812						// packageCards.forEach(card => {
4813						// 	card.classList.remove('d-none');
4814						// });
4815
4816						setTextContentTranslationByElement(text2, 'subscriptionCancelMessage');
4817						// subscriptionCard.classList.add('border-danger');
4818						text2.classList.remove('text-muted');
4819						text2.classList.add('text-danger');
4820
4821						const modal = document.getElementById('modal_cancel_subscription');
4822						const modal2 = bootstrap.Modal.getOrCreateInstance(modal);
4823						modal2.hide();
4824					} else {
4825						showAlert("Error: " + responseBody.statusMessage, "error");
4826					}
4827				} else {
4828					showAlert("Error: " + (response.statusMessage || "Response failed"), "error");
4829				}
4830
4831				removeSpinner(spinner);
4832
4833				// packagescontainer.classList.add('d-none');
4834				// setTextContentTranslationByElement(btnChangePlan, 'changePlan');
4835				// btnChangePlan.classList.add('d-none');
4836			};
4837
4838
4839		});
4840	}
4841}
4842
4843function showBuyPackages(subscription, packages, packagesContainer) {
4844	const tabHeader = document.createElement('ul');
4845	tabHeader.classList.add('nav', 'nav-tabs', 'nav-fill', 'px-3');
4846	packagesContainer.appendChild(tabHeader);
4847
4848	const annualTabHeader = document.createElement('li');
4849	annualTabHeader.classList.add('nav-item');
4850	tabHeader.appendChild(annualTabHeader);
4851
4852	const annualTabLink = document.createElement('a');
4853	annualTabLink.classList.add('nav-link', 'text-black', 'active', 'pe-auto');
4854	setTextContentTranslationByElement(annualTabLink, 'annual');
4855
4856	annualTabHeader.appendChild(annualTabLink);
4857
4858
4859	const monthlyTabHeader = document.createElement('li');
4860	monthlyTabHeader.classList.add('nav-item');
4861	tabHeader.appendChild(monthlyTabHeader);
4862
4863	const monthlyTabLink = document.createElement('a');
4864	monthlyTabLink.classList.add('nav-link', 'text-black');
4865	setTextContentTranslationByElement(monthlyTabLink, 'monthly');
4866
4867	monthlyTabHeader.appendChild(monthlyTabLink);
4868
4869
4870	const tabContentContainer = document.createElement('div');
4871	tabContentContainer.classList.add('tab-content');
4872	packagesContainer.appendChild(tabContentContainer);
4873
4874	const annualTabContent = document.createElement('div');
4875	annualTabContent.classList.add('tab-pane', 'fade', 'show', 'active');
4876	annualTabContent.id = 'annual';
4877	tabContentContainer.appendChild(annualTabContent);
4878
4879	const subAnnualTabContent = document.createElement('div');
4880	subAnnualTabContent.classList.add('d-flex', 'row', 'justify-content-center', 'py-3');
4881	annualTabContent.appendChild(subAnnualTabContent);
4882
4883	const monthlyTabContent = document.createElement('div');
4884	monthlyTabContent.classList.add('tab-pane', 'fade');
4885	monthlyTabContent.id = 'monthly';
4886	tabContentContainer.appendChild(monthlyTabContent);
4887
4888	const subMonthlyTabContent = document.createElement('div');
4889	subMonthlyTabContent.classList.add('d-flex', 'row', 'justify-content-center', 'py-3');
4890	monthlyTabContent.appendChild(subMonthlyTabContent);
4891
4892	annualTabLink.addEventListener('click', event => {
4893		event.preventDefault();
4894		annualTabLink.classList.add('active');
4895		monthlyTabLink.classList.remove('active');
4896		annualTabContent.classList.add('show', 'active');
4897		monthlyTabContent.classList.remove('show', 'active');
4898	});
4899
4900	monthlyTabLink.addEventListener('click', event => {
4901		event.preventDefault();
4902		monthlyTabLink.classList.add('active');
4903		annualTabLink.classList.remove('active');
4904		monthlyTabContent.classList.add('show', 'active');
4905		annualTabContent.classList.remove('show', 'active');
4906	});
4907
4908	const groups = {};
4909	// // packages = packages.sort((a, b) => a.duration - b.duration);
4910	packages.forEach(plan => {
4911		if (!plan.enabled)
4912			return;
4913
4914		if (parseFloat(plan.price) === 0)
4915			return;
4916
4917		plan.name = plan.name.split(' (')[0];
4918		plan.externalInfoLink = plan.externalInfoLink.trim();
4919
4920		const tabPanelContent = plan.duration === 12 ? subAnnualTabContent : subMonthlyTabContent;
4921
4922		const cardCol = document.createElement('div');
4923		cardCol.classList.add('col', 'card-col');
4924		tabPanelContent.appendChild(cardCol);
4925
4926		const planCard = document.createElement('div');
4927		planCard.classList.add('plan-card');
4928		cardCol.appendChild(planCard);
4929
4930		const planCardBody = document.createElement('div');
4931		planCardBody.classList.add('plan-card-body');
4932		planCard.appendChild(planCardBody);
4933
4934		const headerText = document.createElement('span');
4935		headerText.classList.add('header-text');
4936		headerText.textContent = plan.name;
4937		planCardBody.appendChild(headerText);
4938
4939		const priceContainer = document.createElement('div');
4940		priceContainer.classList.add('price-container');
4941
4942		const priceElement = document.createElement('div');
4943		priceElement.classList.add('paid');
4944
4945		let oldPriceContainer = null;
4946		if (plan.price > 0) {
4947			let price = parseFloat(plan.price);
4948			let oldPrice = 0;
4949			let discountPercentage = 0;
4950
4951			if (plan.discountRate) {
4952				price = Math.round(plan.price * (1 - plan.discountRate) * 100) / 100;
4953				oldPrice = parseFloat(plan.price);
4954				discountPercentage = Math.floor(plan.discountRate * 100);
4955			}
4956
4957			if (plan.durationMonth === 12 && plan.otherGroupMemberId) {
4958				const monthlyPlan = packages.find(p => p.id === plan.otherGroupMemberId);
4959				if (monthlyPlan) {
4960					if (oldPrice > 0) {
4961						const twelveMonthsPrice = Math.round(monthlyPlan.price * 12 * 100) / 100;
4962						oldPrice = Math.max(oldPrice, twelveMonthsPrice);
4963					} else {
4964						oldPrice = Math.round(monthlyPlan.price * 12 * 100) / 100;
4965					}
4966					discountPercentage = Math.floor((1 - price / oldPrice) * 100);
4967				}
4968			}
4969			plan.price = price;
4970
4971			const priceParts = price.toFixed(2).split('.');
4972			const currency = currencyMap[plan.currency] || plan.currency;
4973			priceElement.innerHTML = `<span class="currency">${currency}</span>${priceParts[0]}<span class="cent">.${priceParts[1]}</span>`;
4974
4975			if (oldPrice > 0) {
4976				const oldPriceElement = document.createElement('span');
4977				oldPriceElement.classList.add('old-price');
4978				oldPriceElement.innerHTML = `${currency}${oldPrice.toFixed(2)}`;
4979
4980				const discountSvg = `<svg width="20" height="35" viewBox="0 0 21 39" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill="#FF8300" clip-rule="evenodd" fill-rule="evenodd" d="M18.0808 0C16.5161 0 15.0277 0.67567 13.9977 1.8535L1.34101 16.3268C-0.480869 18.4102 -0.442043 21.5311 1.4311 23.5685L14.0068 37.247C15.034 38.3642 16.4822 39 17.9998 39H20.2725V0L18.0808 0ZM11.361 23.4843C13.5614 23.2888 15.1868 21.3465 14.9912 19.146C14.7957 16.9455 12.8534 15.3202 10.6529 15.5157C8.45245 15.7113 6.82711 17.6536 7.02263 19.8541C7.21815 22.0545 9.16049 23.6799 11.361 23.4843Z"></path></svg>`;
4981				const discountElement = document.createElement('div');
4982				let discountText = translations['discountText'] || "[Number]% off";
4983				discountText = discountText.replace('[Number]', discountPercentage.toString());
4984				discountElement.innerHTML =
4985					`<div class="discount-container">
4986						<span class="d-flex">${discountSvg}</span>
4987						<span class="discount-text t-body-2">${discountText}</span>
4988					</div>`;
4989
4990				oldPriceContainer = document.createElement('div');
4991				oldPriceContainer.classList.add('d-flex');
4992				oldPriceContainer.appendChild(oldPriceElement);
4993				oldPriceContainer.appendChild(discountElement);
4994				priceContainer.appendChild(oldPriceContainer);
4995			}
4996
4997		} else if (plan.externalInfoLink) {
4998			priceElement.innerHTML = `<span class="custom-price">Custom Price</span>`;
4999		} else {
5000			priceElement.innerHTML = `<span class="free">Free</span>`;
5001			plan.price = 0;
5002		}
5003
5004		priceContainer.appendChild(priceElement);
5005		planCardBody.appendChild(priceContainer);
5006
5007		const cardText = document.createElement('div');
5008		cardText.classList.add('card-text', 'mb-5', 'text-center', 'align-middle');
5009		let planName = (plan.name.split(' ')[0]).toLowerCase();
5010		plan.description = translations[planName + 'PlanDescription'] || plan.description;
5011		cardText.innerHTML = plan.description;
5012		planCardBody.appendChild(cardText);
5013
5014		const button = document.createElement('button');
5015		button.id = `btn_${plan.id}`;
5016		button.classList.add('btn', 'btn-subscribe');
5017		if (plan.id !== subscription.nextTypeId) {
5018			button.classList.add('btn-success');
5019			setTextContentTranslationByElement(button, plan.externalInfoLink ? 'contactUs' : 'buy');
5020		} else {
5021			button.classList.add('btn-secondary');
5022			button.innerText = translations['subscribed'];
5023			button.disabled = true;
5024		}
5025		planCardBody.appendChild(button);
5026		button.addEventListener('click', event => {
5027			event.preventDefault();
5028			localStorage.setItem('buying_package', JSON.stringify(plan));
5029			localStorage.setItem('subscription', JSON.stringify(subscription));
5030			window.location.href = 'payment.jsp?subject=subscription&sid=' + subscription.id + '&pid=' + plan.id;
5031		});
5032	});
5033
5034	if (subAnnualTabContent.children.length === 0) {
5035		monthlyTabLink.click();
5036		annualTabHeader.style.display = 'none';
5037		monthlyTabHeader.style.display = 'none';
5038	} else if (subMonthlyTabContent.children.length === 0) {
5039		annualTabLink.click();
5040		monthlyTabHeader.style.display = 'none';
5041		annualTabHeader.style.display = 'none';
5042	}
5043}
5044
5045
5046/* PAYMENT */
5047
5048function loadPayment() {
5049	if (checkIfLoginRequired())
5050		return;
5051	const params = new URLSearchParams(window.location.search);
5052	if (params.has('subject')) {
5053		const btnCreditCard = document.getElementById('credit_card');
5054		setTextContentTranslationByElement(btnCreditCard.childNodes[btnCreditCard.childNodes.length - 1], 'debitOrCredit
5054Card');
5055		setTextContentTranslationById('btn_free', 'cancelSubscription');
5056		setTextContentTranslationById('go_back', 'goBackSubscriptions');
5057
5058		const subject = params.get('subject');
5059		if (subject === 'subscription') {
5060			loadSubscriptionPayment();
5061		} else if (subject === 'checkout') {
5062			loadCheckoutPayment();
5063		}
5064	} else {
5065		alog("Jump to subscriptions 18");
5066		window.location.href = 'subscriptions.jsp';
5067	}
5068}
5069
5070	function loadSubscriptionPayment() {
5071		const packageJson = localStorage.getItem('buying_package');
5072		if (packageJson === undefined || packageJson === '') {
5073			alog("Jump to subscriptions 19");
5074			window.location.href = 'subscriptions.jsp';
5075			return;
5076		}
5077
5078		const plan = JSON.parse(packageJson);
5079		const subscriptionJson = localStorage.getItem('subscription');
5080		if (subscriptionJson === undefined || subscriptionJson === '') {
5081			alog("Jump to subscriptions 20");
5082			window.location.href = 'subscriptions.jsp';
5083			return;
5084		}
5085		const subscription = JSON.parse(subscriptionJson);
5086
5087		const title = document.getElementById("title");
5088		if (parseFloat(plan.price) === 0) {
5089			setTextContentTranslationByElement(title, 'cancelSubscriptionOfType');
5090			title.textContent = title.textContent.replace("[Package]", subscription.type);
5091		} else {
5092			setTextContentTranslationByElement(title, 'choosePaymentMethod');
5093		}
5094
5095		const paymentItemList = document.getElementById("paymentItemList");
5096		addPaymentItem(paymentItemList, plan, true);
5097
5098		function subscribe(event) {
5099			if (document.getElementById('btn_spinner'))
5100				return;
5101			const spinner = generateSpinner();
5102			event.currentTarget.appendChild(spinner);
5103
5104			const url = endpoint + "/payment/pay";
5105			const data = {
5106				sid: getSid(),
5107				subscriptionId: subscription.id,
5108				subscriptionTypeId: plan.id,
5109			};
5110			postRequest(url, data).then(async response => {
5111				if (response.ok) {
5112					const responseBody = await response.json();
5113					if (responseBody.statusCode === 0) {
5114						alog("Jump to " + responseBody.content + " 21");
5115						window.location.href = responseBody.content;
5116						removeSpinner(spinner);
5117					} else {
5118						removeSpinner(spinner);
5119						showAlert("Error: " + responseBody.statusMessage, "error");
5120					}
5121				} else {
5122					removeSpinner(spinner);
5123					showAlert("Error: Response failed!", "error");
5124				}
5125			});
5126		}
5127
5128		if (plan.price > 0) {
5129			const btnPaypal = document.getElementById("paypal");
5130			btnPaypal.classList.remove('d-none');
5131			btnPaypal.addEventListener('click', subscribe);
5132
5133			const btnCreditCard = document.getElementById("credit_card");
5134			btnCreditCard.classList.remove('d-none');
5135			btnCreditCard.addEventListener('click', subscribe);
5136		} else {
5137			const btnFree = document.getElementById("btn_free");
5138			btnFree.classList.remove('d-none');
5139			btnFree.addEventListener('click', subscribe);
5140		}
5141
5142		//	document.getElementById("paypal").href = getReturnAddress(package.buyLink);
5143		//	document.getElementById("credit_card").href = getReturnAddress(package.buyLink);
5144
5145		//const goBack = document.getElementById("go_back");
5146		// goBack.addEventListener('click', function () {
5147		//     window.location.href = 'subscriptions.jsp';
5148		// });
5149		//goBack.href = 'subscriptions.jsp';
5150	}
5151
5152	function loadCheckoutPayment() {
5153		const paymentJson = localStorage.getItem('payment');
5154		if (paymentJson === undefined || paymentJson === '') {
5155			alog("Jump to subscriptions 22");
5156			window.location.href = 'subscriptions.jsp';
5157			return;
5158		}
5159
5160
5161		const title = document.getElementById("title");
5162		setTextContentTranslationByElement(title, 'choosePaymentMethod');
5163
5164		const payment = JSON.parse(paymentJson);
5165		const paymentItemList = document.getElementById("paymentItemList");
5166		// payment.items.forEach(item => {
5167		// 	addPaymentItem(paymentItemList, item);
5168		// });
5169
5170		let totalPrice = 0;
5171		for (let i = 0; i < payment.items.length; i++) {
5172			// if (i>0) {
5173			// 	const line = document.createElement('hr');
5174			// 	paymentItemList.appendChild(line);
5175			// }
5176
5177			addPaymentItem(paymentItemList, payment.items[i]);
5178			totalPrice += payment.items[i].price;
5179
5180			const line = document.createElement('hr');
5181			paymentItemList.appendChild(line);
5182
5183			// if (i === payment.items.length - 1) {
5184			// 	line.classList.add('mb-5');
5185			// }
5186		}
5187
5188		const totalPaymentItem = {
5189			name: translations['total'],
5190			//Total Payment Amount of [itemCount] item(s)
5191			description: translations['totalPaymentAmount'].replace('[itemCount]', payment.items.length),
5192			price: totalPrice,
5193			currency: payment.items[0].currency
5194		};
5195		addPaymentItem(paymentItemList, totalPaymentItem, true);
5196
5197		const btnPaypal = document.getElementById("paypal");
5198		btnPaypal.classList.remove('d-none');
5199		btnPaypal.addEventListener('click', function() {
5200			if (document.getElementById('btn_spinner'))
5201				return;
5202			const spinner = generateSpinner();
5203			btnPaypal.appendChild(spinner);
5204
5205			const url = endpoint + "/payment/checkout";
5206			const data = {
5207				sid: getSid(),
5208				// projectTypeId: 2,
5209				paymentId: payment.id,
5210				source: "paypal"
5211			};
5212			postRequest(url, data).then(async response => {
5213				if (response.ok) {
5214					const responseBody = await response.json();
5215					if (responseBody.statusCode === 0) {
5216						alog("Jump to " + responseBody.content + " 23");
5217						window.location.href = responseBody.content;
5218						removeSpinner(spinner);
5219					} else {
5220						removeSpinner(spinner);
5221						showAlert("Error: " + responseBody.statusMessage, "error");
5222					}
5223				}
5224			});
5225		});
5226
5227		const btnCreditCard = document.getElementById("credit_card");
5228		btnCreditCard.classList.remove('d-none');
5229		btnCreditCard.addEventListener('click', function() {
5230			if (document.getElementById('btn_spinner'))
5231				return;
5232			const spinner = generateSpinner();
5233			btnCreditCard.appendChild(spinner);
5234
5235			const url = endpoint + "/payment/checkout";
5236			const data = {
5237				sid: getSid(),
5238				// projectTypeId: 2,
5239				paymentId: payment.id,
5240				source: 'card'
5241			};
5242			postRequest(url, data).then(async response => {
5243				if (response.ok) {
5244					const responseBody = await response.json();
5245					if (responseBody.statusCode === 0) {
5246						alog("Jump to " + responseBody.content + " 24");
5247						window.location.href = responseBody.content;
5248						removeSpinner(spinner);
5249					} else {
5250						removeSpinner(spinner);
5251						showAlert("Error: " + responseBody.statusMessage, "error");
5252					}
5253				}
5254			});
5255		});
5256
5257		const goBack = document.getElementById("go_back");
5258		goBack.addEventListener('click', function() {
5259			window.location.href = 'subscriptions.jsp';
5260		});
5261	}
5262
5263function generateSpinner() {
5264	const spinner = document.createElement('span');
5265	spinner.id = 'btn_spinner'
5266	spinner.attributes['role'] = 'status';
5267	spinner.attributes['aria-hidden'] = 'true';
5268	spinner.classList.add('spinner-border', 'spinner-border-sm', 'ms-2', 'text-light');
5269	// spinner.style.width = '1.4rem';
5270	// spinner.style.height = '1.4rem';
5271	spinner.started = new Date();
5272	return spinner;
5273}
5274
5275function removeSpinner(spinner, ff) {
5276	var endDate = new Date();
5277	var msecs = endDate.getTime() - spinner.started.getTime();
5278	console.debug("msecs=" + msecs);
5279	if (msecs >= 2000) {
5280		spinner.remove();
5281		if (ff)
5282			ff();
5283	}
5284	else
5285		setTimeout(function() { removeSpinner(spinner, ff); }, 2001 - msecs);
5286}
5287
5288
5289function addPaymentItem(container, item, isTotal) {
5290	const itemContainer = document.createElement('div');
5291	itemContainer.classList.add('d-flex', 'align-items-stretch'); //border-top
5292	container.appendChild(itemContainer);
5293
5294	const textContainer = document.createElement('div');
5295	textContainer.classList.add('d-flex', 'flex-column', 'flex-grow-1');
5296	itemContainer.appendChild(textContainer);
5297
5298	const itemName = document.createElement('p');
5299	itemName.classList.add('card-text', 'fs-5');
5300	itemName.textContent = item.name;
5301	textContainer.appendChild(itemName);
5302
5303	const itemDescription = document.createElement('p');
5304	itemDescription.classList.add('card-text', 'text-muted', 'fs-6');
5305	itemDescription.innerHTML = item.description;
5306	textContainer.appendChild(itemDescription);
5307
5308	const itemPrice = document.createElement('p');
5309	itemPrice.classList.add('card-text', 'ms-auto');
5310	if (isTotal) {
5311		itemPrice.classList.add('fw-bold', 'fs-3');
5312	} else {
5313		itemPrice.classList.add('fs-5');
5314	}
5315	// itemPrice.textContent = item.price + ' ' + item.currency;
5316	itemPrice.textContent = item.price + ' ' + item.currency;
5317	itemContainer.appendChild(itemPrice);
5318}
5319
5320
5321/* PAYMENT RESULTS */
5322
5323function loadPaymentResult() {
5324	if (checkIfLoginRequired() )
5325		return;
5326	const params = new URLSearchParams(window.location.search);
5327	if (params.has('state')) {
5328		const state = params.get("state");
5329		if (state === "1") {
5330			const successSection = document.getElementById("success_section");
5331			setTextContentTranslationByElement(successSection.getElementsByTagName('h3')[0], 'paymentIsSuccessful');
5332			setTextContentTranslationByElement(successSection.getElementsByTagName('p')[0], 'subscriptionPaymentSuccess');
5333			setTextContentTranslationByElement(successSection.getElementsByTagName('a')[0], 'goSubscriptions');
5334			successSection.classList.remove('d-none');
5335		} else if (state === "0") {
5336			const failureSection = document.getElementById("failure_section");
5337			setTextContentTranslationByElement(failureSection.getElementsByTagName('h3')[0], 'paymentFailed');
5338			setTextContentTranslationByElement(failureSection.getElementsByTagName('p')[0], 'subscriptionPaymentFailed');
5339			setTextContentTranslationByElement(failureSection.getElementsByTagName('a')[0], 'goSubscriptions');
5340			failureSection.classList.remove('d-none');
5341		} else if (state === "2") {
5342			const expiredSection = document.getElementById("expired_section");
5343			setTextContentTranslationByElement(expiredSection.getElementsByTagName('h3')[0], 'paymentExpired');
5344			setTextContentTranslationByElement(expiredSection.getElementsByTagName('p')[0], 'subscriptionPaymentExpired');
5345			setTextContentTranslationByElement(expiredSection.getElementsByTagName('a')[0], 'goSubscriptions');
5346			expiredSection.classList.remove('d-none');
5347		} else
5348			showAlert("Invalid payment state!", "error");
5349	}
5350}
5351
5352
5353/* PAYMENTS */
5354
5355async function loadPayments() {
5356	if (checkIfLoginRequired())
5357		return;
5358
5359	setTextContentTranslationById('title', 'paymentHistory');
5360	const table = document.getElementById('payments_table');
5361	const ths = table.getElementsByTagName('th');
5362	let colIndex = 0;
5363	if (isReseller)
5364		ths[0].remove();
5365	else
5366		setTextContentTranslationByElement(ths[colIndex++], 'plan');
5367	setTextContentTranslationByElement(ths[colIndex++], 'date');
5368	setTextContentTranslationByElement(ths[colIndex++], 'price');
5369	setTextContentTranslationByElement(ths[colIndex++], 'invoice');
5370
5371	const data = {
5372		sid: getSid(),
5373	};
5374	postRequest(endpoint + '/payment/list', data).then(async response => {
5375		if (response.ok) {
5376			const table = document.getElementById('payments_table');
5377
5378			while (table.rows.length > 1) {
5379				table.deleteRow(1);
5380			}
5381
5382			const responseBody = await response.json();
5383			if (responseBody.statusCode === 0) {
5384				const checkouts = [];
5385				responseBody.content.forEach(payment => {
5386					if (payment.state === 3) {//Completed
5387						const row = table.insertRow(-1);
5388						if (!isReseller)
5389							row.insertCell(-1).textContent = payment.subscriptionType;
5390						row.insertCell(-1).textContent = payment.createdDate;
5391
5392						const totalPrice = payment.items.reduce((total, item) => total + item.price, 0);
5393						row.insertCell(-1).textContent = totalPrice + ' ' + payment.items[0].currency;
5394
5395						const invoiceCell = document.createElement('td');
5396						row.appendChild(invoiceCell);
5397						if (payment.invoiceCreated) {
5398							const invoiceOpenLink = document.createElement('a');
5399							invoiceOpenLink.classList.add('btn', 'btn-outline-primary', 'btn-sm');
5400							invoiceOpenLink.href = endpoint + '/payment/invoice/' + payment.id + '/open';
5401							invoiceOpenLink.target = '_blank';
5402							invoiceOpenLink.textContent = 'Open';
5403							invoiceCell.appendChild(invoiceOpenLink);
5404
5405							const invoiceDownloadLink = document.createElement('a');
5406							invoiceDownloadLink.classList.add('btn', 'btn-outline-primary', 'btn-sm', 'ms-2');
5407							invoiceDownloadLink.href = endpoint + '/payment/invoice/' + payment.id + '/download';
5408							invoiceDownloadLink.textContent = 'Download';
5409							invoiceCell.appendChild(invoiceDownloadLink);
5410						}
5411					} else if (payment.state === 0) {//Pending
5412						checkouts.push(payment);
5413					}
5414				});
5415
5416				if (responseBody.content.length === 0) {
5417					table.innerHTML = translations['noPaymentsFound'] || 'No payments yet.';
5418					table.classList.add('no-projects-link');
5419				}
5420
5421				if (isReseller) {
5422					if (checkouts && checkouts.length > 0)
5423						displayCheckouts(checkouts);
5424					else
5425						displayUsedDomainCount();
5426				}
5427			} else {
5428				console.log("Error: " + responseBody.statusMessage);
5429			}
5430		}
5431	});
5432}
5433
5434async function getProjects() {
5435	const data = { sid: getSid(), projectTypeId: 2 };
5436	const response = await postRequest(endpoint + '/project/list', data);
5437	//debugger;
5438	if (response.ok) {
5439		const responseBody = await response.json();
5440		if (responseBody.statusCode === 0)
5441			return responseBody.contents;
5442		else if (responseBody.statusCode === 3) {
5443			deleteCookie('sid');
5444			deleteCookie('name');
5445			deleteCookie('email');
5446			clearLocalStorage();
5447			alog("Jump to login 23");
5448			window.location.href = '../views/login.jsp';
5449		}
5450	} else {
5451		console.log('Error: ' + response.statusText);
5452	}
5453	return null;
5454}
5455
5456async function loadProjectPayments(projectId) {
5457	const data = {
5458		sid: getSid(),
5459		projectId: projectId
5460	};
5461	postRequest(endpoint + '/payment/list', data).then(async response => {
5462		if (response.ok) {
5463			const table = document.getElementById('payments_table');
5464
5465			while (table.rows.length > 1) {
5466				table.deleteRow(1);
5467			}
5468
5469			const responseBody = await response.json();
5470			if (responseBody.statusCode === 0) {
5471				// const tableBody = table.getElementsByTagName('tbody')[0];
5472				responseBody.content.forEach(payment => {
5473					const row = table.insertRow(-1);
5474					row.insertCell(-1).textContent = payment.projectName;
5475					row.insertCell(-1).textContent = payment.subscriptionType;
5476					row.insertCell(-1).textContent = payment.amount + ' ' + payment.currency;
5477					row.insertCell(-1).textContent = payment.createdDate;
5478
5479					const invoiceCell = document.createElement('td');
5480					row.appendChild(invoiceCell);
5481
5482					if (payment.invoiceCreated) {
5483						const invoiceOpenLink = document.createElement('a');
5484						invoiceOpenLink.classList.add('btn', 'btn-outline-primary', 'btn-sm');
5485						invoiceOpenLink.href = endpoint + '/payment/invoice/' + payment.id + '/open';
5486						invoiceOpenLink.target = '_blank';
5487						invoiceOpenLink.textContent = 'Open';
5488						invoiceCell.appendChild(invoiceOpenLink);
5489
5490						const invoiceDownloadLink = document.createElement('a');
5491						invoiceDownloadLink.classList.add('btn', 'btn-outline-primary', 'btn-sm', 'ms-2');
5492						invoiceDownloadLink.href = endpoint + '/payment/invoice/' + payment.id + '/download';
5493						invoiceDownloadLink.textContent = 'Download';
5494						invoiceCell.appendChild(invoiceDownloadLink);
5495					}
5496				});
5497			} else {
5498				console.log("Error: " + responseBody.statusMessage);
5499			}
5500		}
5501	});
5502}
5503
5504
5505/* CONTACT US */
5506
5507function loadContactUs() {
5508	setTextContentTranslationById('contact_us_title', 'contactUs');
5509	setTextContentTranslationByQuery('label[for="email"]', 'email');
5510	setPlaceholderTranslationById('email', 'enterYourEmail');
5511	setTextContentTranslationByQuery('label[for="message"]', 'message');
5512	setPlaceholderTranslationById('message', 'enterYourMessage');
5513	setTextContentTranslationByQuery('button[type="submit"]', 'send');
5514
5515	const email = getCookie('email');
5516	const emailElement = document.getElementById('email');
5517	emailElement.value = email;
5518
5519	const contactForm = document.getElementById('contact_form');
5520	contactForm.addEventListener('submit', event => {
5521		event.preventDefault();
5522		onContact(contactForm);
5523	});
5524}
5525
5526function onContact(contactForm) {
5527	const email = contactForm.email.value;
5528	const message = contactForm.message.value;
5529	const data = { sid: getSid(), email: email, message: message, projectTypeId: 2 };
5530	postRequest(endpoint + '/contactus', data).then(async response => {
5531		if (response.ok) {
5532			const responseBody = await response.json();
5533			if (responseBody.statusCode === 0) {
5534				displayAlert('Your message has been sent successfully!', 'success');
5535				console.log('Your message has been sent successfully!');
5536				contactForm.reset();
5537				contactForm.email.value = email;
5538			} else {
5539				showAlert('Error: ' + responseBody.statusMessage, "error");
5540			}
5541		}
5542	});
5543}
5544
5545
5546async function postRequest(url, data) {
5547	return fetch(url, {
5548		//mode: 'cors',
5549		method: 'POST',
5550		headers: {
5551			'Content-Type': 'application/json; charset=UTF-8'
5552		},
5553		body: JSON.stringify(data)
5554	}).then((response) => {
5555		if (response.ok) {
5556			return response;
5557		} else if (response.status === 401) {
5558			logout();
5559		} else {
5560			console.error('Request failed: ' + response.statusText);
5561			return null;
5562		}
5563	});
5564}
5565
5566function getRequest(url) {
5567	return fetch(url, {
5568		//mode: 'cors',
5569		method: 'GET',
5570		// headers: {
5571		//     'Content-Type': 'application/json'
5572		// }
5573	});
5574}
5575
5576function createCookie(key, value) {
5577	const age = 60 * 60 * 24 * 7; // 7 days
5578	document.cookie = key + "=" + value + "; path=/; max-age=" + age;
5579}
5580
5581function getCookie(key) {
5582	const cookies = document.cookie.split("; ");
5583	for (let i = 0; i < cookies.length; i++) {
5584		const pair = cookies[i].split("=");
5585		if (pair[0] === key)
5586			return pair[1];
5587	}
5588	return null;
5589}
5590
5591function getSid() {
5592	return getCookie('sid');
5593}
5594
5595function deleteCookie(key) {
5596	document.cookie = key + "=; path=/; max-age=0";
5597}
5598
5599function clearLocalStorage() {
5600	localStorage.removeItem('project');
5601	localStorage.removeItem('buying_package');
5602	localStorage.removeItem('payment');
5603}
5604
5605
5606function displayCheckouts(checkouts) {
5607	// const userAlert = document.getElementById('user_alert');
5608	// const messageElement = userAlert.getElementsByTagName('p')[0];
5609	// let text = checkouts.length > 1 ? translations['pendingCheckoutMsgPlural'] : translations['pendingCheckoutMsg'];
5610	// messageElement.innerText = text.replace('[checkoutCount]', checkouts.length);
5611	// userAlert.classList.remove('d-none');
5612	
5613	displayAlert(translations['pendingCheckoutMsg'].replace('[checkoutCount]', checkouts.length));
5614
5615	const checkoutsElement = document.getElementById('checkouts');
5616	checkoutsElement.classList.remove('d-none');
5617
5618	checkouts.forEach(checkout => {
5619		const items = checkout.items;
5620
5621		const card = document.createElement('div');
5622		card.classList.add('card', 'mb-4', 'w-100', 'd-flex', 'align-items-stretch');
5623		checkoutsElement.appendChild(card);
5624
5625		const row = document.createElement('div');
5626		row.classList.add('row');
5627		card.appendChild(row);
5628
5629		const col1 = document.createElement('div');
5630		col1.classList.add('col-md-9', 'd-flex', 'align-items-stretch');
5631		row.appendChild(col1);
5632
5633		const cardBody = document.createElement('div');
5634		cardBody.classList.add('card-body');
5635		col1.appendChild(cardBody);
5636
5637		addPaymentItem(cardBody, items[0]);
5638		let totalPrice = items[0].price;
5639
5640		if (items.length > 1) {
5641			const moreList = document.createElement('div');
5642			moreList.classList.add('d-none');
5643			cardBody.appendChild(moreList);
5644
5645			for (let i = 1; i < items.length; i++) {
5646				const line = document.createElement('hr');
5647				moreList.appendChild(line);
5648
5649				const item = items[i];
5650				addPaymentItem(moreList, item);
5651				totalPrice += item.price;
5652			}
5653
5654			const showMoreButton = document.createElement('button');
5655			showMoreButton.classList.add('btn', 'btn-link', 'text-decoration-none', 'text-muted', 'fw-bold', 'mt-3', 'p-0');
5656			// showMoreButton.textContent = 'Show all (' + (items.length - 1) + ')';
5657			setTextContentTranslationByElement(showMoreButton, 'showAll');
5658			const angleDown = document.createElement('i');
5659			angleDown.classList.add('fas', 'fa-angle-down', 'ms-1');
5660			showMoreButton.appendChild(angleDown);
5661			cardBody.appendChild(showMoreButton);
5662			showMoreButton.addEventListener('click', (event) => {
5663				event.preventDefault();
5664				// const showMoreButton = event.target;
5665
5666				if (moreList.classList.contains('d-none')) {
5667					showMoreButton.textContent = translations['showLess'];
5668					const angleUp = document.createElement('i');
5669					angleUp.classList.add('fas', 'fa-angle-up', 'ms-1');
5670					showMoreButton.appendChild(angleUp);
5671					moreList.classList.remove('d-none');
5672				} else {
5673					showMoreButton.textContent = translations['showAll'];
5674					showMoreButton.appendChild(angleDown);
5675					moreList.classList.add('d-none');
5676				}
5677				// showMoreButton.remove();
5678			});
5679		}
5680
5681		const col2 = document.createElement('div');
5682		col2.classList.add('col-md-3', 'd-block', 'py-3');
5683		row.appendChild(col2);
5684
5685		const paymentPrice = document.createElement('span');
5686		// paymentPrice.classList.add('d-flex', 'flex-column', 'align-items-center', 'justify-content-center', 'w-100', 'h-100');
5687		paymentPrice.classList.add('d-block', 'text-center', 'mb-2', 'fw-bold', 'fs-4');
5688		paymentPrice.textContent = totalPrice + ' ' + items[0].currency;
5689		col2.appendChild(paymentPrice);
5690
5691		const payButtonContainer = document.createElement('div');
5692		payButtonContainer.classList.add('d-flex', 'align-items-center', 'justify-content-center', 'w-100');
5693		col2.appendChild(payButtonContainer);
5694
5695		const payButton = document.createElement('button');
5696		setTextContentTranslationByElement(payButton, 'pay');
5697		payButton.classList.add('btn', 'btn-success', 'w-75');
5698		payButtonContainer.appendChild(payButton);
5699
5700		payButton.addEventListener('click', async (event) => {
5701			event.preventDefault();
5702			localStorage.setItem('payment', JSON.stringify(checkout));
5703			window.location.href = 'payment.jsp?subject=checkout&cid=' + checkout.id;
5704		});
5705	});
5706}
5707
5708function displayUsedDomainCount() {
5709	if (usedDomainCount > 0)
5710		displayAlert(translations['usingDomainCountMsg'].replace('[domainCount]', usedDomainCount), 'info');
5711	else
5712		displayAlert(translations['notUsingAnyDomainMsg'], 'info');
5713}
5714
5715// function togglePasswordVisibility() {
5716	//	const passwordInput = document.getElementById("password");
5717	//	const passwordIcon = document.getElementById("password_icon");
5718	//
5719	//	if (passwordInput.type === "password") {
5720	//		passwordInput.type = "text";
5721	//		passwordIcon.classList.remove("fa-eye");
5722	//		passwordIcon.classList.add("fa-eye-slash");
5723	//	} else {
5724	//		passwordInput.type = "password";
5725	//		passwordIcon.classList.remove("fa-eye-slash");
5726	//		passwordIcon.classList.add("fa-eye");
5727	//	}
5728// }
5729
5730function numberToText(num){
5731	num = num.toString().replace(/[^0-9.]/g, '');
5732	if (num < 1000)
5733		return [num, "", ""];
5734
5735	let si = [
5736		// { v: 1E3, s: "K", c: "text-success" },
5737		// { v: 1E6, s: "M", c: "text-warning" },
5738		// { v: 1E9, s: "B", c: "text-danger" },
5739		// { v: 1E12, s: "T", c: "text-danger" },
5740		// { v: 1E15, s: "P", c: "text-danger" },
5741		// { v: 1E18, s: "E", c: "text-danger" }
5742		{ v: 1E3, s: "k", c: "text-success" },
5743		{ v: 1E6, s: "m", c: "text-warning" },
5744		{ v: 1E9, s: "b", c: "text-danger" },
5745		{ v: 1E12, s: "T", c: "text-danger" },
5746		{ v: 1E15, s: "P", c: "text-danger" },
5747		{ v: 1E18, s: "E", c: "text-danger" }
5748	];
5749	let index;
5750	for (index = si.length - 1; index > 0; index--) {
5751		if (num >= si[index].v) {
5752			break;
5753		}
5754	}
5755
5756	//	return (num / si[index].v).toFixed(2).replace(/\.0+$|(\.[0-9]*[1-9])0+$/, "$1") + si[index].s;
5757
5758	const value = (num / si[index].v).toFixed(2).replace(/\.0+$|(\.[0-9]*[1-9])0+$/, "$1");
5759	const ending = si[index].s;
5760	const color = si[index].c;
5761	return [value, ending, color];
5762}
5763
5764// visitsTotal -> Visits Total
5765function keyToTitle(key) {
5766	let title = "";
5767	for (let i = 0; i < key.length; i++) {
5768		if (i === 0)
5769			title = key[0].toUpperCase();
5770		else if (key[i] === key[i].toUpperCase())
5771			title += " " + key[i];
5772		else
5773			title += key[i];
5774	}
5775	return title;
5776}
5777
5778function displayAlert(message, type = 'danger', sticky = false) {
5779	if (type === 'error')
5780		type = 'danger';
5781
5782	// Transient notices are toasts (top-right, auto-closing, not in the content
5783	// flow — an inline message above a long form was easy to miss). Messages here
5784	// may carry markup (<br>, links, translated strings), hence html: true.
5785	if (!sticky) {
5786		showToast(translations[message] || message, type, { html: true });
5787		return;
5788	}
5789
5790	// sticky: a page-state notice with a link/action that must stay until the
5791	// user closes it (plan-limit banner) — rendered inline in #alert_container.
5792	const alertText = document.createElement('div');
5793	alertText.classList.add('alert', 'alert-' + type, 'mb-2');
5794	alertText.setAttribute('role', 'alert');
5795	alertText.innerHTML = translations[message] || message;
5796
5797	const alertContainer = document.getElementById('alert_container');
5798	if (!alertContainer)
5799		return;
5800	alertContainer.innerHTML = '';
5801	alertContainer.classList.add('pb-1');
5802	alertContainer.appendChild(alertText);
5803
5804	const closeBtn = document.createElement('button');
5805	closeBtn.type = 'button';
5806	closeBtn.classList.add('btn-close');
5807	closeBtn.style.position = 'absolute';
5808	closeBtn.style.top = '10px';
5809	closeBtn.style.right = '5px';
5810	closeBtn.style.width = '5px';
5811	closeBtn.style.height = '5px';
5812	closeBtn.setAttribute('aria-label', translations['close'] || 'Close');
5813	closeBtn.addEventListener('click', function() {
5814		alertText.remove();
5815	});
5816	alertText.appendChild(closeBtn);
5817}
5818
5819function removeAfterLast(text, searchElement) {
5820	const lastSpaceIndex = text.lastIndexOf(searchElement);
5821	if (lastSpaceIndex > -1)
5822		return text.substring(0, lastSpaceIndex);
5823	return text;
5824}
5825
5826
5827function setTooltips() {
5828	const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
5829	tooltipTriggerList.map(function(tooltipTriggerEl) {
5830		return new bootstrap.Tooltip(tooltipTriggerEl);
5831	});
5832}
5833
5834/*function setCancelBtnClicking(page) {
5835	const btnCancel = document.getElementById('btn_cancel');
5836	if (!btnCancel)
5837		return;
5838
5839	btnCancel.addEventListener('click', function() {
5840		const params = new URLSearchParams(window.location.search);
5841		const pp = params.get("pp");
5842		if (pp) {
5843			switch (pp) {
5844				case 'w':
5845					window.location.href = "websites.jsp";
5846					break;
5847				case 't':
5848					window.location.href = "templates.jsp";
5849					break;
5850				default:
5851					window.location.href = "dashboard.jsp";
5852			}
5853		} else {
5854			switch (page) {
5855				case 'template_settings.jsp':
5856					window.location.href = "templates.jsp";
5857					break;
5858				case 'website_settings.jsp':
5859					window.location.href = "websites.jsp";
5860					break;
5861				default:
5862					window.location.href = "dashboard.jsp";
5863			}
5864		}
5865	});
5866}*/
5867
5868async function loadResellers() {
5869	const btnConfirm = document.getElementById('btn_resellersavesettings');
5870
5871	if (checkIfLoginRequired()  || !btnConfirm)
5872		return;
5873	
5874	var setg = document.getElementById('setgoogleservers');
5875	if (setg){
5876		setg.onclick = function(){
5877			event.preventDefault();
5878			document.getElementById('smtphost').value = 'smtp.gmail.com';
5879			document.getElementById('smtpport').value = '587';
5880			document.getElementById('enableauth').checked='on';
5881			document.getElementById('enabletls').checked='on';
5882		};
5883	}
5884	
5885	btnConfirm.textContent = "Save";
5886	setTextContentTranslationByElement(btnConfirm, 'save');
5887
5888	const logo = document.getElementById('panellogo');
5889	const fav = document.getElementById('panelfav');
5890	const css = document.getElementById('panelcss');
5891	const title = document.getElementById('paneltitle');
5892	const url = document.getElementById('panelurl');
5893	const consentCssUrl = document.getElementById('consentcss');
5894	const consentLogoUrl = document.getElementById('consentlogo');
5895	const dpaUrl = document.getElementById('dpaurl');
5896	const termsUrl = document.getElementById('termsofuseurl');
5897	const privUrl = document.getElementById('privacypolicyurl');
5898	const supportEmail = document.getElementById('supportemail');
5899	const domainName = document.getElementById('domainname');
5900	const productName = document.getElementById('productname');
5901	const vendorName = document.getElementById('vendorname');
5902
5903	const emailsender =  document.getElementById('emailsender');
5904	const emailname = document.getElementById('emailname');
5905	const emailpass = document.getElementById('emailpass');
5906	const smtphost = document.getElementById('smtphost');
5907	const smtpport = document.getElementById('smtpport');
5908	const enableauth = document.getElementById('enableauth');
5909	const enabletls = document.getElementById('enabletls');
5910
5911	const usersTable = document.getElementById('reseller_users_table');
5912	const tableCols = usersTable.querySelector('thead tr');
5913
5914	setTextContentTranslationByElement(tableCols.children[0], 'id');
5915	setTextContentTranslationByElement(tableCols.children[1], 'email');
5916	setTextContentTranslationByElement(tableCols.children[2], 'enabled');
5917	setTextContentTranslationByElement(tableCols.children[3], 'actions');
5918
5919	btnConfirm.onclick = async function(event) {
5920		event.preventDefault();
5921
5922		btnConfirm.textContent = 'updating...';
5923		setTextContentTranslationByElement(btnConfirm, 'updating');
5924		const spinner = generateSpinner();
5925		btnConfirm.appendChild(spinner);
5926
5927		const data = {
5928			sid: getSid(), panelLogoUrl: logo.value, panelCssUrl: css.value, panelTitle: title.value, 
5929			panelHostName: url.value, panelFaviconUrl: fav.value, 
5930			consentCssUrl: consentCssUrl.value, consentLogoUrl: consentLogoUrl.value,
5931			smtpEmail: emailname.value, smtpPass:emailpass.value, emailSender: emailsender.value,
5932			smtpHost: smtphost.value, smtpPort:smtpport.value, smtpAuth: enableauth.checked?'1':'0',
5933			smtpTls:enabletls.checked?'1':'0', 
5934			dpaAgreementUrl: dpaUrl.value, termsOfUseUrl: termsUrl.value, privacyPolicyUrl:privUrl.value,
5935			productName:productName.value, domainName: domainName.value, supportEmail: supportEmail.value,
5936			vendorName:vendorName.value
5937		};
5938		const response = await postRequest(endpoint + '/reseller/settings', data);
5939
5940		removeSpinner(spinner, () => {
5941			btnConfirm.textContent = "Save";
5942			setTextContentTranslationByElement(btnConfirm, 'save');
5943		});
5944
5945		if (response.ok) {
5946			const responseBody = await response.json();
5947			if (responseBody.statusCode === 0) {
5948				window.location.reload();
5949			} else {
5950				showAlert("Error: " + responseBody.statusMessage, "error");
5951			}
5952		} else {
5953			showAlert("Error: " + (response.statusMessage || "Response failed"), "error");
5954		}
5955	};
5956
5957	//const btnConfirm = document.getElementById('btn_resellercreate_user');
5958	const data = { sid: getSid() };
5959	const response = await postRequest(endpoint + '/reseller/settings', data);
5960	if (response.ok) {
5961		const responseBody = await response.json();
5962		if (responseBody.statusCode === 0) {
5963			//console.dir("=============================");
5964			//console.dir(responseBody);
5965			var set = responseBody.settings;
5966			logo.value = set.panelLogoUrl;
5967			css.value = set.panelCssUrl;
5968			title.value = set.panelTitle;
5969			url.value = set.panelHostName?set.panelHostName:set.domainName;
5970			fav.value = set.panelFaviconUrl;
5971			emailsender.value = set.emailSender;
5972			consentcss.value = set.consentCssUrl;
5973			consentlogo.value = set.consentLogoUrl;
5974			domainName.value = set.domainName;
5975			termsUrl.value= set.termsOfUseUrl;
5976			privUrl.value=set.privacyPolicyUrl;
5977			dpaUrl.value = set.dpaAgreementUrl;
5978			supportEmail.value = set.supportEmail;
5979			productName.value = set.productName;
5980			vendorName.value = set.vendorName || set.productName;
5981			emailname.value = set.smtpEmail; 
5982			emailpass.value = set.smtpPass;
5983			smtphost.value = set.smtpHost;
5984			smtpport.value = set.smtpPort;
5985			enableauth.checked = set.smtpAuth === '1';
5986			enabletls.checked = set.smtpTls === '1';
5987 
5988			const users = responseBody.settings.users;
5989			if (!users || !users.length) {
5990				document.getElementById('nousersfound').innerHTML = translations["noUsersFound"] ? translations["noUsersFound"] : 'No users Found';
5991				return;
5992			}
5993
5994			usersTable.style.display = '';
5995			const tableBody = usersTable.querySelector('tbody');
5996			users.forEach(project => {
5997				const row = document.createElement('tr');
5998
5999				var nameCell = document.createElement('td');
6000				nameCell.textContent = project.id;
6001				row.appendChild(nameCell);
6002
6003				const websitesCell = document.createElement('td');
6004				websitesCell.textContent = project.name + '(' +  project.email +')';
6005				row.appendChild(websitesCell);
6006
6007				const enabledCell = document.createElement('td');
6008				enabledCell.classList.add('text-center');
6009				const enabledIcon = document.createElement('i');
6010				enabledIcon.classList.add('fa', project.enabled ? 'fa-check' : 'fa-times');
6011				enabledIcon.classList.add(project.enabled ? 'text-success' : 'text-danger');
6012				enabledCell.appendChild(enabledIcon);
6013				row.appendChild(enabledCell);
6014
6015				const actionCell = document.createElement('td');
6016				actionCell.classList.add('text-center');
6017
6018				// set password, set email
6019				addActionButtonToToolbar(actionCell, 'Settings', 'settings', () => {
6020					alog("Jump to user settings " + project.id);
6021					window.location.href = "reseller_user.jsp?id=" + project.id;
6022				});
6023
6024				/*addActionButtonToToolbar(actionCell, project.enabled ? 'Disable' : 'Enable', project.enabled ? 'disable' : 'enable', function(btn) {
6025				});
6026				addActionButtonToToolbar(actionCell, 'Settings', 'settings', () => {
6027					alog("Jump to user settings " + project.id);
6028					window.location.href = "reseller_user.jsp?id=" + project.id;
6029				});*/
6030
6031				addActionButtonToToolbar(actionCell, 'Impersonate', 'impersonate', async function() {
6032					const data = { sid: getSid(), uid: project.id };
6033					const response = await postRequest(endpoint + '/reseller/impersonate', data);
6034					if (response.ok) {
6035						const responseLogin = await response.json();
6036						createSessionCookie(responseLogin.auth);
6037						console.dir(responseLogin);
6038						console.dir(responseLogin.content);
6039						createCookie("name", removeAfterLast(responseLogin.content.fullName, ' '));
6040						// createCookie("name", responseLogin.content.fullName.split(' ')[0]);
6041						createCookie("email", responseLogin.content.email);
6042						// createCookie("lang", responseLogin.lang);
6043						// window.location.href = responseLogin.numProjects ? "templates.jsp" : "template_settings.jsp?create=1";
6044						window.location.href = responseLogin.numProjects ? "dashboard.jsp" : "website_settings.jsp";
6045					} else {
6046						showAlert("Error: " + (response.statusMessage || "Response failed"), "error");
6047					}
6048				});
6049
6050				row.appendChild(actionCell);
6051				tableBody.appendChild(row);
6052			});
6053		} else {
6054			showAlert("Error: " + responseBody.statusMessage, "error");
6055		}
6056	} else {
6057		showAlert("Error: " + (response.statusMessage || "Response failed"), "error");
6058	}
6059}
6060
6061async function loadResellerUser() {
6062	if (checkIfLoginRequired() )
6063		return;
6064	const params = new URLSearchParams(window.location.search);
6065	const userid = params.get("id") || 0;
6066	const isCreate = "1" === params.get("create");
6067
6068	const btnConfirm = document.getElementById('btn_resellersaveuser');
6069	//btnConfirm.textContent = isCreate ? (translations["create"] ? translations["create"] : 'Create') :
6070	//	(translations["update"] ? translations["update"] : 'Update');
6071	setTextContentTranslationByElement(btnConfirm, isCreate ? 'create' : 'update');
6072
6073	const email = document.getElementById('email');
6074	const name = document.getElementById('name');
6075	const enabled = document.getElementById('enabled');
6076	const pass1 = document.getElementById('pass1');
6077	const pass2 = document.getElementById('pass2');
6078	const domains = document.getElementById('domains');
6079	const subsselect = document.getElementById('subsselect');
6080	const subdesc = document.getElementById('subdesc');
6081
6082	if (!isCreate) {
6083		document.getElementById('pwdsection2').style.display = '';
6084		document.getElementById('pwdsectiontitle').style.display = '';
6085		}
6086
6087		const data = { sid: getSid(), uid: userid };
6088		const response = await postRequest(endpoint + '/reseller/userget', data);
6089		if (response.ok) {
6090			const responseBody = await response.json();
6091			if (responseBody.statusCode === 0) {
6092				name.value = responseBody.user.name;
6093				email.value = responseBody.user.email;
6094				enabled.checked = responseBody.user.enabled;
6095				domains.value = responseBody.user.domains;
6096				for (var i = 0; i < responseBody.subscriptionTypes.length; i++) {
6097					var opt = document.createElement('option');
6098					var st = responseBody.subscriptionTypes[i];
6099					opt.value = st.id;
6100					opt.innerHTML = st.name;
6101					if (st.id == responseBody.user.subscriptionType) {
6102						opt.selected = true;
6103						subdesc.innerHTML = st.description;
6104					}
6105					subsselect.appendChild(opt);
6106				}
6107				subsselect.onchange = () => {
6108					for (var i = 0; i < responseBody.subscriptionTypes.length; i++) {
6109						if ("" + responseBody.subscriptionTypes[i].id == "" + subsselect.value)
6110							subdesc.innerHTML = responseBody.subscriptionTypes[i].description;
6111					}
6112				};
6113			} else {
6114				showAlert("Error: " + responseBody.statusMessage, "error");
6115			}
6116		} else {
6117			showAlert("Error: " + (response.statusMessage || "Response failed"), "error");
6118		}
6119
6120	btnConfirm.onclick = async function() {
6121		event.preventDefault();
6122
6123		btnConfirm.textContent = 'updating...';
6124		setTextContentTranslationByElement(btnConfirm, 'updating');
6125		const spinner = generateSpinner();
6126		btnConfirm.appendChild(spinner);
6127
6128		const data = {
6129			sid: getSid(), email: email.value, name: name.value, enabled: enabled.checked,
6130			pass: isCreate ? pass1.value : '', uid: userid, domains: domains.value, subscriptionType: subsselect.value
6131		};
6132		const response = await postRequest(endpoint + '/reseller/userupdate', data);
6133
6134		//console.dir("remove spinner");
6135
6136		removeSpinner(spinner, () => {
6137			btnConfirm.textContent = isCreate ? 'Create' : 'Update';
6138			setTextContentTranslationByElement(btnConfirm, isCreate ? 'create' : 'update');
6139		});
6140
6141		if (response.ok) {
6142			const responseBody = await response.json();
6143			if (responseBody.statusCode === 0) {
6144				window.location.href = 'reseller.jsp';
6145			} else {
6146				showAlert("Error: " + responseBody.statusMessage, "error");
6147			}
6148		} else {
6149			showAlert("Error: " + (response.statusMessage || "Response failed"), "error");
6150		}
6151	};
6152
6153
6154	const btnPwd = document.getElementById('btn_savepasswd');
6155	setTextContentTranslationByElement(btnPwd, 'update');
6156
6157	btnPwd.onclick = async function() {
6158		event.preventDefault();
6159
6160		btnPwd.textContent = 'updating...';
6161		setTextContentTranslationByElement(btnPwd, 'updating');
6162		const spinner = generateSpinner();
6163		btnPwd.appendChild(spinner);
6164
6165		const data = { sid: getSid(), pass: pass2.value, uid: params.get("id") };
6166		const response = await postRequest(endpoint + '/reseller/userpassword', data);
6167
6168		removeSpinner(spinner, () => {
6169			btnPwd.textContent = 'Update';
6170			setTextContentTranslationByElement(btnPwd, 'update');
6171		});
6172
6173		if (response.ok) {
6174			const responseBody = await response.json();
6175			if (responseBody.statusCode === 0) {
6176				window.location.href = 'reseller.jsp';
6177			} else {
6178				showAlert("Error: " + responseBody.statusMessage, "error");
6179			}
6180		} else {
6181			showAlert("Error: " + (response.statusMessage || "Response failed"), "error");
6182		}
6183	};
6184}
6185
6186function addActionButtonToToolbar(parent, labelText, labelKey, ff) {
6187	const button = document.createElement('button');
6188	button.textContent = translations[labelKey] ? translations[labelKey] : labelText;
6189	button.classList.add('btn', 'btn-outline-primary', 'btn-sm');
6190	button.addEventListener('click', () => ff(button));
6191	parent.appendChild(button);
6192	return button;
6193}
6194
6195function createSessionCookie(sessionId) {
6196	//    var sessionCookie = "sid=" + sessionId + "; path=/; max-age=86400"; // 24 hours
6197	//    document.cookie = sessionCookie;
6198	createCookie("sid", sessionId);
6199}
6200
6201
6202
6203///  Document Generator
6204
6205async function loadDocumentGenerator() {
6206    if (checkIfLoginRequired())
6207        return;
6208
6209    // Check if required DOM elements exist
6210    const projectDropdown = document.getElementById('select_domain');
6211    const languageDropdown = document.getElementById('select_language');
6212    const container = document.getElementById('document_generator_container');
6213
6214    if (!projectDropdown) {
6215        console.error('Project dropdown not found - make sure element with ID "select_domain" exists');
6216        return;
6217    }
6218
6219    if (!languageDropdown) {
6220        console.error('Language dropdown not found - make sure element with ID "select_language" exists');
6221        return;
6222    }
6223
6224    if (!container) {
6225        console.error('Document generator container not found - make sure element with ID "document_generator_container" exists');
6226        return;
6227    }
6228
6229    try {
6230        // Load project domains
6231        let projects = null;
6232        const data = { sid: getSid(), projectTypeId: 2, method: 1 };
6233        const response = await postRequest(endpoint + '/project-domain/list', data);
6234
6235        if (response.ok) {
6236            const responseBody = await response.json();
6237            if (responseBody.statusCode === 0) {
6238                projects = responseBody.domains;
6239            } else {
6240                console.error('Error loading projects: ' + responseBody.statusMessage);
6241                displayAlertDocument(translations['failedToLoadProjects'] || 'Failed to load projects: ' + responseBody.statusMessage, 'error');
6242                return;
6243            }
6244        } else {
6245            console.error('Error: ' + response.statusText);
6246            displayAlertDocument(translations['failedToLoadProjects'] || 'Failed to load projects: ' + response.statusText, 'error');
6247            return;
6248        }
6249
6250        // Clear and populate project dropdown
6251        projectDropdown.innerHTML = '';
6252
6253        if (projects && projects.length > 0) {
6254            projects.forEach(project => {
6255                const option = document.createElement('option');
6256                option.value = project.id;
6257                option.text = project.domainName;
6258                projectDropdown.appendChild(option);
6259            });
6260        } else {
6261            console.error('No projects available');
6262            displayAlertDocument(translations['noProjectsAvailable'] || 'No projects available', 'error');
6263            return;
6264        }
6265
6266        // Add project change event listener
6267        projectDropdown.addEventListener('change', function() {
6268            const selectedProjectId = this.value;
6269			console.log('selectedProjectId='+selectedProjectId);
6270            if (selectedProjectId) {
6271                loadCompanyInformation(selectedProjectId).then(() => {
6272                    populateCompanyData(window.companyData);
6273                }).catch(error => {
6274                    console.error('Error loading company information:', error);
6275                });
6276            }
6277        });
6278
6279        // Load languages for the language dropdown
6280        try {
6281            await loadLanguageDropdown();
6282        } catch (error) {
6283            console.error('Error loading languages:', error);
6284            // Continue even if languages fail to load
6285        }
6286
6287        // Trigger project selection to load company data
6288        if (projectDropdown.value) {
6289            projectDropdown.dispatchEvent(new Event('change'));
6290        }
6291
6292        // Clear any existing content in the container
6293        container.innerHTML = '';
6294
6295        // Create form card
6296        const card = document.createElement('div');
6297        card.classList.add('card', 'mb-4');
6298        container.appendChild(card);
6299
6300        const cardBody = document.createElement('div');
6301        cardBody.classList.add('card-body');
6302        card.appendChild(cardBody);
6303
6304        // Document type selection section at TOP
6305        const docTypeSection = document.createElement('div');
6306        docTypeSection.classList.add('mb-4');
6307        cardBody.appendChild(docTypeSection);
6308
6309        const docTypeTitle = document.createElement('h5');
6310        docTypeTitle.id = 'document_type_title';
6311        docTypeTitle.classList.add('mb-3');
6312        docTypeTitle.textContent = translations['selectDocumentType'] || 'Select Document Type';
6313        docTypeSection.appendChild(docTypeTitle);
6314
6315        // Document types with descriptions and specific fields
6316        const documentTypes = [
6317            {
6318                id: 'privacyPolicy',
6319                label: translations['privacyPolicy'] || 'Privacy Policy',
6320                descriptionId: 'privacyPolicyDescription',
6321                description: translations['privacyPolicyDescription'] || 'A document that explains how you collect, use, and manage users\' personal data.',
6322                fields: ['lastUpdated', 'effectiveDate', 'name', 'address', 'country', 'email', 'phone', 'websiteUrl']
6323            },
6324            {
6325                id: 'termsOfUse',
6326                label: translations['termsOfUse'] || 'Terms of Use',
6327                descriptionId: 'termsOfUseDescription',
6328                description: translations['termsOfUseDescription'] || 'A document that outlines the rules and guidelines for using your website or service.',
6329                fields: ['lastUpdated', 'effectiveDate', 'name', 'serviceName', 'address', 'country', 'email', 'phone', 'companyRegNo', 'websiteUrl', 'privacyUrl', 'pricingUrl']
6330            },
6331            {
6332                id: 'dpa',
6333                label: translations['dpa'] || 'Data Processing Agreement (DPA)',
6334                descriptionId: 'dpaDescription',
6335                description: translations['dpaDescription'] || 'A c
6335ontract that regulates the handling of personal data.',
6336                fields: ['lastUpdated', 'effectiveDate', 'name', 'legalEntity', 'address', 'country', 'termsUrl']
6337            },
6338            {
6339                id: 'cookiePolicy',
6340                label: translations['cookiePolicy'] || 'Cookie Policy',
6341                descriptionId: 'cookiePolicyDescription',
6342                description: translations['cookiePolicyDescription'] || 'A document that explains how cookies are used on your website.',
6343                fields: ['lastUpdated', 'effectiveDate', 'name', 'address', 'country', 'email', 'websiteUrl']
6344            }
6345        ];
6346
6347        // Create buttons container - vertical layout
6348        const buttonContainer = document.createElement('div');
6349        buttonContainer.id = 'document_type_buttons';
6350        buttonContainer.classList.add('d-flex', 'flex-column', 'gap-2');
6351        docTypeSection.appendChild(buttonContainer);
6352
6353        // Create the Go Back button with red outline style
6354        const goBackBtn = document.createElement('button');
6355        goBackBtn.type = 'button';
6356        goBackBtn.id = 'cancel_selection';
6357        goBackBtn.classList.add('btn', 'btn-outline-danger', 'me-2', 'd-none');
6358        goBackBtn.textContent = translations['goBack'] || 'Go Back';
6359
6360        // Add custom styling for better hover effect
6361        const style = document.createElement('style');
6362        style.textContent = `
6363            #cancel_selection {
6364                color: #dc3545 !important; /* Red text color */
6365                background-color: transparent !important; /* No background */
6366                border: 1px solid #dc3545 !important; /* Red border, always visible */
6367                transition: all 0.15s ease-in-out !important; /* Smooth transition */
6368            }
6369
6370            #cancel_selection:hover {
6371                color: #dc3545 !important; /* Keep red text on hover */
6372                background-color: rgba(220, 53, 69, 0.1) !important; /* Light red background on hover */
6373                border: 1px solid #dc3545 !important; /* Red border on hover */
6374                transform: translateY(-1px) !important; /* Slight lift effect */
6375            }
6376
6377            #cancel_selection:active {
6378                color: #fff !important; /* White text when clicked */
6379                background-color: #dc3545 !important; /* Red background when clicked */
6380                border: 1px solid #dc3545 !important; /* Red border when clicked */
6381                transform: translateY(0) !important; /* Reset lift effect */
6382            }
6383
6384            #cancel_selection:focus {
6385                box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25) !important; /* Red focus outline */
6386            }
6387        `;
6388
6389        // Add the custom styles to the document head
6390        document.head.appendChild(style);
6391
6392        // Go Back button click handler (same as before)
6393        goBackBtn.addEventListener('click', () => {
6394            // Reset title to "Select Document Type"
6395            const docTypeTitle = document.getElementById('document_type_title');
6396            docTypeTitle.textContent = translations['selectDocumentType'] || 'Select Document Type';
6397
6398            // Show all document type buttons
6399            document.querySelectorAll('.doc-type-btn').forEach(btn => {
6400                btn.classList.remove('d-none');
6401                btn.classList.remove('btn-info', 'btn-primary', 'btn-secondary');
6402                btn.classList.add('btn-outline-primary');
6403                btn.setAttribute('aria-pressed', 'false');
6404
6405        // Completely reset all inline styles
6406                btn.removeAttribute('style');
6407
6408                // Reset all child elements
6409                const buttonContent = btn.querySelector('div');
6410                if (buttonContent) {
6411                    buttonContent.removeAttribute('style');
6412
6413                    // Reset label styling
6414                    const label = buttonContent.querySelector('.fw-bold');
6415                    if (label) {
6416                        label.removeAttribute('style');
6417                        label.classList.remove('text-white', 'text-dark', 'text-dark-75');
6418                    }
6419
6420                    // Reset description styling
6421                    const description = buttonContent.querySelector('small');
6422                    if (description) {
6423                        description.removeAttribute('style');
6424                        description.classList.remove('text-white', 'text-white-75', 'text-dark', 'text-dark-75');
6425                        // Ensure it has the correct muted class
6426                        if (!description.classList.contains('text-muted')) {
6427                            description.classList.add('text-muted');
6428                        }
6429                    }
6430                }
6431
6432
6433            });
6434
6435            // Hide form fields, format options, and generate button container
6436            formFieldsSection.classList.add('d-none');
6437            formatGroup.classList.add('d-none');
6438            generateBtnContainer.classList.add('d-none');
6439
6440            // Hide the document result section
6441            const resultSection = document.getElementById('document_result');
6442            if (resultSection) {
6443                resultSection.classList.add('d-none');
6444                resultSection.innerHTML = '';
6445            }
6446
6447            // Clear any displayed alerts
6448            const existingAlerts = document.querySelectorAll('.alert');
6449            existingAlerts.forEach(alert => alert.remove());
6450        });
6451
6452
6453        // Create buttons for document types
6454        documentTypes.forEach(docType => {
6455            const button = document.createElement('button');
6456            button.type = 'button';
6457            button.id = `btn_${docType.id}`;
6458            button.classList.add('btn', 'btn-outline-info', 'text-start', 'py-2', 'doc-type-btn');
6459            button.dataset.docType = docType.id;
6460            button.dataset.fields = JSON.stringify(docType.fields);
6461            buttonContainer.appendChild(button);
6462
6463            // Create content inside the button with flex layout
6464            const buttonContent = document.createElement('div');
6465            buttonContent.classList.add('d-flex', 'flex-column', 'align-items-start');
6466            button.appendChild(buttonContent);
6467
6468            const docTypeLabel = document.createElement('span');
6469            docTypeLabel.classList.add('fw-bold', 'mb-1');
6470            docTypeLabel.textContent = docType.label;
6471            buttonContent.appendChild(docTypeLabel);
6472
6473            const docTypeDesc = document.createElement('small');
6474            docTypeDesc.classList.add('text-muted');
6475            docTypeDesc.textContent = docType.description;
6476            buttonContent.appendChild(docTypeDesc);
6477
6478            // Add click handler for button
6479            button.addEventListener('click', () => {
6480                // Hide all unselected buttons
6481                document.querySelectorAll('.doc-type-btn').forEach(btn => {
6482                    if (btn.id !== button.id) {
6483                        btn.classList.add('d-none');
6484                    }
6485                });
6486
6487                // Mark this button as selected with proper styling
6488                button.classList.remove('btn-outline-primary');
6489                button.classList.add('btn-info');
6490                button.setAttribute('aria-pressed', 'true');
6491
6492                // Fix text color for selected button - make all text black
6493                    button.style.color = 'black';
6494                    button.style.backgroundColor = '#d1ecf1'; // Light blue background
6495                    button.style.borderColor = '#bee5eb'; // Light blue border
6496                    const buttonContent = button.querySelector('div');
6497                    if (buttonContent) {
6498                        buttonContent.style.color = 'black';
6499                        // Make sure all child elements are black
6500                        const label = buttonContent.querySelector('.fw-bold');
6501                        const description = buttonContent.querySelector('.text-muted, small');
6502                        if (label) {
6503                            label.style.color = 'black';
6504                            label.classList.remove('text-muted'); // Remove any muted classes
6505                        }
6506                        if (description) {
6507                            description.style.color = '#495057'; // Dark gray for description
6508                            description.classList.remove('text-muted'); // Remove muted class
6509                            description.classList.add('text-dark-75'); // Add dark gray class
6510                        }
6511                    }
6512
6513                // Change title
6514                const docTypeTitle = document.getElementById('document_type_title');
6515                const selectedLabel = docType.label;
6516                docTypeTitle.textContent = `${translations['documentInformationOf'] || 'Document Information of'} ${selectedLabel}`;
6517
6518                // Show cancel button
6519                goBackBtn.classList.remove('d-none');
6520
6521                // Update form with fields for this document type
6522                updateFormFields(docType.fields, docType.id);
6523
6524                // Show the form fields section
6525                formFieldsSection.classList.remove('d-none');
6526
6527                // Show format options and generate button
6528                formatGroup.classList.remove('d-none');
6529                generateBtnContainer.classList.remove('d-none');
6530
6531                // Load company information for the selected project
6532                const selectedProjectId = document.getElementById('select_domain').value;
6533                if (selectedProjectId) {
6534                    loadCompanyInformation(selectedProjectId).then(() => {
6535                        populateCompanyData(window.companyData);
6536                    }).catch(error => {
6537                        console.error('Error loading company information:', error);
6538                    });
6539                }
6540            });
6541        });
6542
6543        // Dynamic form fields section - initially hidden
6544        const formFieldsSection = document.createElement('div');
6545        formFieldsSection.id = 'dynamic_form_fields';
6546        formFieldsSection.classList.add('mb-4', 'd-none');
6547        cardBody.appendChild(formFieldsSection);
6548
6549        // Format options - initially hidden
6550        const formatGroup = document.createElement('div');
6551        formatGroup.classList.add('mb-3', 'mt-4', 'd-none');
6552        cardBody.appendChild(formatGroup);
6553
6554        const formatLabel = document.createElement('label');
6555        formatLabel.classList.add('form-label');
6556        formatLabel.htmlFor = 'document_format';
6557        formatLabel.textContent = translations['documentFormat'] || 'Document Format';
6558        formatGroup.appendChild(formatLabel);
6559
6560        const formatSelect = document.createElement('select');
6561        formatSelect.id = 'document_format';
6562        formatSelect.classList.add('form-select');
6563        formatGroup.appendChild(formatSelect);
6564
6565        const formatOptions = [
6566            { value: 'html', label: translations['formatHtml'] || 'HTML' },
6567            { value: 'pdf', label: translations['formatPdf'] || 'PDF' }
6568        ];
6569
6570        formatOptions.forEach(option => {
6571            const optionEl = document.createElement('option');
6572            optionEl.value = option.value;
6573            optionEl.textContent = option.label;
6574            formatSelect.appendChild(optionEl);
6575        });
6576
6577        // Add generate button container with BOTH buttons - initially hidden
6578        const generateBtnContainer = document.createElement('div');
6579        generateBtnContainer.classList.add('d-flex', 'justify-content-center', 'mt-4', 'd-none');
6580        cardBody.appendChild(generateBtnContainer);
6581
6582        // Add the Go Back button FIRST (on the left)
6583        generateBtnContainer.appendChild(goBackBtn);
6584
6585        // Then add the Generate Document button (on the right)
6586        const generateBtn = document.createElement('button');
6587        generateBtn.type = 'button';
6588        generateBtn.id = 'generate_document_btn';
6589        generateBtn.classList.add('btn', 'btn-primary', 'px-4', 'py-2');
6590        generateBtn.textContent = translations['generateDocument'] || 'Generate Document';
6591        generateBtnContainer.appendChild(generateBtn);
6592
6593        // Create result section
6594        const resultSection = document.createElement('div');
6595        resultSection.id = 'document_result';
6596        resultSection.classList.add('mt-4', 'd-none');
6597        container.appendChild(resultSection);
6598
6599        // Add event listener for generating documents
6600        generateBtn.addEventListener('click', handleGenerateDocument);
6601
6602        console.log('Document generator loaded successfully');
6603
6604    } catch (error) {
6605        console.error('Error loading document generator:', error);
6606        displayAlertDocument(translations['failedToLoadDocumentGenerator'] || 'Failed to load document generator: ' + error.message, 'error');
6607    }
6608}
6609
6610function isValidEmail(email) {
6611    const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
6612    return emailRegex.test(email);
6613}
6614
6615function isValidUrl(url) {
6616    try {
6617        const urlObj = new URL(url);
6618        return urlObj.protocol === 'http:' || urlObj.protocol === 'https:';
6619    } catch (e) {
6620        return false;
6621    }
6622}
6623
6624function isValidPhoneNumber(phone) {
6625    const phoneRegex = /^[+]?[(]?[0-9]{1,4}[)]?[-\s.]?[0-9]{1,3}[-\s.]?[0-9]{1,4}[-\s.]?[0-9]{1,4}$/;
6626    return phoneRegex.test(phone);
6627}
6628
6629function addInputValidation(field) {
6630    const input = document.getElementById(field);
6631    if (!input) return;
6632
6633    // Determine which validation to apply based on field ID or type
6634    if (field === 'email' || input.type === 'email') {
6635        // Email validation
6636        input.addEventListener('blur', function() {
6637            if (this.value && !isValidEmail(this.value)) {
6638                this.classList.add('is-invalid');
6639
6640                // Add validation message if doesn't exist
6641                if (!this.nextElementSibling || !this.nextElementSibling.classList.contains('invalid-feedback')) {
6642                    const feedback = document.createElement('div');
6643                    feedback.classList.add('invalid-feedback');
6644                    feedback.textContent = translations['invalidEmail'] || 'Please enter a valid email address';
6645                    this.parentNode.appendChild(feedback);
6646                }
6647            } else {
6648                this.classList.remove('is-invalid');
6649            }
6650        });
6651    } else if (field === 'websiteUrl' || field === 'privacyUrl' || field === 'termsUrl' ||
6652               field === 'pricingUrl' || input.type === 'url') {
6653        // URL validation - must start with http:// or https://
6654        input.addEventListener('blur', function() {
6655            if (this.value && !isValidUrl(this.value)) {
6656                this.classList.add('is-invalid');
6657
6658                // Add validation message if doesn't exist
6659                if (!this.nextElementSibling || !this.nextElementSibling.classList.contains('invalid-feedback')) {
6660                    const feedback = document.createElement('div');
6661                    feedback.classList.add('invalid-feedback');
6662                    feedback.textContent = translations['invalidUrl'] || 'Please enter a valid URL starting with http:// or https://';
6663                    this.parentNode.appendChild(feedback);
6664                }
6665            } else {
6666                this.classList.remove('is-invalid');
6667            }
6668        });
6669    } else if (field === 'phone' || input.type === 'tel') {
6670        // Phone validation
6671        input.addEventListener('blur', function() {
6672            if (this.value && !isValidPhoneNumber(this.value)) {
6673                this.classList.add('is-invalid');
6674
6675                // Add validation message if doesn't exist
6676                if (!this.nextElementSibling || !this.nextElementSibling.classList.contains('invalid-feedback')) {
6677                    const feedback = document.createElement('div');
6678                    feedback.classList.add('invalid-feedback');
6679                    feedback.textContent = translations['invalidPhone'] || 'Please enter a valid phone number';
6680                    this.parentNode.appendChild(feedback);
6681                }
6682            } else {
6683                this.classList.remove('is-invalid');
6684            }
6685        });
6686    }
6687}
6688
6689function updateFormFields(fields, docTypeId) {
6690    const formFieldsSection = document.getElementById('dynamic_form_fields');
6691    formFieldsSection.innerHTML = '';
6692
6693    // Create form fields row
6694    const row = document.createElement('div');
6695    row.classList.add('row');
6696    formFieldsSection.appendChild(row);
6697
6698    // Field definitions with their properties
6699    const fieldDefinitions = {
6700        lastUpdated: {
6701            id: 'lastUpdated',
6702            labelId: 'lastUpdated',
6703            label: 'Last Updated',
6704            type: 'date',
6705            placeholderId: 'lastUpdatedPlaceholder',
6706            placeholder: 'Select last updated date',
6707            required: true,
6708            colSize: 6,
6709            defaultValue: new Date().toISOString().split('T')[0] // Today's date
6710        },
6711        effectiveDate: {
6712            id: 'effectiveDate',
6713            labelId: 'effectiveDate',
6714            label: 'Effective Date',
6715            type: 'date',
6716            placeholderId: 'effectiveDatePlaceholder',
6717            placeholder: 'Select effective date',
6718            required: true,
6719            colSize: 6,
6720            defaultValue: new Date().toISOString().split('T')[0] // Today's date
6721        },
6722        name: {
6723            labelId: 'companyName',
6724            label: 'Company Name',
6725            type: 'text',
6726            placeholderId: 'companyNamePlaceholder',
6727            placeholder: 'Enter your company name',
6728            required: true,
6729            colSize: 12,
6730            companyField: 'companyName'
6731        },
6732        serviceName: {
6733            labelId: 'serviceName',
6734            label: 'Service Name',
6735            type: 'text',
6736            placeholderId: 'serviceNamePlaceholder',
6737            placeholder: 'Enter your service name',
6738            required: true,
6739            colSize: 12,
6740            companyField: 'serviceName'
6741        },
6742        address: {
6743            labelId: 'address',
6744            label: 'Address',
6745            type: 'text',
6746            placeholderId: 'companyAddressPlaceholder',
6747            placeholder: 'Enter your company address',
6748            required: true,
6749            colSize: 12,
6750            companyField: 'address'
6751        },
6752        country: {
6753            labelId: 'country',
6754            label: 'Country',
6755            type: 'text',
6756            placeholderId: 'companyCountryPlaceholder',
6757            placeholder: 'Enter your country',
6758            required: true,
6759            colSize: 6,
6760            companyField: 'country'
6761        },
6762        email: {
6763            labelId: 'email',
6764            label: 'Email',
6765            type: 'email',
6766            placeholderId: 'enterYourEmail',
6767            placeholder: 'Enter contact email',
6768            required: true,
6769            colSize: 6,
6770            companyField: 'email',
6771            validate: true
6772        },
6773        phone: {
6774            labelId: 'phone',
6775            label: 'Phone',
6776            type: 'tel',
6777            placeholderId: 'companyPhonePlaceholder',
6778            placeholder: 'Enter contact phone number',
6779            required: true,
6780            colSize: 6,
6781            companyField: 'phone',
6782            validate: true
6783        },
6784        websiteUrl: {
6785            labelId: 'websiteUrl',
6786            label: 'Website URL',
6787            type: 'url',
6788            placeholderId: 'companyWebsitePlaceholder',
6789            placeholder: 'https://example.com',
6790            required: true,
6791            colSize: 12,
6792            companyField: 'websiteUrl',
6793            validate: true
6794        },
6795        privacyUrl: {
6796            labelId: 'privacyUrl',
6797            label: 'Privacy URL',
6798            type: 'url',
6799            placeholderId: 'companyPrivacyPlaceholder',
6800            placeholder: 'https://example.com/privacy',
6801            required: true,
6802            colSize: 12,
6803            companyField: 'privacyUrl',
6804            validate: true
6805        },
6806        termsUrl: {
6807            labelId: 'termsUrl',
6808            label: 'Terms of Use URL',
6809            type: 'url',
6810            placeholderId: 'companyTermsPlaceholder',
6811            placeholder: 'https://example.com/terms',
6812            required: true,
6813            colSize: 12,
6814            companyField: 'termsUrl',
6815            validate: true
6816        },
6817        pricingUrl: {
6818            labelId: 'pricingUrl',
6819            label: 'Pricing URL',
6820            type: 'url',
6821            placeholderId: 'companyPricingPlaceholder',
6822            placeholder: 'https://example.com/pricing',
6823            required: true,
6824            colSize: 12,
6825            companyField: 'pricingUrl',
6826            validate: true
6827        },
6828        companyRegNo: {
6829            labelId: 'companyRegNo',
6830            label: 'Company Registration Number',
6831            type: 'text',
6832            placeholderId: 'companyRegNoPlaceholder',
6833            placeholder: 'Enter company registration number',
6834            required: true,
6835            colSize: 6,
6836            companyField: 'companyRegNo'
6837        },
6838        legalEntity: {
6839            labelId: 'legalEntity',
6840            label: 'Company Legal Entity',
6841            type: 'text',
6842            placeholderId: 'companyLegalEntityPlaceholder',
6843            placeholder: 'Enter company legal entity',
6844            required: true,
6845            colSize: 12,
6846            companyField: 'legalEntity'
6847        }
6848    };
6849
6850    // Access company data if available
6851    const companyData = window.companyData || {};
6852
6853    // Create form fields based on the selected document type
6854    fields.forEach(fieldName => {
6855        const field = fieldDefinitions[fieldName];
6856        if (!field) return;
6857
6858        const col = document.createElement('div');
6859        col.classList.add('col-md-' + field.colSize, 'mb-3');
6860        row.appendChild(col);
6861
6862        const label = document.createElement('label');
6863        label.classList.add('form-label');
6864        label.htmlFor = fieldName;
6865        label.textContent = translations[field.labelId] || field.label;
6866        if (field.required) {
6867            label.innerHTML += ' <span class="text-danger">*</span>';
6868        }
6869        col.appendChild(label);
6870
6871        const input = document.createElement('input');
6872        input.type = field.type;
6873        input.id = fieldName;
6874        input.name = fieldName;
6875        input.classList.add('form-control');
6876        if (field.placeholder) input.placeholder = translations[field.placeholderId] || field.placeholder;
6877        if (field.required) input.required = true;
6878
6879        // Pre-fill with data from company if available
6880        if (field.companyField && companyData[field.companyField]) {
6881            input.value = companyData[field.companyField];
6882        } else if (field.defaultValue) {
6883            input.value = field.defaultValue;
6884        }
6885
6886        col.appendChild(input);
6887
6888        // Add validation for specific field types
6889        if (field.validate || field.type === 'email' || field.type === 'url' || field.type === 'tel') {
6890            addInputValidation(fieldName);
6891        }
6892    });
6893}
6894
6895async function handleGenerateDocument() {
6896    // Get the selected document type and format
6897    const activeBtn = document.querySelector('.doc-type-btn[aria-pressed="true"]');
6898    if (!activeBtn) {
6899        showAlert(translations['selectDocTypeFirst'] || 'Please select a document type first.', "warning");
6900        return;
6901    }
6902
6903    // Get form values and validate
6904    const formFields = document.getElementById('dynamic_form_fields').querySelectorAll('input');
6905    const formData = {};
6906    let isValid = true;
6907    let invalidFields = [];
6908
6909    formFields.forEach(field => {
6910        // Check required fields
6911        if (field.required && !field.value) {
6912            field.classList.add('is-invalid');
6913            isValid = false;
6914            invalidFields.push(field.id);
6915            return;
6916        }
6917
6918        // Validate field types
6919        if (field.value) {
6920            if ((field.id === 'email' || field.type === 'email') && !isValidEmail(field.value)) {
6921                field.classList.add('is-invalid');
6922                isValid = false;
6923                invalidFields.push(field.id);
6924                return;
6925            }
6926
6927            if ((field.id === 'websiteUrl' || field.id === 'privacyUrl' ||
6928                 field.id === 'termsUrl' || field.id === 'pricingUrl' ||
6929                 field.type === 'url') && !isValidUrl(field.value)) {
6930                field.classList.add('is-invalid');
6931                isValid = false;
6932                invalidFields.push(field.id);
6933                return;
6934            }
6935
6936            if ((field.id === 'phone' || field.type === 'tel') && !isValidPhoneNumber(field.value)) {
6937                field.classList.add('is-invalid');
6938                isValid = false;
6939                invalidFields.push(field.id);
6940                return;
6941            }
6942        }
6943
6944        field.classList.remove('is-invalid');
6945        formData[field.id] = field.value;
6946    });
6947
6948    if (!isValid) {
6949        // Create detailed validation message
6950        let errorMessage = translations['fillRequiredFields'] || 'Please fix the following issues:';
6951
6952        if (invalidFields.includes('email')) {
6953            errorMessage += '\n- ' + (translations['invalidEmail'] || 'Please enter a valid email address');
6954        }
6955
6956        if (invalidFields.some(field => field.includes('Url'))) {
6957            errorMessage += '\n- ' + (translations['invalidUrl'] || 'URLs must start with http:// or https://');
6958        }
6959
6960        if (invalidFields.includes('phone')) {
6961            errorMessage += '\n- ' + (translations['invalidPhone'] || 'Please enter a valid phone number');
6962        }
6963
6964        if (invalidFields.some(field => !field.includes('Url') && field !== 'email' && field !== 'phone')) {
6965            errorMessage += '\n- ' + (translations['missingRequiredFields'] || 'Fill in all required fields marked with *');
6966        }
6967
6968        showAlert(errorMessage, "error");
6969        return;
6970    }
6971
6972    const docType = activeBtn.dataset.docType;
6973    const format = document.getElementById('document_format').value;
6974
6975    // Get the selected project ID
6976    const selectedProjectId = document.getElementById('select_domain').value;
6977
6978    // Get the selected language ID from the dropdown
6979    const languageDropdown = document.getElementById('select_language');
6980    const selectedLanguageId = languageDropdown ? parseInt(languageDropdown.value) : 2;
6981
6982    console.log('Generating document with language ID:', selectedLanguageId);
6983
6984    // Store the selected language ID globally for use in other functions
6985    window.currentLanguageId = selectedLanguageId;
6986
6987    // Store form data globally for use in save & download
6988    window.currentDocumentData = {
6989        docType: docType,
6990        format: format,
6991        projectId: selectedProjectId,
6992        languageId: selectedLanguageId,
6993        formData: formData
6994    };
6995
6996    const generateBtn = document.getElementById('generate_document_btn');
6997    const originalText = generateBtn.textContent;
6998    generateBtn.disabled = true;
6999    generateBtn.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> ' +
7000                           (translations['generating'] || 'Generating...');
7001
7002    try {
7003        // First save the company information (but not the document)
7004        const saveResult = await checkCompanyInformation(selectedProjectId);
7005
7006        if (!saveResult) {
7007            // If saving failed, stop the process
7008            generateBtn.disabled = false;
7009            generateBtn.textContent = originalText;
7010            return;
7011        }
7012
7013        // Fetch the document template from the server
7014        let documentContent;
7015        try {
7016            // Update button text to indicate template fetching
7017            generateBtn.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> ' +
7018                                   (translations['fetchingTemplate'] || 'Fetching template...');
7019
7020            console.log('Fetching template for document type:', docType, 'language:', selectedLanguageId);
7021
7022            // Get the document template using the selected language
7023            const templateData = await getDocumentTemplate(docType, selectedLanguageId);
7024
7025            console.log('Template received for language', selectedLanguageId, ':', templateData);
7026
7027            if (!templateData || !templateData.templateText) {
7028                throw new Error('No template found for the selected document type and language');
7029            }
7030
7031            generateBtn.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> ' +
7032                                   (translations['generating'] || 'Generating...');
7033
7034            // Process the template by replacing placeholders with form data
7035            documentContent = processTemplate(templateData.templateText, formData);
7036
7037            // §10: a cookie policy has to say WHICH cookies. Fetch the composed section
7038            // from the domain's actual scan and fold it in. Failure is non-fatal — the
7039            // merchant still gets the templated document rather than nothing.
7040            if (docType === 'cookiePolicy') {
7041                try {
7042                    const policy = await fcFetchCookiePolicy(selectedProjectId, fcSelectedLanguageCode());
7043                    documentContent = fcApplyCookiePolicySection(documentContent, policy);
7044                } catch (policyErr) {
7045                    console.error('Could not add the tracker section:', policyErr);
7046                }
7047            }
7048
7049            console.log('Document content generated for language:', selectedLanguageId);
7050
7051            // Store the generated content globally for save & download
7052            window.currentDocumentData.generatedContent = documentContent;
7053
7054        } catch (error) {
7055            console.error('Error processing template:', error);
7056            displayAlertDocument('Failed to process document template: ' + error.message, 'error');
7057            generateBtn.disabled = false;
7058            generateBtn.textContent = originalText;
7059            return;
7060        }
7061
7062        // Display the generated document (WITHOUT saving to database)
7063        // Pass null as documentId since we haven't saved it yet
7064        displayGeneratedDocument(documentContent, docType, format, null, selectedLanguageId);
7065
7066        // Reset button
7067        generateBtn.disabled = false;
7068        generateBtn.textContent = originalText;
7069
7070        // Show success message
7071        displayAlertDocument(translations['documentGeneratedSuccessfully'] || 'Document generated successfully. Click "Save & Download" to save and download it.', 'success');
7072
7073    } catch (error) {
7074        // Handle unexpected errors
7075        displayAlertDocument('Error generating document: ' + error.message, 'error');
7076        console.error("Error generating document:", error);
7077
7078        // Reset button
7079        generateBtn.disabled = false;
7080        generateBtn.textContent = originalText;
7081    }
7082}
7083
7084async function loadCompanyInformation(projectDomainId) {
7085    const data = { sid: getSid(), projectDomainId: projectDomainId };
7086    const response = await postRequest(endpoint + '/document/getCompany', data);
7087    if (response.ok) {
7088        const responseData = await response.json();
7089        if (responseData.statusCode === 0 && responseData.company) {
7090            window.companyData = responseData.company;
7091        } else {
7092            window.companyData = {
7093            name: '',
7094            address: '',
7095            country: '',
7096            email: '',
7097            phone: '',
7098            websiteUrl: '',
7099            serviceName: '',
7100            pricingUrl: '',
7101            privacyUrl: '',
7102            termsUrl: '',
7103            companyRegNo: '',
7104            legalEntity: ''
7105            }
7106        }
7107    }
7108
7109}
7110
7111function populateCompanyData(company) {
7112    $('#name').val(company.name || '');
7113    $('#address').val(company.address || '');
7114    $('#country').val(company.country || '');
7115    $('#email').val(company.email || '');
7116    $('#phone').val(company.phone || '');
7117    $('#websiteUrl').val(company.websiteUrl || '');
7118    $('#serviceName').val(company.serviceName || '');
7119    $('#pricingUrl').val(company.pricingUrl || '');
7120    $('#privacyUrl').val(company.privacyUrl || '');
7121    $('#termsUrl').val(company.termsUrl || '');
7122    $('#companyRegNo').val(company.companyRegNo || '');
7123    $('#legalEntity').val(company.legalEntity || '');
7124}
7125
7126async function saveCompanyInformation(projectDomainId, companyData) {
7127    // Prepare request data
7128    const data = {
7129        sid: getSid(),
7130        projectDomainId: projectDomainId,
7131        company: companyData
7132    };
7133
7134    console.log("Saving company data:", data);  // Debug log
7135
7136    // Show save indicator/spinner if needed
7137    const saveButton = $('#save_company_btn');
7138    const originalText = saveButton.text();
7139    if (saveButton.length) {
7140        saveButton.prop('disabled', true);
7141        saveButton.html('<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> Saving...');
7142    }
7143
7144    try {
7145        // Send request to save company data - using correct endpoint path
7146        const response = await postRequest(endpoint + '/document/saveCompany', data);
7147
7148        console.log("Save company response status:", response.status);  // Debug log
7149
7150        if (response.ok) {
7151            const responseData = await response.json();
7152            console.log("Save company response data:", responseData);  // Debug log
7153
7154            if (responseData.statusCode === 0) {
7155                // Show success message
7156                displayAlertDocument('Company information saved successfully', 'success');
7157
7158                // Update form with returned data if needed
7159                if (responseData.company) {
7160                    populateCompanyData(responseData.company);
7161                }
7162
7163                return true;
7164            } else {
7165                // Handle error
7166                displayAlertDocument('Error saving company information: ' + (responseData.statusMessage || 'Unknown error'), 'error');
7167                console.error("Error saving company information:", responseData.statusMessage);
7168                return false;
7169            }
7170        } else {
7171            // Handle network error
7172            displayAlertDocument('Network error while saving company information: ' + response.status, 'error');
7173            console.error("Network error saving company information:", response.status);
7174            return false;
7175        }
7176    } catch (error) {
7177        // Handle unexpected errors
7178        displayAlertDocument('Unexpected error while saving company information: ' + error.message, 'error');
7179        console.error("Exception while saving company information:", error);
7180        return false;
7181    } finally {
7182        // Reset button state if button exists
7183        if (saveButton.length) {
7184            saveButton.prop('disabled', false);
7185            saveButton.text(originalText);
7186        }
7187    }
7188}
7189
7190async function checkCompanyInformation(projectDomainId) {
7191    initialCompanyData = window.companyData;
7192
7193// Collect all company data from form fields
7194    const companyData = {
7195        name: $('#name').val() || initialCompanyData.name,
7196        address: $('#address').val() || initialCompanyData.address,
7197        country: $('#country').val() || initialCompanyData.country,
7198        email: $('#email').val() || initialCompanyData.email,
7199        phone: $('#phone').val() || initialCompanyData.phone,
7200        websiteUrl: $('#websiteUrl').val() || initialCompanyData.websiteUrl,
7201        serviceName: $('#serviceName').val() || initialCompanyData.serviceName,
7202        pricingUrl: $('#pricingUrl').val() || initialCompanyData.pricingUrl,
7203        privacyUrl: $('#privacyUrl').val() || initialCompanyData.privacyUrl,
7204        termsUrl: $('#termsUrl').val() || initialCompanyData.termsUrl,
7205        companyRegNo: $('#companyRegNo').val() || initialCompanyData.companyRegNo,
7206        legalEntity: $('#legalEntity').val() || initialCompanyData.legalEntity
7207    };
7208
7209
7210const saveResult = await saveCompanyInformation(projectDomainId, companyData);
7211
7212return saveResult;
7213}
7214
7215function processTemplate(template, formData) {
7216    // Replace all placeholders in the template with actual values
7217    let processedTemplate = template;
7218
7219    // Map form fields to template placeholders
7220    const placeholderMap = {
7221        'name': '{{COMPANY_NAME}}',
7222        'address': '{{COMPANY_ADDRESS}}',
7223        'country': '{{COUNTRY}}',
7224        'email': '{{EMAIL}}',
7225        'phone': '{{PHONE_NUMBER}}',
7226        'websiteUrl': '{{WEBSITE_URL}}',
7227        'lastUpdated': '{{LAST_UPDATED}}',
7228        'effectiveDate': '{{EFFECTIVE_DATE}}',
7229        'serviceName': '{{SERVICE_NAME}}',
7230        'companyRegNo': '{{COMPANY_REG_NO}}',
7231        'legalEntity': '{{LEGAL_ENTITY}}',
7232        'privacyUrl': '{{PRIVACY_URL}}',
7233        'termsUrl': '{{TERMS_URL}}',
7234        'pricingUrl': '{{PRICING_URL}}'
7235    };
7236
7237    // Replace each placeholder with the actual value
7238    for (const [field, placeholder] of Object.entries(placeholderMap)) {
7239        let value = formData[field] || '';
7240
7241        // Format dates nicely
7242        if (field === 'lastUpdated' || field === 'effectiveDate') {
7243            value = formatDateForDisplay(value);
7244        }
7245
7246        processedTemplate = processedTemplate.replace(new RegExp(placeholder, 'g'), value);
7247    }
7248
7249    // For HTML format, wrap with proper HTML structure
7250    // Replace newlines with <br> tags
7251    processedTemplate = processedTemplate.replace(/\n/g, '<br>');
7252
7253    // Wrap the content in a document container
7254    return `<div class="document-container">${processedTemplate}</div>`;
7255}
7256
7257function downloadDocumentClientSide(format, documentId = null, languageId = null) {
7258    // Get the selected language ID
7259    const selectedLanguageId = languageId ||
7260                               window.currentLanguageId ||
7261                               (document.getElementById('select_language') ? document.getElementById('select_language').value : null) ||
7262                               2; // Default to 2 if not found
7263
7264    console.log('Download starting with language ID:', selectedLanguageId);
7265
7266    // For the Save & Download flow, we don't show additional loading indicators
7267    // since the Save & Download button already shows loading state
7268
7269    try {
7270        // Get the document content that's already displayed
7271        const contentContainer = document.querySelector('.document-preview');
7272        if (!contentContainer) {
7273            throw new Error('Document content not found');
7274        }
7275
7276        // Get the document title
7277        const title = document.querySelector('#document_result h4')?.textContent || 'document';
7278        const sanitizedTitle = title.replace(/\s+/g, '_').toLowerCase();
7279
7280        // Get the current date for the filename
7281        const currentDate = new Date().toISOString().slice(0, 10).replace(/-/g, '');
7282
7283        if (format === 'html') {
7284            // For HTML, ensure we're returning the proper HTML document with color overrides
7285            let htmlContent = contentContainer.innerHTML;
7286            const isCompleteHtml = htmlContent.trim().toLowerCase().includes('<!doctype html>') ||
7287                                   htmlContent.trim().toLowerCase().includes('<html');
7288
7289            let content;
7290            if (isCompleteHtml) {
7291                // If it's already complete HTML, parse it to add our CSS overrides
7292                const parser = new DOMParser();
7293                const htmlDoc = parser.parseFromString(htmlContent, 'text/html');
7294
7295                // Add style to override red accent color
7296                const styleOverride = document.createElement('style');
7297                styleOverride.textContent = `
7298                    :root {
7299                        --accent-color: #555 !important; /* Override red with gray */
7300                    }
7301                    .variable {
7302                        color: #555 !important; /* Override any red variable text */
7303                    }
7304                    .highlight {
7305                        border-left-color: #3498db !important; /* Use secondary color instead of accent */
7306                    }
7307                    .important-notice {
7308                        border-left-color: #3498db !important; /* Use secondary color instead of accent */
7309                    }
7310                `;
7311
7312                // Add the style override to the head
7313                htmlDoc.head.appendChild(styleOverride);
7314
7315                // Add language metadata to the document
7316                const languageMeta = document.createElement('meta');
7317                languageMeta.setAttribute('name', 'document-language-id');
7318                languageMeta.setAttribute('content', selectedLanguageId.toString());
7319                htmlDoc.head.appendChild(languageMeta);
7320
7321                // Add document ID metadata if available
7322                if (documentId) {
7323                    const docIdMeta = document.createElement('meta');
7324                    docIdMeta.setAttribute('name', 'document-id');
7325                    docIdMeta.setAttribute('content', documentId.toString());
7326                    htmlDoc.head.appendChild(docIdMeta);
7327                }
7328
7329                content = '<!DOCTYPE html>\n' + htmlDoc.documentElement.outerHTML;
7330            } else {
7331                // If it's not complete HTML, wrap it properly and add our overrides
7332                content = `<!DOCTYPE html>
7333<html>
7334<head>
7335    <meta charset="UTF-8">
7336    <meta name="document-language-id" content="${selectedLanguageId}">
7337    ${documentId ? `<meta name="document-id" content="${documentId}">` : ''}
7338    <title>${title}</title>
7339    <style>
7340        body { font-family: Arial, sans-serif; margin: 2em; }
7341        .document-container { margin: 0 auto; line-height: 1.5; }
7342        .variable { color: #555 !important; } /* Remove red color for variables */
7343        [style*="color: var(--accent-color)"] { color: #555 !important; }
7344    </style>
7345</head>
7346<body>
7347    <div class="document-container">
7348        ${htmlContent}
7349    </div>
7350</body>
7351</html>`;
7352            }
7353
7354            // Create a Blob with the HTML content
7355            const blob = new Blob([content], { type: 'text/html' });
7356            // Include language ID and document ID in filename if available
7357            const fileName = documentId ?
7358                `${sanitizedTitle}_id${documentId}_lang${selectedLanguageId}_${currentDate}.html` :
7359                `${sanitizedTitle}_lang${selectedLanguageId}_${currentDate}.html`;
7360
7361            // Create download link
7362            const url = window.URL.createObjectURL(blob);
7363            const a = document.createElement('a');
7364            a.style.display = 'none';
7365            a.href = url;
7366            a.download = fileName;
7367            document.body.appendChild(a);
7368            a.click();
7369
7370            // Clean up
7371            window.URL.revokeObjectURL(url);
7372            document.body.removeChild(a);
7373
7374            console.log('HTML download completed');
7375
7376        } else if (format === 'pdf') {
7377            // For PDF, use the modified function that removes red colors and includes language
7378            generatePdfFromIframe(contentContainer, sanitizedTitle, currentDate, null, null, null, selectedLanguageId, documentId);
7379        } else {
7380            throw new Error('Unsupported format: ' + format);
7381        }
7382    } catch (error) {
7383        console.error("Download error:", error);
7384        displayAlertDocument('Download failed: ' + error.message, 'error');
7385    }
7386}
7387
7388function formatDateForDisplay(dateString) {
7389    if (!dateString) return '';
7390
7391    const date = new Date(dateString);
7392    const options = { year: 'numeric', month: 'long', day: 'numeric' };
7393    return date.toLocaleDateString(undefined, options);
7394}
7395
7396function displayAlertDocument(message, type) {
7397    // Toast, not an inline alert: the old version also removed every .alert on the
7398    // page, taking the static hint boxes with it.
7399    showToast(message, type === 'error' ? 'error' : type === 'success' ? 'success' : 'info');
7400}
7401
7402function downloadDocument(documentId, format) {
7403    // Show loading indicator
7404    const container = document.getElementById('document_result');
7405    const loadingIndicator = document.createElement('div');
7406    loadingIndicator.classList.add('text-center', 'my-3', 'download-loading');
7407    loadingIndicator.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> ' +
7408                               (translations['downloading'] || 'Downloading...');
7409    container.appendChild(loadingIndicator);
7410
7411    // Get the current session ID
7412    const currentSid = getSid();
7413    console.log("Using session ID:", currentSid); // Log for debugging
7414
7415    // Construct the URL for downloading
7416    const downloadUrl = `${endpoint}/document/download?sid=${currentSid}&documentId=${documentId}&format=${format}`;
7417
7418    // Use fetch API with credentials included
7419    fetch(downloadUrl, {
7420        method: 'GET',
7421        credentials: 'include', // Important: include cookies for session-based auth
7422        headers: {
7423            // Add any additional auth headers that your system might require
7424            'X-Auth-Token': currentSid
7425        }
7426    })
7427    .then(response => {
7428        // Remove loading indicator
7429        const indicator = document.querySelector('.download-loading');
7430        if (indicator) {
7431            container.removeChild(indicator);
7432        }
7433
7434        if (!response.ok) {
7435            return response.json().then(errorData => {
7436                throw new Error(`Download failed: ${errorData.statusMessage || response.statusText} (Code: ${errorData.statusCode || response.status})`);
7437            });
7438        }
7439
7440        // Check if it's a JSON response (might be an error)
7441        const contentType = response.headers.get('content-type');
7442        if (contentType && contentType.includes('application/json')) {
7443            return response.json().then(data => {
7444                if (data.statusCode !== 0) {
7445                    throw new Error(data.statusMessage || 'Download failed');
7446                }
7447                return data; // Might be JSON file data
7448            });
7449        }
7450
7451        // Process as regular file download
7452        return response.blob().then(blob => {
7453            // If blob is empty or too small, it might be an error
7454            if (blob.size < 10) {
7455                throw new Error('Downloaded file appears to be empty or invalid');
7456            }
7457
7458            // Create a proper filename
7459            const currentDate = new Date().toISOString().slice(0, 10);
7460            let docTypeName = document.querySelector('#document_result h4')?.textContent || 'document';
7461            docTypeName = docTypeName.replace(/\s+/g, '_').toLowerCase();
7462            const fileName = `${docTypeName}_${currentDate}.${format}`;
7463
7464            // Create and trigger download
7465            const url = window.URL.createObjectURL(blob);
7466            const a = document.createElement('a');
7467            a.style.display = 'none';
7468            a.href = url;
7469            a.download = fileName;
7470            document.body.appendChild(a);
7471            a.click();
7472
7473            // Clean up
7474            window.URL.revokeObjectURL(url);
7475            document.body.removeChild(a);
7476            displayAlertDocument(translations['downloadCompleted'] || 'Document downloaded successfully.', 'success');
7477        });
7478    })
7479    .catch(error => {
7480        // Remove loading indicator if still present
7481        const indicator = document.querySelector('.download-loading');
7482        if (indicator) {
7483            container.removeChild(indicator);
7484        }
7485        console.error("Download error:", error);
7486        // Auth error specific handling
7487        if (error.message.includes('Auth') || error.message.includes('authentication') || error.message.includes('Code: 3')) {
7488                    displayAlertDocument('Authentication failed. Please refresh the page and try again.', 'error');
7489                    refreshSession().then(() => {
7490                        displayAlertDocument('Session refreshed. Please try downloading again.', 'info');
7491                    }).catch(refreshError => {
7492                        console.error("Session refresh failed:", refreshError);
7493                    });
7494                } else {
7495            displayAlertDocument('Download failed: ' + error.message, 'error');
7496        }
7497    });
7498}
7499
7500async function refreshSession() {
7501    try {
7502        const response = await fetch(`${endpoint}/auth/refresh`, {
7503            method: 'POST',
7504            credentials: 'include',
7505            headers: {
7506                'Content-Type': 'application/json'
7507            },
7508            body: JSON.stringify({ sid: getSid() })
7509        });
7510        if (!response.ok) {
7511            throw new Error('Failed to refresh session');
7512        }
7513        const data = await response.json();
7514        if (data.statusCode !== 0) {
7515            throw new Error(data.statusMessage || 'Session refresh failed');
7516        }
7517        // If your system stores the session ID somewhere
7518        if (data.sid) {
7519            // Store the new session ID however your application does that
7520            localStorage.setItem('sid', data.sid);
7521        }
7522        return true;
7523    } catch (error) {
7524        console.error("Session refresh error:", error);
7525        throw error;
7526    }
7527}
7528
7529function displayGeneratedDocument(documentContent, docType, format, documentId, languageId) {
7530    // Get the result section
7531    const resultSection = document.getElementById('document_result');
7532
7533    // Clear any existing content
7534    resultSection.innerHTML = '';
7535
7536    // Make the result section visible
7537    resultSection.classList.remove('d-none');
7538
7539    // Store language ID as a data attribute for use in download
7540    resultSection.dataset.languageId = languageId || window.currentLanguageId || 2;
7541
7542    // Create the result header
7543    const headerRow = document.createElement('div');
7544    headerRow.classList.add('d-flex', 'justify-content-between', 'align-items-start', 'mb-3');
7545    resultSection.appendChild(headerRow);
7546
7547    // Add title (left side)
7548    const titleContainer = document.createElement('div');
7549    titleContainer.classList.add('flex-grow-1');
7550    headerRow.appendChild(titleContainer);
7551
7552    const title = document.createElement('h4');
7553    title.textContent = translations[docType] || getDocumentTypeName(docType);
7554    titleContainer.appendChild(title);
7555
7556    // Add button and disclaimer container (right side)
7557    const buttonContainer = document.createElement('div');
7558    buttonContainer.classList.add('d-flex', 'flex-column', 'align-items-end');
7559    headerRow.appendChild(buttonContainer);
7560
7561    // Add Save & Download button
7562    const saveDownloadBtn = document.createElement('button');
7563    saveDownloadBtn.type = 'button';
7564    saveDownloadBtn.classList.add('btn', 'btn-success', 'mb-2'); // Added mb-2 for spacing
7565    saveDownloadBtn.innerHTML = '<i class="fa fa-save me-2"></i>' + (translations['saveAndDownload'] || 'Save & Download');
7566
7567    // Store document info as data attributes for save & download
7568    saveDownloadBtn.dataset.docType = docType;
7569    saveDownloadBtn.dataset.format = format;
7570    saveDownloadBtn.dataset.languageId = languageId || window.currentLanguageId || 2;
7571
7572    // Add click handler for Save & Download
7573    saveDownloadBtn.addEventListener('click', async () => {
7574        console.log('Save & Download initiated');
7575
7576        // Show loading state
7577        const originalText = saveDownloadBtn.innerHTML;
7578        saveDownloadBtn.disabled = true;
7579        saveDownloadBtn.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> ' +
7580                                   (translations['savingAndDownloading'] || 'Saving & Downloading...');
7581
7582        try {
7583            if (!window.currentDocumentData) {
7584                throw new Error('No document data available. Please regenerate the document.');
7585            }
7586
7587            // First, save the document to the database
7588            const documentSaveData = {
7589                sid: getSid(),
7590                projectDomainId: window.currentDocumentData.projectId,
7591                documentTypeCode: window.currentDocumentData.docType,
7592                lastUpdated: window.currentDocumentData.formData.lastUpdated,
7593                effectiveDate: window.currentDocumentData.formData.effectiveDate,
7594                generatedText: window.currentDocumentData.generatedContent,
7595                languageId: window.currentDocumentData.languageId
7596            };
7597
7598            console.log('Saving document to database with language ID:', window.currentDocumentData.languageId);
7599
7600            const response = await postRequest(endpoint + '/document/saveDocument', documentSaveData);
7601
7602            if (!response.ok) {
7603                throw new Error('Failed to save document: ' + response.status);
7604            }
7605
7606            const responseData = await response.json();
7607
7608            if (responseData.statusCode !== 0) {
7609                throw new Error('Error saving document: ' + (responseData.statusMessage || 'Unknown error'));
7610            }
7611
7612            console.log('Document saved successfully with ID:', responseData.document.id);
7613
7614            // Update button text to indicate download is starting
7615            saveDownloadBtn.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> ' +
7616                                       (translations['downloading'] || 'Downloading...');
7617
7618            // Now proceed with the download
7619            const selectedLanguageId = window.currentDocumentData.languageId;
7620            downloadDocumentClientSide(format, responseData.document.id, selectedLanguageId);
7621
7622            // Show success message
7623            displayAlertDocument(translations['documentSavedAndDownloaded'] || 'Document saved and download started successfully.', 'success');
7624
7625        } catch (error) {
7626            console.error('Save & Download error:', error);
7627            displayAlertDocument('Save & Download failed: ' + error.message, 'error');
7628        } finally {
7629            // Reset button state
7630            saveDownloadBtn.disabled = false;
7631            saveDownloadBtn.innerHTML = originalText;
7632        }
7633    });
7634
7635    buttonContainer.appendChild(saveDownloadBtn);
7636
7637    // Add legal disclaimer text
7638    const disclaimerText = document.createElement('small');
7639    disclaimerText.classList.add('text-muted', 'text-end', 'fst-italic');
7640    disclaimerText.style.maxWidth = '450px'; // Increased width to accommodate 2 lines
7641    disclaimerText.style.lineHeight = '1.3';
7642    disclaimerText.style.whiteSpace = 'nowrap'; // Prevent additional wrapping
7643
7644    // Get translation or use default text with icon
7645    const disclaimerTextContent = translations['legalDisclaimerText'] ||
7646                                 'We strongly recommend having the final version reviewed<br>by a qualified legal professional to ensure compliance.';
7647
7648    disclaimerText.innerHTML = '<span style="white-space: normal; display: inline-block;">' +
7649                              '<i class="fa fa-exclamation-triangle me-1" style="color: #ffc107;"></i>' +
7650                              disclaimerTextContent +
7651                              '</span>';
7652    buttonContainer.appendChild(disclaimerText);
7653
7654    // Create container for the document content
7655    const contentContainer = document.createElement('div');
7656    contentContainer.classList.add('document-preview', 'p-3', 'border', 'rounded');
7657    contentContainer.dataset.languageId = languageId || window.currentLanguageId || 2;
7658    resultSection.appendChild(contentContainer);
7659
7660    // Add the document content
7661    contentContainer.innerHTML = documentContent;
7662
7663    // Scroll to the result section
7664    resultSection.scrollIntoView({ behavior: 'smooth' });
7665}
7666
7667function generatePdfFromIframe(contentElement, title, date, container, loadingIndicator, documentType, languageId, documentId) {
7668    // Get the language ID if not provided
7669    const selectedLanguageId = languageId ||
7670                               window.currentLanguageId ||
7671                               (document.getElementById('select_language') ? document.getElementById('select_language').value : null) ||
7672                               2; // Default fallback
7673
7674    console.log('Generating PDF with language ID:', selectedLanguageId);
7675
7676    // Create a loading indicator if none provided
7677    let createdLoadingIndicator = false;
7678    if (!loadingIndicator) {
7679        // Create a temporary loading indicator
7680        loadingIndicator = document.createElement('div');
7681        loadingIndicator.classList.add('text-center', 'my-3', 'pdf-loading');
7682        loadingIndicator.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> ' +
7683                                   (translations['preparingPdf'] || 'Preparing PDF...');
7684
7685        // Find a good place to insert it
7686        const targetContainer = container ||
7687                               document.getElementById('document_result') ||
7688                               document.getElementById('document_generator_container') ||
7689                               document.body;
7690
7691        targetContainer.appendChild(loadingIndicator);
7692        createdLoadingIndicator = true;
7693    }
7694
7695    // Check if we need to load jsPDF library
7696    loadJsPdfLibraries(() => {
7697        try {
7698            // Update loading message safely
7699            if (loadingIndicator && loadingIndicator.innerHTML !== undefined) {
7700                loadingIndicator.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> ' +
7701                                          'Preparing document for PDF conversion...';
7702            }
7703
7704            let htmlContent = contentElement.innerHTML;
7705
7706            // Check if it's a complete HTML document
7707            const isCompleteHtml = htmlContent.trim().toLowerCase().includes('<!doctype html>') ||
7708                                   htmlContent.trim().toLowerCase().includes('<html');
7709
7710            // Create an iframe to properly render the HTML with all CSS
7711            const iframe = document.createElement('iframe');
7712            iframe.style.width = '1000px';  // Set to a suitable width
7713            iframe.style.height = '100px';
7713  // Will be adjusted based on content
7714            iframe.style.position = 'absolute';
7715            iframe.style.left = '-9999px';  // Hide the iframe
7716            document.body.appendChild(iframe);
7717
7718            // Set iframe content
7719            const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
7720
7721            if (isCompleteHtml) {
7722                // If it's a complete HTML document, parse it to add our CSS overrides
7723                const parser = new DOMParser();
7724                const htmlDoc = parser.parseFromString(htmlContent, 'text/html');
7725
7726                // Add style to override red accent color
7727                const styleOverride = document.createElement('style');
7728                styleOverride.textContent = `
7729                    :root {
7730                        --accent-color: #555 !important; /* Override red with gray */
7731                    }
7732                    .variable {
7733                        color: #555 !important; /* Override any red variable text */
7734                    }
7735                    .highlight {
7736                        border-left-color: #3498db !important; /* Use secondary color instead of accent */
7737                    }
7738                    .important-notice {
7739                        border-left-color: #3498db !important; /* Use secondary color instead of accent */
7740                    }
7741                `;
7742
7743                // Add the style override to the head
7744                htmlDoc.head.appendChild(styleOverride);
7745
7746                // Add language metadata
7747                const languageMeta = document.createElement('meta');
7748                languageMeta.setAttribute('name', 'document-language-id');
7749                languageMeta.setAttribute('content', selectedLanguageId.toString());
7750                htmlDoc.head.appendChild(languageMeta);
7751
7752                // Add document ID metadata if provided
7753                if (documentId) {
7754                    const docIdMeta = document.createElement('meta');
7755                    docIdMeta.setAttribute('name', 'document-id');
7756                    docIdMeta.setAttribute('content', documentId.toString());
7757                    htmlDoc.head.appendChild(docIdMeta);
7758                }
7759
7760                // Write the modified HTML to the iframe
7761                iframeDoc.open();
7762                iframeDoc.write(htmlDoc.documentElement.outerHTML);
7763                iframeDoc.close();
7764            } else {
7765                // If it's a fragment, wrap it in a basic HTML structure with our overrides
7766                iframeDoc.open();
7767                iframeDoc.write(`
7768                <!DOCTYPE html>
7769                <html>
7770                <head>
7771                    <meta charset="UTF-8">
7772                    <meta name="document-language-id" content="${selectedLanguageId}">
7773                    ${documentId ? `<meta name="document-id" content="${documentId}">` : ''}
7774                    <title>${title}</title>
7775                    <style>
7776                        body { font-family: Arial, sans-serif; margin: 20px; }
7777                        .document-container { width: 800px; margin: 0 auto; line-height: 1.5; }
7778                        .variable { color: #555 !important; } /* Remove red color for variables */
7779                        [style*="color: var(--accent-color)"] { color: #555 !important; }
7780                    </style>
7781                </head>
7782                <body>
7783                    <div class="document-container">${htmlContent}</div>
7784                </body>
7785                </html>
7786                `);
7787                iframeDoc.close();
7788            }
7789
7790            // Wait for iframe content to load completely
7791            setTimeout(() => {
7792                try {
7793                    // Update loading message safely
7794                    if (loadingIndicator && loadingIndicator.innerHTML !== undefined) {
7795                        loadingIndicator.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> ' +
7796                                                 'Converting to PDF format...';
7797                    }
7798
7799                    // Apply additional color overrides directly to elements
7800                    try {
7801                        const iframeWindow = iframe.contentWindow || iframe;
7802                        const redElements = iframeDoc.querySelectorAll('.variable, [style*="color: var(--accent-color)"], [style*="color:#e74
7802c3c"], [style*="color: #e74c3c"]');
7803                        redElements.forEach(el => {
7804                            el.style.color = '#555';
7805                        });
7806
7807                        // Override border colors
7808                        const borderElements = iframeDoc.querySelectorAll('.highlight, .important-notice');
7809                        borderElements.forEach(el => {
7810                            el.style.borderLeftColor = '#3498db';
7811                        });
7812                    } catch (e) {
7813                        console.warn('Could not apply all color overrides:', e);
7814                    }
7815
7816                    // Adjust iframe height to match content
7817                    iframe.style.height = iframeDoc.body.scrollHeight + 50 + 'px';
7818
7819                    // Use html2canvas on the body of the iframe
7820                    html2canvas(iframeDoc.body, {
7821                        scale: 2,
7822                        useCORS: true,
7823                        allowTaint: true,
7824                        scrollY: 0,
7825                        windowWidth: 1000,
7826                        windowHeight: iframeDoc.body.scrollHeight + 50
7827                    }).then(canvas => {
7828                        try {
7829                            // Update loading message safely
7830                            if (loadingIndicator && loadingIndicator.innerHTML !== undefined) {
7831                                loadingIndicator.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> ' +
7832                                                         'Generating PDF file...';
7833                            }
7834
7835                            // Create PDF document with A4 dimensions
7836                            const pdf = new jspdf.jsPDF({
7837                                orientation: 'portrait',
7838                                unit: 'mm',
7839                                format: 'a4'
7840                            });
7841
7842                            // Calculate dimensions
7843                            const imgWidth = 210; // A4 width in mm
7844                            const pageHeight = 295; // A4 height in mm
7845                            const imgHeight = (canvas.height * imgWidth) / canvas.width;
7846
7847                            // Add image to PDF
7848                            const imgData = canvas.toDataURL('image/jpeg', 1.0);
7849
7850                            // Handle multi-page documents
7851                            let heightLeft = imgHeight;
7852                            let position = 0;
7853                            let pageNumber = 1;
7854
7855                            // Add first page
7856                            pdf.addImage(imgData, 'JPEG', 0, position, imgWidth, imgHeight);
7857                            heightLeft -= pageHeight;
7858
7859                            // Add additional pages if content is taller than one page
7860                            while (heightLeft > 0) {
7861                                pageNumber++;
7862                                position = -pageHeight * (pageNumber - 1);
7863                                pdf.addPage();
7864                                pdf.addImage(imgData, 'JPEG', 0, position, imgWidth, imgHeight);
7865                                heightLeft -= pageHeight;
7866                            }
7867
7868                            // Include language ID and document ID in filename for clarity
7869                            let filename = `${title}_lang${selectedLanguageId}_${date}.pdf`;
7870                            if (documentId) {
7871                                filename = `${title}_id${documentId}_lang${selectedLanguageId}_${date}.pdf`;
7872                            }
7873
7874                            // Save the PDF
7875                            pdf.save(filename);
7876
7877                            console.log('PDF generated successfully for language:', selectedLanguageId);
7878
7879                            // Show success message
7880                            displayAlertDocument('PDF document downloaded successfully.', 'success');
7881                        } catch (error) {
7882                            console.error('PDF generation error:', error);
7883                            displayAlertDocument('Error creating PDF: ' + error.message, 'error');
7884                        } finally {
7885                            // Clean up
7886                            document.body.removeChild(iframe);
7887
7888                            // Remove loading indicator
7889                            if (loadingIndicator && loadingIndicator.parentNode) {
7890                                if (createdLoadingIndicator) {
7891                                    // Remove the temporary loading indicator we created
7892                                    loadingIndicator.parentNode.removeChild(loadingIndicator);
7893                                } else {
7894                                    // Just hide the existing one
7895                                    loadingIndicator.style.display = 'none';
7896                                }
7897                            }
7898                        }
7899                    }).catch(error => {
7900                        console.error('Canvas rendering error:', error);
7901                        displayAlertDocument('Error creating PDF: ' + error.message, 'error');
7902
7903                        // Clean up
7904                        document.body.removeChild(iframe);
7905
7906                        // Remove loading indicator
7907                        if (loadingIndicator && loadingIndicator.parentNode) {
7908                            if (createdLoadingIndicator) {
7909                                loadingIndicator.parentNode.removeChild(loadingIndicator);
7910                            } else {
7911                                loadingIndicator.style.display = 'none';
7912                            }
7913                        }
7914                    });
7915                } catch (error) {
7916                    console.error('PDF preparation error:', error);
7917                    displayAlertDocument('Error preparing PDF: ' + error.message, 'error');
7918
7919                    // Clean up
7920                    document.body.removeChild(iframe);
7921
7922                    // Remove loading indicator
7923                    if (loadingIndicator && loadingIndicator.parentNode) {
7924                        if (createdLoadingIndicator) {
7925                            loadingIndicator.parentNode.removeChild(loadingIndicator);
7926                        } else {
7927                            loadingIndicator.style.display = 'none';
7928                        }
7929                    }
7930                }
7931            }, 1000); // Wait for 1 second to ensure iframe is fully rendered
7932
7933        } catch (error) {
7934            console.error('PDF setup error:', error);
7935            displayAlertDocument('Error setting up PDF generation: ' + error.message, 'error');
7936
7937            // Remove loading indicator
7938            if (loadingIndicator && loadingIndicator.parentNode) {
7939                if (createdLoadingIndicator) {
7940                    loadingIndicator.parentNode.removeChild(loadingIndicator);
7941                } else {
7942                    loadingIndicator.style.display = 'none';
7943                }
7944            }
7945        }
7946    });
7947}
7948
7949function downloadDocumentClientSide(format, documentId = null, languageId = null) {
7950    // Get the selected language ID
7951    const selectedLanguageId = languageId ||
7952                               window.currentLanguageId ||
7953                               (document.getElementById('select_language') ? document.getElementById('select_language').value : null) ||
7954                               2; // Default to 2 if not found
7955
7956    console.log('Download starting with language ID:', selectedLanguageId);
7957
7958    try {
7959        // Get the document content that's already displayed
7960        const contentContainer = document.querySelector('.document-preview');
7961        if (!contentContainer) {
7962            throw new Error('Document content not found');
7963        }
7964
7965        // Get the document title
7966        const title = document.querySelector('#document_result h4')?.textContent || 'document';
7967        const sanitizedTitle = title.replace(/\s+/g, '_').toLowerCase();
7968
7969        // Get the current date for the filename
7970        const currentDate = new Date().toISOString().slice(0, 10).replace(/-/g, '');
7971
7972        if (format === 'html') {
7973            // HTML download logic remains the same...
7974            let htmlContent = contentContainer.innerHTML;
7975            const isCompleteHtml = htmlContent.trim().toLowerCase().includes('<!doctype html>') ||
7976                                   htmlContent.trim().toLowerCase().includes('<html');
7977
7978            let content;
7979            if (isCompleteHtml) {
7980                const parser = new DOMParser();
7981                const htmlDoc = parser.parseFromString(htmlContent, 'text/html');
7982
7983                const styleOverride = document.createElement('style');
7984                styleOverride.textContent = `
7985                    :root {
7986                        --accent-color: #555 !important;
7987                    }
7988                    .variable {
7989                        color: #555 !important;
7990                    }
7991                    .highlight {
7992                        border-left-color: #3498db !important;
7993                    }
7994                    .important-notice {
7995                        border-left-color: #3498db !important;
7996                    }
7997                `;
7998
7999                htmlDoc.head.appendChild(styleOverride);
8000
8001                const languageMeta = document.createElement('meta');
8002                languageMeta.setAttribute('name', 'document-language-id');
8003                languageMeta.setAttribute('content', selectedLanguageId.toString());
8004                htmlDoc.head.appendChild(languageMeta);
8005
8006                if (documentId) {
8007                    const docIdMeta = document.createElement('meta');
8008                    docIdMeta.setAttribute('name', 'document-id');
8009                    docIdMeta.setAttribute('content', documentId.toString());
8010                    htmlDoc.head.appendChild(docIdMeta);
8011                }
8012
8013                content = '<!DOCTYPE html>\n' + htmlDoc.documentElement.outerHTML;
8014            } else {
8015                content = `<!DOCTYPE html>
8016<html>
8017<head>
8018    <meta charset="UTF-8">
8019    <meta name="document-language-id" content="${selectedLanguageId}">
8020    ${documentId ? `<meta name="document-id" content="${documentId}">` : ''}
8021    <title>${title}</title>
8022    <style>
8023        body { font-family: Arial, sans-serif; margin: 2em; }
8024        .document-container { margin: 0 auto; line-height: 1.5; }
8025        .variable { color: #555 !important; }
8026        [style*="color: var(--accent-color)"] { color: #555 !important; }
8027    </style>
8028</head>
8029<body>
8030    <div class="document-container">
8031        ${htmlContent}
8032    </div>
8033</body>
8034</html>`;
8035            }
8036
8037            const blob = new Blob([content], { type: 'text/html' });
8038            const fileName = documentId ?
8039                `${sanitizedTitle}_id${documentId}_lang${selectedLanguageId}_${currentDate}.html` :
8040                `${sanitizedTitle}_lang${selectedLanguageId}_${currentDate}.html`;
8041
8042            const url = window.URL.createObjectURL(blob);
8043            const a = document.createElement('a');
8044            a.style.display = 'none';
8045            a.href = url;
8046            a.download = fileName;
8047            document.body.appendChild(a);
8048            a.click();
8049
8050            window.URL.revokeObjectURL(url);
8051            document.body.removeChild(a);
8052
8053            console.log('HTML download completed');
8054
8055        } else if (format === 'pdf') {
8056            // For PDF, call the fixed function with all required parameters
8057            generatePdfFromIframe(
8058                contentContainer,
8059                sanitizedTitle,
8060                currentDate,
8061                null, // container - will be auto-detected
8062                null, // loadingIndicator - will be auto-created
8063                null, // documentType - not needed
8064                selectedLanguageId,
8065                documentId
8066            );
8067        } else {
8068            throw new Error('Unsupported format: ' + format);
8069        }
8070    } catch (error) {
8071        console.error("Download error:", error);
8072        displayAlertDocument('Download failed: ' + error.message, 'error');
8073    }
8074}
8075
8076function loadJsPdfLibraries(callback) {
8077    // Track how many libraries have loaded
8078    let librariesLoaded = 0;
8079    const requiredLibraries = 2;
8080
8081    // Function to check if all libraries are loaded
8082    const checkAllLoaded = () => {
8083        librariesLoaded++;
8084        if (librariesLoaded === requiredLibraries) {
8085            callback();
8086        }
8087    };
8088
8089    // Check if jsPDF is already loaded
8090    if (typeof jspdf !== 'undefined') {
8091        librariesLoaded++;
8092    } else {
8093        // Load jsPDF
8094        const jsPdfScript = document.createElement('script');
8095        jsPdfScript.src = 'https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js';
8096        jsPdfScript.onload = checkAllLoaded;
8097        jsPdfScript.onerror = () => {
8098            console.error('Failed to load jsPDF library');
8099            displayAlertDocument('Failed to load PDF generation library', 'error');
8100        };
8101        document.head.appendChild(jsPdfScript);
8102    }
8103
8104    // Check if html2canvas is already loaded
8105    if (typeof html2canvas !== 'undefined') {
8106        librariesLoaded++;
8107    } else {
8108        // Load html2canvas
8109        const html2canvasScript = document.createElement('script');
8110        html2canvasScript.src = 'https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js';
8111        html2canvasScript.onload = checkAllLoaded;
8112        html2canvasScript.onerror = () => {
8113            console.error('Failed to load html2canvas library');
8114            displayAlertDocument('Failed to load PDF generation library', 'error');
8115        };
8116        document.head.appendChild(html2canvasScript);
8117    }
8118
8119    // If both already loaded, call callback immediately
8120    if (librariesLoaded === requiredLibraries) {
8121        callback();
8122    }
8123}
8124
8125function processTemplate(template, formData) {
8126    // Replace all placeholders in the template with actual values
8127    let processedTemplate = template;
8128
8129    // Map form fields to template placeholders
8130    const placeholderMap = {
8131        'name': '{{COMPANY_NAME}}',
8132        'address': '{{COMPANY_ADDRESS}}',
8133        'country': '{{COUNTRY}}',
8134        'email': '{{EMAIL}}',
8135        'phone': '{{PHONE_NUMBER}}',
8136        'websiteUrl': '{{WEBSITE_URL}}',
8137        'lastUpdated': '{{LAST_UPDATED}}',
8138        'effectiveDate': '{{EFFECTIVE_DATE}}',
8139        'serviceName': '{{SERVICE_NAME}}',
8140        'companyRegNo': '{{COMPANY_REG_NO}}',
8141        'legalEntity': '{{LEGAL_ENTITY}}',
8142        'privacyUrl': '{{PRIVACY_URL}}',
8143        'termsUrl': '{{TERMS_URL}}',
8144        'pricingUrl': '{{PRICING_URL}}'
8145    };
8146
8147    // Replace each placeholder with the actual value
8148    for (const [field, placeholder] of Object.entries(placeholderMap)) {
8149        let value = formData[field] || '';
8150
8151        // Format dates nicely
8152        if (field === 'lastUpdated' || field === 'effectiveDate') {
8153            value = formatDateForDisplay(value);
8154        }
8155
8156        // Use a global regex to replace all instances of the placeholder
8157        processedTemplate = processedTemplate.replace(new RegExp(placeholder, 'g'), value);
8158    }
8159
8160    // IMPORTANT: Do NOT convert newlines to <br> tags here
8161    // This was causing the issue with extra <br> tags in the HTML output
8162    // Remove this line: processedTemplate = processedTemplate.replace(/\n/g, '<br>');
8163
8164    // Check if the template is already a complete HTML document
8165    const isCompleteHtml = processedTemplate.trim().toLowerCase().startsWith('<!doctype html>') ||
8166                          processedTemplate.trim().toLowerCase().startsWith('<html');
8167
8168    // If it's already complete HTML, return it as-is
8169    if (isCompleteHtml) {
8170        return processedTemplate;
8171    }
8172
8173    // Otherwise, wrap the content in a minimal container
8174    return `<div class="document-container">${processedTemplate}</div>`;
8175}
8176
8177async function getDocumentTemplate(docTypeCode, languageId = null) {
8178    try {
8179        // If no language ID is provided, use the currently selected language
8180        if (languageId === null && window.currentLanguageId) {
8181            languageId = window.currentLanguageId;
8182        } else if (languageId === null) {
8183            // Default language ID (you may need to adjust this)
8184            languageId = 2;
8185        }
8186
8187    
8187    const data = {
8188            sid: getSid(),
8189            documentTypeCode: docTypeCode,
8190            languageId: languageId
8191        };
8192
8193        const response = await postRequest(endpoint + '/document/getDocumentTemplate', data);
8194
8195        if (!response.ok) {
8196            throw new Error('Failed to fetch document template: ' + response.status);
8197        }
8198
8199        const responseData = await response.json();
8200
8201        if (responseData.statusCode !== 0) {
8202            throw new Error('Error fetching document template: ' + (responseData.statusMessage || 'Unknown error'));
8203        }
8204
8205        if (!responseData.template || !responseData.template.templateText) {
8206            throw new Error('No template found for document type: ' + docTypeCode);
8207        }
8208
8209        return responseData.template;
8210    } catch (error) {
8211        console.error('Error fetching document template:', error);
8212        throw error;
8213    }
8214}
8215
8216function getDocumentTypeName(docTypeCode) {
8217    const documentTypeMap = {
8218        'privacyPolicy': 'Privacy Policy',
8219        'termsOfUse': 'Terms of Use',
8220        'dpa': 'Data Processing Agreement',
8221        'cookiePolicy': 'Cookie Policy'
8222    };
8223
8224    return documentTypeMap[docTypeCode] || 'Document';
8225}
8226
8227function getDGCookie(name) {
8228    const value = `; ${document.cookie}`;
8229    const parts = value.split(`; ${name}=`);
8230    if (parts.length === 2) return parts.pop().split(';').shift();
8231    return null;
8232}
8233
8234function normalizeLanguageCode(langCode) {
8235    if (!langCode) return null;
8236
8237    // Convert to lowercase and replace underscores with hyphens
8238    let normalized = langCode.toLowerCase().replace(/_/g, '-');
8239
8240    // Return both the full code and just the language part (before the first hyphen)
8241    const parts = normalized.split('-');
8242    return {
8243        full: normalized,           // e.g., 'fr-fr'
8244        language: parts[0],         // e.g., 'fr'
8245        original: langCode          // e.g., 'fr_fr'
8246    };
8247}
8248
8249function languageCodesMatch(cookieCode, dbCode) {
8250    if (!cookieCode || !dbCode) return false;
8251
8252    // Direct exact match (most likely to work)
8253    if (cookieCode === dbCode) return true;
8254
8255    const cookie = normalizeLanguageCode(cookieCode);
8256    const db = normalizeLanguageCode(dbCode);
8257
8258    // Normalized match
8259    if (cookie.full === db.full) return true;
8260    if (cookie.original.toLowerCase() === dbCode.toLowerCase()) return true;
8261    if (cookieCode.toLowerCase() === dbCode.toLowerCase()) return true;
8262
8263    // Language part match (e.g., 'fr' matches 'fr-FR')
8264    if (cookie.language === db.language) return true;
8265    if (cookie.language === db.full) return true;
8266    if (cookie.full === db.language) return true;
8267
8268    return false;
8269}
8270
8271async function loadLanguageDropdown() {
8272    try {
8273        // Check if language dropdown exists
8274        const languageDropdown = document.getElementById('select_language');
8275        if (!languageDropdown) {
8276            console.warn('Language dropdown not found - skipping language loading');
8277            return;
8278        }
8279
8280        // Get language code from cookie (read only, don't set)
8281        const savedLanguageCode = getDGCookie('lang');
8282        console.log('Saved language code from cookie:', savedLanguageCode);
8283
8284        // Prepare request data
8285        const data = { sid: getSid() };
8286
8287        // Make the API request to get languages using your existing endpoint
8288        const response = await postRequest(endpoint + '/languages', data);
8289
8290        if (!response.ok) {
8291            throw new Error('Failed to fetch languages: ' + response.status);
8292        }
8293
8294        const responseData = await response.json();
8295
8296        if (responseData.statusCode !== 0) {
8297            console.error('Error fetching languages:', responseData.statusMessage);
8298            // Don't throw error, just log it and continue without languages
8299            return;
8300        }
8301
8302        // Clear existing options
8303        languageDropdown.innerHTML = '';
8304
8305        // Add languages to dropdown
8306        if (responseData.languages && Array.isArray(responseData.languages) && responseData.languages.length > 0) {
8307            let defaultLanguageId = 2; // Fallback default (English)
8308            let savedLanguageFound = false;
8309
8310            // Debug: Log all available languages and their codes
8311            //console.log('Available languages:');
8312            //responseData.languages.forEach(lang => {
8313            //   console.log(`- ${lang.name}: ${lang.shortCode} (ID: ${lang.id})`);
8314            //});
8315
8316            responseData.languages.forEach(language => {
8317                const option = document.createElement('option');
8318                option.value = language.id;
8319                option.text = language.name; // This will show "Français (French)"
8320
8321                // Store language code as data attribute for future reference
8322                option.dataset.languageCode = language.shortCode;
8323
8324                languageDropdown.appendChild(option);
8325
8326                // Check if this language matches the saved cookie using flexible matching
8327                if (savedLanguageCode && languageCodesMatch(savedLanguageCode, language.shortCode)) {
8328                    defaultLanguageId = language.id;
8329                    savedLanguageFound = true; 
8330                    console.log(`✓ Found matching language for cookie '${savedLanguageCode}':`, language.name, `(shortCode: '${language.shortCode}', ID: ${language.id})`);
8331                }
8332
8333                // Check specific cases based on your data structure
8334                // English (ID: 2) as fallback default
8335                //if (!savedLanguageFound && language.id === 2) {
8336                    // Keep English as fallback if no cookie match found
8337                //}
8338            });
8339
8340            // Set the selected language
8341            languageDropdown.value = defaultLanguageId;
8342
8343            // Store current language ID in window object for easy access
8344            window.currentLanguageId = parseInt(defaultLanguageId);
8345
8346            // Add event listener for language change (without setting cookies)
8347            languageDropdown.addEventListener('change', function() {
8348                // Update global language ID
8349                window.currentLanguageId = parseInt(this.value);
8350
8351                // Get the selected language info
8352                const selectedOption = this.options[this.selectedIndex];
8353                const selectedLanguageName = selectedOption.text;
8354                const selectedLanguageCode = selectedOption.dataset.languageCode;
8355
8356                console.log(`Language changed to: ${selectedLanguageName} (ID: ${this.value}, shortCode: ${selectedLanguageCode})`);
8357            });
8358
8359            // Log final result
8360            if (savedLanguageFound) {
8361                console.log('✓ Successfully loaded saved language from cookie');
8362            } else if (savedLanguageCode) {
8363                console.warn(`⚠ Saved language code '${savedLanguageCode}' not found in available languages. Using default English (ID: 2).`);
8364            } else {
8365                console.log('No language cookie found. Using default English (ID: 2).');
8366            }
8367
8368            console.log('Languages loaded successfully. Selected language ID:', defaultLanguageId);
8369        } else {
8370            console.warn('No languages returned from server');
8371            // Add a default English option
8372            const defaultOption = document.createElement('option');
8373            defaultOption.value = 2;
8374            defaultOption.text = 'English';
8375            defaultOption.dataset.languageCode = 'en_gb';
8376            languageDropdown.appendChild(defaultOption);
8377            window.currentLanguageId = 2;
8378        }
8379    } catch (error) {
8380        console.error('Error loading languages:', error);
8381        // Don't prevent the rest of the initialization from continuing
8382        // Just set a default language
8383        window.currentLanguageId = 2;
8384    }
8385}
8386
8387
8388
8389// Cookie Manager Implementation
8390
8391async function loadCookieManager() {
8392    if (checkIfLoginRequired())
8393        return;
8394
8395    // Check if required DOM elements exist - UPDATED: Changed to select_domain
8396    const domainDropdown = document.getElementById('select_domain');
8397    const languageDropdown = document.getElementById('select_language');
8398    const container = document.getElementById('cookie_manager_container');
8399
8400    if (!domainDropdown) {
8401        console.error('Domain dropdown not found - make sure element with ID "select_domain" exists');
8402        return;
8403    }
8404
8405    if (!languageDropdown) {
8406        console.error('Language dropdown not found - make sure element with ID "select_language" exists');
8407        return;
8408    }
8409
8410    if (!container) {
8411        console.error('Cookie manager container not found - make sure element with ID "cookie_manager_container" exists');
8412        return;
8413    }
8414
8415    try {
8416        await loadLanguageDropdown();
8417    } catch (error) {
8418        console.error('Error loading languages:', error);
8419    }
8420
8421    // UPDATED: Load domains instead of projects
8422    try {
8423        let domains = null;
8424        const data = { sid: getSid(), projectTypeId: 2, method: 1 };
8425        const response = await postRequest(endpoint + '/project-domain/list', data);
8426
8427        if (response.ok) {
8428            const responseBody = await response.json();
8429            if (responseBody.statusCode === 0) {
8430                domains = responseBody.domains;
8431            } else {
8432                console.error('Error loading domains: ' + responseBody.statusMessage);
8433                displayAlertCookie('Failed to load domains: ' + responseBody.statusMessage, 'error');
8434                return;
8435            }
8436        } else {
8437            console.error('Error: ' + response.statusText);
8438            displayAlertCookie('Failed to load domains: ' + response.statusText, 'error');
8439            return;
8440        }
8441
8442        // Clear existing options first to prevent duplicates
8443        domainDropdown.innerHTML = '';
8444
8445        // Add domain options
8446        let firstValidDomain = null;
8447        if (domains && domains.length > 0) {
8448            domains.forEach(domain => {
8449                const option = document.createElement('option');
8450                option.value = domain.id;
8451                option.text = domain.domainName;
8452                domainDropdown.appendChild(option);
8453
8454                // Store first domain for default selection
8455                if (!firstValidDomain) {
8456                    firstValidDomain = domain;
8457                }
8458            });
8459        } else {
8460            console.error('No domains available');
8461            displayAlertCookie('No domains available', 'error');
8462            return;
8463        }
8464
8465        // Set first domain as default if no URL parameter is provided
8466        const selectedDomainId = new URLSearchParams(window.location.search).get("id");
8467        if (selectedDomainId) {
8468            domainDropdown.value = selectedDomainId;
8469        } else if (firstValidDomain) {
8470            domainDropdown.value = firstValidDomain.id;
8471        }
8472
8473        // UPDATED: Changed event listener to use domain instead of project
8474        domainDropdown.addEventListener('change', function() {
8475            domainDropdown.classList.remove('d-none');
8476            loadCookiesForDomain(domainDropdown.value);
8477			setCookieScanUrl(domainDropdown.options[domainDropdown.selectedIndex].text,1);
8478        });
8479
8480        domainDropdown.dispatchEvent(new Event('change'));
8481
8482    } catch (error) {
8483        console.error('Error loading domains:', error);
8484        displayAlertCookie('Failed to load domains: ' + error.message, 'error');
8485    }
8486}
8487
8488function setCookieScanUrl(url,count){
8489	if (count>20)
8490		return;
8491	
8492	var domainDropdown = document.getElementById('select_domain'), vvx = document.getElementById('scanUrl');
8493	//console.log("sdfsfsdfsfsdfd " + url);
8494	if (vvx)
8495		vvx.value = 'https://'+domainDropdown.options[domainDropdown.selectedIndex].text+'/';
8496	else
8497		setTimeout(function(){setCookieScanUrl(url, count+1);},200);
8498}
8499
8500function validateCookieData(cookieData) {
8501    const errors = [];
8502
8503    // Debug: Log the data being validated
8504    console.log('Validating cookie data:', cookieData);
8505
8506    // Check required fields with more detailed logging
8507    if (!cookieData.name || cookieData.name.trim() === '') {
8508        console.log('Name validation failed:', cookieData.name);
8509        errors.push(translations['nameRequired'] || 'Cookie name is required');
8510    }
8511
8512    if (!cookieData.type || cookieData.type.trim() === '') {
8513        console.log('Type validation failed:', cookieData.type);
8514        errors.push(translations['typeRequired'] || 'Type is required');
8515    }
8516
8517    // Validate URL format for privacyUrl if provided
8518    if (cookieData.privacyUrl && cookieData.privacyUrl.trim() !== '') {
8519        try {
8520            new URL(cookieData.privacyUrl);
8521        } catch (e) {
8522            console.log('Privacy URL format validation failed:', cookieData.privacyUrl);
8523            errors.push('Privacy URL must be a valid URL');
8524        }
8525    }
8526
8527    console.log('Validation errors:', errors);
8528    return errors;
8529}
8530
8531function getLanguageShortCodeById(languageId) {
8532    // Language mapping based on the provided data structure
8533    const languageMap = {
8534        '1': 'tr_TR',     // Türkçe (Turkish)
8535        '2': 'en_gb',     // English
8536        '3': 'fr_fr',     // Français (French)
8537        '4': 'es_es',     // Español (Spanish)
8538        '5': 'de',        // Deutsch (German)
8539        '6': 'zh',        // 中文 (Chinese)
8540        '7': 'ar',        // اللغة العربية (Arabic)
8541        '8': 'ro',        // Română (Romanian)
8542        '9': 'ru',        // Русский (Russian)
8543        '10': 'ja',       // 日本語 (Japanese)
8544        '11': 'ko',       // 한국어 (Korean)
8545        '12': 'it_it',    // Italiano (Italian)
8546        '13': 'bg',       // Български (Bulgarian)
8547        '14': 'bs',       // Bosanski (Bosnian)
8548        '15': 'ca',       // Català (Catalan)
8549        '16': 'cs',       // Česky (Czech)
8550        '17': 'cy',       // Cymraeg (Welsh)
8551        '18': 'da',       // Dansk (Danish)
8552        '19': 'el',       // Ελληνική (Greek)
8553        '20': 'et',       // Eesti (Estonian)
8554        '21': 'eu',       // Euskara (Basque)
8555        '22': 'fi',       // Suomalainen (Finnish)
8556        '23': 'gl',       // Galego (Galician)
8557        '24': 'he',       // עִברִית (Hebrew)
8558        '25': 'hr',       // Hrvatski (Croatian)
8559        '26': 'hu',       // Magyar (Hungarian)
8560        '27': 'id',       // Bahasa Indonesia (Indonesian)
8561        '28': 'lt',       // Lietuvių kalba (Lithuanian)
8562        '29': 'lv',       // latviski (Latvian)
8563        '30': 'mk',       // македонски (Macedonian)
8564        '31': 'ms',       // Melayu (Malay)
8565        '32': 'mt',       // Malti (Malteze)
8566        '33': 'nl',       // Nederlands (Dutch)
8567        '34': 'nb',       // Norsk (Norwegian)
8568        '35': 'pl',       // Polski (Polish)
8569        '36': 'pt-br',    // Português - Brasil (Portuguese)
8570        '37': 'pt-pt',    // Português - Portugal (Portuguese)
8571        '38': 'sk',       // Slovenská (Slovak)
8572        '39': 'sl',       // Slovenski (Slovenian)
8573        '40': 'sr',       // Српски (Serbian)
8574        '41': 'sv',       // Svenska (Swedish)
8575        '42': 'uk',       // Українська (Ukrainian)
8576        '43': 'fl'        // Filipino (Tagalog)
8577    };
8578
8579    return languageMap[languageId] || null;
8580}
8581
8582function getLanguageNameByShortCode(shortCode) {
8583    const languageNames = {
8584        'tr_TR': 'TR',
8585        'en_gb': 'EN',
8586        'fr_fr': 'FR',
8587        'es_es': 'ES',
8588        'de': 'DE',
8589        'zh': 'ZH',
8590        'ar': 'AR',
8591        'ro': 'RO',
8592        'ru': 'RU',
8593        'ja': 'JA',
8594        'ko': 'KO',
8595        'it_it': 'IT',
8596        'bg': 'BG',
8597        'bs': 'BS',
8598        'ca': 'CA',
8599        'cs': 'CS',
8600        'cy': 'CY',
8601        'da': 'DA',
8602        'el': 'EL',
8603        'et': 'ET',
8604        'eu': 'EU',
8605        'fi': 'FI',
8606        'gl': 'GL',
8607        'he': 'HE',
8608        'hr': 'HR',
8609        'hu': 'HU',
8610        'id': 'ID',
8611        'lt': 'LT',
8612        'lv': 'LV',
8613        'mk': 'MK',
8614        'ms': 'MS',
8615        'mt': 'MT',
8616        'nl': 'NL',
8617        'nb': 'NB',
8618        'pl': 'PL',
8619        'pt-br': 'PT-BR',
8620        'pt-pt': 'PT-PT',
8621        'sk': 'SK',
8622        'sl': 'SL',
8623        'sr': 'SR',
8624        'sv': 'SV',
8625        'uk': 'UK',
8626        'fl': 'FL'
8627    };
8628
8629    return languageNames[shortCode] || shortCode.toUpperCase();
8630}
8631
8632function createEnhancedCookieModal(title, cookieData, onSave, domainId) {
8633    const modal = document.createElement('div');
8634    modal.classList.add('modal', 'fade');
8635    modal.tabIndex = -1;
8636
8637    // Store domainId on the modal element for debugging
8638    modal.setAttribute('data-domain-id', domainId);
8639    console.log('Creating modal with domainId:', domainId);
8640
8641    // Get selected language shortCode for description
8642    const languageDropdown = document.getElementById('select_language');
8643    let selectedLanguageCode = 'en_gb'; // default fallback to English
8644
8645    if (languageDropdown && languageDropdown.selectedOptions.length > 0) {
8646        const selectedOption = languageDropdown.selectedOptions[0];
8647        const selectedId = selectedOption.value;
8648
8649        // Try to get shortCode from data attribute first, then lookup by ID
8650        selectedLanguageCode = selectedOption.dataset.shortcode ||
8651                             selectedOption.getAttribute('data-shortcode') ||
8652                             getLanguageShortCodeById(selectedId) ||
8653                             'en_gb';
8654    }
8655
8656    console.log('Modal created with language code:', selectedLanguageCode);
8657
8658    // Get description for selected language
8659    let currentDescription = '';
8660    if (cookieData.description) {
8661        if (typeof cookieData.description === 'object') {
8662            currentDescription = cookieData.description[selectedLanguageCode] || cookieData.description['en'] || '';
8663        } else {
8664            // Handle legacy string descriptions
8665            currentDescription = cookieData.description;
8666        }
8667    }
8668
8669    // Get current storage type value (handle both numeric and string)
8670    const currentStorageType = parseInt(cookieData.storageType, 10) || 1;
8671
8672    modal.innerHTML = `
8673        <div class="modal-dialog modal-lg">
8674            <div class="modal-content">
8675                <div class="modal-header">
8676                    <h5 class="modal-title">${title}</h5>
8677                    <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
8678                </div>
8679                <div class="modal-body">
8680                    <div id="validationErrors" class="alert alert-danger d-none"></div>
8681                    <form id="cookieForm" novalidate>
8682                        <!-- Basic Information -->
8683                        <div class="row">
8684                            <div class="col-12">
8685                                <h6 class="text-primary mb-3"><i class="fas fa-info-circle me-2"></i>${translations['basicInformation'] || 'Basic Information'}</h6>
8686                            </div>
8687                        </div>
8688
8689                        <div class="row">
8690                            <div class="col-md-6">
8691                                <div class="mb-3">
8692                                    <label for="cookieName" class="form-label">
8693                                        ${translations['name'] || 'Name'} <span class="text-danger">*</span>
8694                                    </label>
8695                                    <input type="text" class="form-control" id="cookieName"
8696                                           value="${cookieData.name || ''}" required>
8697                                    <div class="invalid-feedback">${translations['nameRequired'] || 'Cookie name is required'}</div>
8698                                </div>
8699                            </div>
8700                            <div class="col-md-6">
8701                                <div class="mb-3">
8702                                    <label for="cookieDisplayName" class="form-label">
8703                                        ${translations['displayName'] || 'Display Name'}
8704                                    </label>
8705                                    <input type="text" class="form-control" id="cookieDisplayName"
8706                                           value="${cookieData.displayName || ''}" placeholder="${translations['displayNamePlaceHolder'] || 'e.g., Analytics Co
8706okie (optional - defaults to name'}">
8707                                    <small class="form-text text-muted" ${translations['displayNameNote'] || 'If empty, will default to the cookie name'}></small>
8708                                </div>
8709                            </div>
8710                        </div>
8711
8712                        <div class="row">
8713                            <div class="col-md-6">
8714                                <div class="mb-3">
8715                                    <label for="cookieType" class="form-label">
8716                                        ${translations['type'] || 'Type'} <span class="text-danger">*</span>
8717                                    </label>
8718                                    <select class="form-select" id="cookieType" required>
8719                                        <option value="">${translations['selectType'] || 'Select Type'}</option>
8720                                        <option value="Essential" ${cookieData.type?.toLowerCase() === 'essential' ? 'selected' : ''}>${translations['essential'] || 'Essential'}</option>
8721                                        <option value="Personalization" ${cookieData.type?.toLowerCase() === 'personalization' ? 'selected' : ''}>${translations['personalization'] || 'Personalization'}</option>
8722                                        <option value="Analytics" ${cookieData.type?.toLowerCase() === 'analytics' ? 'selected' : ''}>${translations['analytics'] || 'Analytics'}</option>
8723                                        <option value="Marketing" ${cookieData.type?.toLowerCase() === 'marketing' ? 'selected' : ''}>${translations['marketing'] || 'Marketing'}</option>
8724                                        <option value="Security" ${cookieData.type?.toLowerCase() === 'security' ? 'selected' : ''}>${translations['security'] || 'Security'}</option>
8725                                        <option value="Other" ${cookieData.type?.toLowerCase() === 'other' ? 'selected' : ''}>${translations['other'] || 'Other'}</option>
8726                                    </select>
8727                                    <div class="invalid-feedback">${translations['typeRequired'] || 'Type is required'}</div>
8728                                </div>
8729                            </div>
8730
8731                            <div class="col-md-6">
8732                                <div class="mb-3">
8733                                    <label for="cookieRetention" class="form-label">${translations['retentionPeriod'] || 'Retention Period'}
8734                                    </label>
8735                                    <input type="text" class="form-control" id="cookieRetention"
8736                                           value="${cookieData.retentionPeriod || 'Session'}"
8737                                           placeholder="e.g., Session, 30 days, 1 year, etc.">
8738                                    <div class="form-text">${translations['retentionPeriodNote'] || 'Examples: Session, 30 days, 1 year, 2 years, etc.'}</div>
8739                                </div>
8740                            </div>
8741                        </div>
8742
8743                        <div class="row">
8744                            <div class="col-md-6">
8745                                <div class="mb-3">
8746                                    <label for="cookieStorageType" class="form-label">${translations['storageType'] || 'Storage Type'}
8747                                    </label>
8748                                    <select class="form-select" id="cookieStorageType">
8749                                        <option value="1" ${currentStorageType === 1 ? 'selected' : ''}>HTTP Cookie</option>
8750                                        <option value="2" ${currentStorageType === 2 ? 'selected' : ''}>Local Storage</option>
8751                                        <option value="3" ${currentStorageType === 3 ? 'selected' : ''}>Session Storage</option>
8752                                        <option value="5" ${currentStorageType === 5 ? 'selected' : ''}>Tracking Pixel</option>
8753                                    </select>
8754                                    <div class="form-text">${translations['cookieStorageNote'] || 'Select how this cookie/tracker stores data'}</div>
8755                                </div>
8756                            </div>
8757                        </div>
8758
8759                        <!-- Description and Purpose -->
8760                        <div class="row">
8761                            <div class="col-12">
8762                                <h6 class="text-primary mb-3 mt-4"><i class="fas fa-file-text me-2"></i>${translations['descriptionPurpose'] || 'Description and Purpose'}</h6>
8763                            </div>
8764                        </div>
8765
8766                        <div class="mb-3">
8767                            <label for="cookieDescription" class="form-label">
8768                                ${translations['description'] || 'Description'} <span class="badge bg-success">${getLanguageNameByShortCode(selectedLanguageCode)}</span>
8769                            </label>
8770                            <textarea class="form-control" id="cookieDescription" rows="3"
8771                                      placeholder="Describe the purpose of this cookie, what data it stores, and why it is necessary...">${currentDescription}</textarea>
8772                            <small class="form-text text-muted">${translations['descriptionLanguageNote'] || 'Description will be saved for the selected language'} (${getLanguageNameByShortCode(selectedLanguageCode)})</small>
8773                        </div>
8774
8775                        <div class="row">
8776                            <div class="col-md-6">
8777                                <div class="mb-3">
8778                                    <label for="cookieVendor" class="form-label">
8779                                        ${translations['vendor'] || 'Vendor'}
8780                                    </label>
8781                                    <input type="text" class="form-control" id="cookieVendor"
8782                                           value="${cookieData.vendor || ''}" placeholder="Google, Facebook, etc.">
8783                                </div>
8784                            </div>
8785                            <div class="col-md-6">
8786                                <div class="mb-3">
8787                                    <label for="cookiePrivacyUrl" class="form-label">
8788                                        ${translations['privacyUrl'] || 'Privacy URL'}
8789                                    </label>
8790                                    <input type="url" class="form-control" id="cookiePrivacyUrl"
8791                                           value="${cookieData.privacyUrl || ''}" placeholder="https://example.com/privacy">
8792                                    <div class="invalid-feedback">Valid privacy URL is required</div>
8793                                </div>
8794                            </div>
8795                        </div>
8796
8797                        <!-- Technical Details -->
8798                        <div class="row">
8799                            <div class="col-12">
8800                                <h6 class="text-primary mb-3 mt-4"><i class="fas fa-cogs me-2"></i>${translations['technicalDetails'] || 'Technical Details'}</h6>
8801                            </div>
8802                        </div>
8803
8804                        <div class="mb-3">
8805                            <label for="cookieScript" class="form-label">${translations['script'] || 'Script URL'}</label>
8806                            <input type="url" class="form-control" id="cookieScript"
8807                                      placeholder="${translations['cookieScriptPlaceholder'] || 'https://www.google-analytics.com/analytics.js'}" value="${cookieData.script || ''}">
8808                            <small class="form-text text-muted">${translations['cookieScriptNote'] || 'If no script url provided, the cookie shouldn\'t be marked as essential.'}</small>
8809                        </div>
8810
8811                        <div class="alert alert-warning py-2 mb-3" role="alert">
8812                            <strong>${translations['important'] || 'Important:'}</strong>
8813                            <p class="mb-2 mt-1">
8814                                ${translations['importantNote'] || 'Avoid using your own website\'s URL as the Script URL, as it may disrupt your website\'s functionality.'}
8815                            </p>
8816                            <p class="mb-1">
8817                                ${translations['importantExample'] || 'For example, if the third-party script that sets this cookie is:'}
8818                            </p>
8819                            <code class="d-block bg-light p-2 mb-2 rounded">
8820                                &lt;script async src="https://www.google-analytics.com/analytics.js"&gt;&lt;/script&gt;
8821                            </code>
8822                            <p class="mb-0">
8823                                ${translations['importantConclusion'] || 'You should enter the Script URL pattern as:'} <strong>https://www.google-analytics.com/analytics.js</strong>
8824                            </p>
8825                        </div>
8826                    </form>
8827                </div>
8828                <div class="modal-footer">
8829                    <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
8830                        ${translations['cancel'] || 'Cancel'}
8831                    </button>
8832                    <button type="button" class="btn btn-primary" id="saveCookieBtn">
8833                        <i class="fas fa-save me-2"></i>${translations['save'] || 'Save'}
8834                    </button>
8835                </div>
8836            </div>
8837        </div>
8838    `;
8839
8840    // Add save functionality with enhanced validation
8841    const saveBtn = modal.querySelector('#saveCookieBtn');
8842    const form = modal.querySelector('#cookieForm');
8843    const errorContainer = modal.querySelector('#validationErrors');
8844
8845    saveBtn.addEventListener('click', () => {
8846        // Clear previous validation states
8847        form.classList.remove('was-validated');
8848        errorContainer.classList.add('d-none');
8849
8850        // Get form data - Updated with multilingual description support using shortCode
8851        const nameValue = modal.querySelector('#cookieName').value.trim();
8852        const displayNameValue = modal.querySelector('#cookieDisplayName').value.trim();
8853        const descriptionValue = modal.querySelector('#cookieDescription').value.trim();
8854        const scriptValue = modal.querySelector('#cookieScript').value.trim();
8855        let typeValue = modal.querySelector('#cookieType').value;
8856
8857        // Get storage type as numeric value
8858        const storageTypeValue = parseInt(modal.querySelector('#cookieStorageType').value, 10);
8859
8860        // If no script is provided and type is essential, change to other
8861        if (!scriptValue && typeValue === 'essential') {
8862            typeValue = 'other';
8863            console.log('Script is blank and type was essential - changed to other');
8864        }
8865
8866        // Get selected language shortCode from dropdown based on the actual language structure
8867        const languageDropdown = document.getElementById('select_language');
8868        let selectedLanguageCode = 'en_gb'; // default fallback to English
8869
8870        if (languageDropdown && languageDropdown.selectedOptions.length > 0) {
8871            const selectedOption = languageDropdown.selectedOptions[0];
8872            const selectedId = selectedOption.value;
8873
8874            // Try to find the language shortCode by matching the ID
8875            selectedLanguageCode = selectedOption.dataset.shortcode ||
8876                                 selectedOption.getAttribute('data-shortcode') ||
8877                                 getLanguageShortCodeById(selectedId) ||
8878                                 'en_gb';
8879        }
8880
8881        console.log('Selected language code for description:', selectedLanguageCode);
8882
8883        // Handle multilingual description
8884        let descriptionObject = {};
8885        if (cookieData.description && typeof cookieData.description === 'object') {
8886            // Keep existing translations
8887            descriptionObject = { ...cookieData.description };
8888        }
8889        // Update the description for the selected language
8890        descriptionObject[selectedLanguageCode] = descriptionValue;
8891
8892        const formData = {
8893            name: nameValue,
8894            displayName: displayNameValue || nameValue, // Use name if displayName is empty
8895            type: typeValue, // Use the potentially modified type
8896            retentionPeriod: modal.querySelector('#cookieRetention').value.trim() || 'Session',
8897            storageType: storageTypeValue, // Send numeric value (1-6)
8898            custom: true, // Always true for user-added/edited cookies
8899            description: descriptionObject,
8900            vendor: modal.querySelector('#cookieVendor').value.trim(),
8901            privacyUrl: modal.querySelector('#cookiePrivacyUrl').value.trim(),
8902            script: scriptValue,
8903            id: cookieData.id || generateCookieId()
8904        };
8905
8906        // Validate form data
8907        const validationErrors = validateCookieData(formData);
8908
8909        if (validationErrors.length > 0) {
8910            errorContainer.innerHTML = translations['fixErrorMessage'] || '<strong>Please fix the following errors:</strong><ul>' +
8911                validationErrors.map(error => `<li>${error}</li>`).join('') + '</ul>';
8912            errorContainer.classList.remove('d-none');
8913            form.classList.add('was-validated');
8914            return;
8915        }
8916
8917        // Check HTML5 validation
8918        if (form.checkValidity()) {
8919            onSave(formData);
8920            bootstrap.Modal.getInstance(modal).hide();
8921        } else {
8922            form.classList.add('was-validated');
8923        }
8924    });
8925
8926    return modal;
8927}
8928
8929function showAddCookieModal(data, domainId) {
8930    console.log('showAddCookieModal called with domainId:', domainId);
8931
8932    const modal = createEnhancedCookieModal(translations['addCookie'] || 'Add Cookie', {
8933        name: '',
8934        displayName: '',
8935        type: '',
8936        retentionPeriod: 'Session',
8937        custom: true,
8938        description: '',
8939        vendor: '',
8940        privacyUrl: '',
8941        script: ''
8942    }, (cookieData, modalDomainId) => {
8943        console.log('Add cookie callback called with domainId:', modalDomainId || domainId);
8944        addCookie(cookieData, data, modalDomainId || domainId);
8945    }, domainId);
8946
8947    document.body.appendChild(modal);
8948    const bsModal = new bootstrap.Modal(modal);
8949    bsModal.show();
8950
8951    modal.addEventListener('hidden.bs.modal', () => {
8952        document.body.removeChild(modal);
8953    });
8954}
8955
8956function showEditCookieModal(cookie, data, domainId) {
8957    console.log('showEditCookieModal called with domainId:', domainId);
8958
8959    const modal = createEnhancedCookieModal('Edit Cookie', cookie, (cookieData, modalDomainId) => {
8960        console.log('Edit cookie callback called with domainId:', modalDomainId || domainId);
8961        updateCookie(cookieData, data, modalDomainId || domainId, cookie);
8962    }, domainId);
8963
8964    document.body.appendChild(modal);
8965    const bsModal = new bootstrap.Modal(modal);
8966    bsModal.show();
8967
8968    modal.addEventListener('hidden.bs.modal', () => {
8969        document.body.removeChild(modal);
8970    });
8971}
8972
8973function createEnhancedCookieRow(cookie, data, domainId) {
8974    const row = document.createElement('tr');
8975    row.classList.add('cookie-row');
8976
8977    // Display Name cell (first column) with comprehensive badges
8978    const displayNameCell = document.createElement('td');
8979    const displayNameContainer = document.createElement('div');
8980
8981    const displayNameSpan = document.createElement('span');
8982    displayNameSpan.textContent = cookie.displayName || 'N/A';
8983    displayNameSpan.classList.add('fw-bold');
8984    displayNameContainer.appendChild(displayNameSpan);
8985
8986    // Vendor info
8987    if (cookie.vendor) {
8988        const vendorSpan = document.createElement('small');
8989        vendorSpan.classList.add('text-muted', 'd-block');
8990        vendorSpan.textContent = `by ${cookie.vendor}`;
8991        displayNameContainer.appendChild(vendorSpan);
8992    }
8993
8994    // Add badge for custom/system cookies
8995    if (cookie.custom) {
8996        const customBadge = document.createElement('span');
8997        customBadge.classList.add('badge', 'bg-success', 'ms-2');
8998        customBadge.textContent = 'Custom';
8999        customBadge.style.fontSize = '0.7rem';
9000        displayNameContainer.appendChild(customBadge);
9001    } else {
9002        const systemBadge = document.createElement('span');
9003        systemBadge.classList.add('badge', 'bg-outline-secondary', 'text-secondary', 'border', 'border-secondary', 'ms-2');
9004        systemBadge.textContent = 'System';
9005        systemBadge.style.fontSize = '0.7rem';
9006        systemBadge.style.backgroundColor = 'transparent';
9007        displayNameContainer.appendChild(systemBadge);
9008    }
9009
9010    // Set by a response header (HttpOnly): the auto blocker can only refuse the request
9011    // that sets it; no script can read, veto or delete it once it is there.
9012    if (cookie.gateOnly) {
9013        const gateBadge = document.createElement('span');
9014        gateBadge.classList.add('badge', 'bg-warning', 'text-dark', 'ms-2');
9015        gateBadge.textContent = translations['serverSetCookie'] || 'Server-set';
9016        gateBadge.title = translations['serverSetCookieHint']
9017            || 'HttpOnly cookie set by a response header — blocked by refusing the request, not deletable by script';
9018        gateBadge.style.fontSize = '0.7rem';
9019        displayNameContainer.appendChild(gateBadge);
9020    }
9021
9022    displayNameCell.appendChild(displayNameContainer);
9023    row.appendChild(displayNameCell);
9024
9025    // Name cell (second column)
9026    const nameCell = document.createElement('td');
9027    nameCell.textContent = cookie.name || '';
9028    nameCell.classList.add('font-monospace', 'small');
9029    row.appendChild(nameCell);
9030
9031    // Type
9032    const typeCell = document.createElement('td');
9033    const typeSpan = document.createElement('span');
9034    typeSpan.textContent = cookie.type || 'unspecified';
9035    typeSpan.classList.add('badge', getTypeColor(cookie.type));
9036    typeCell.appendChild(typeSpan);
9037    row.appendChild(typeCell);
9038
9039    // Retention Period
9040    const retentionCell = document.createElement('td');
9041    retentionCell.textContent = cookie.retentionPeriod || 'Session';
9042    if (cookie.retentionPeriod === 'Session') {
9043        retentionCell.classList.add('text-warning');
9044    }
9045    row.appendChild(retentionCell);
9046
9047    // Actions
9048    const actionsCell = document.createElement('td');
9049    actionsCell.classList.add('text-nowrap');
9050
9051    // View/Details button
9052    const viewButton = document.createElement('button');
9053    viewButton.classList.add('btn', 'btn-sm', 'btn-outline-info', 'me-1');
9054    viewButton.innerHTML = '<i class="fa fa-eye"></i>';
9055    viewButton.title = 'View Details';
9056    viewButton.addEventListener('click', () => showCookieDetailsModal(cookie));
9057    actionsCell.appendChild(viewButton);
9058
9059    const editButton = document.createElement('button');
9060    editButton.classList.add('btn', 'btn-sm', 'btn-outline-primary', 'me-1');
9061    editButton.innerHTML = '<i class="fa fa-edit"></i>';
9062    editButton.title = 'Edit';
9063    editButton.addEventListener('click', () => showEditCookieModal(cookie, data, domainId));
9064    actionsCell.appendChild(editButton);
9065
9066    const deleteButton = document.createElement('button');
9067    deleteButton.classList.add('btn', 'btn-sm', 'btn-outline-danger');
9068    deleteButton.innerHTML = '<i class="fa fa-trash"></i>';
9069    deleteButton.title = translations['delete'] || 'Delete';
9070    deleteButton.addEventListener('click', () => deleteCookieConfirm(cookie, data, domainId));
9071    actionsCell.appendChild(deleteButton);
9072
9073    row.appendChild(actionsCell);
9074    return row;
9075}
9076
9077function getTypeColor(type) {
9078    switch (type?.toLowerCase()) {
9079        case 'essential': return 'bg-danger';
9080        case 'personalization': return 'bg-primary';
9081        case 'analytics': return 'bg-info';
9082        case 'marketing': return 'bg-warning';
9083        case 'security': return 'bg-dark';
9084        case 'other': return 'bg-secondary';
9085        default: return 'bg-light text-dark';
9086    }
9087}
9088
9089// Storage Type Helper Functions
9090const STORAGE_TYPES = {
9091    1: 'HTTP Cookie',
9092    2: 'Local Storage',
9093    3: 'Session Storage',
9094    5: 'Tracking Pixel'
9095};
9096
9097function getStorageTypeName(storageTypeId) {
9098    const id = parseInt(storageTypeId, 10);
9099    return STORAGE_TYPES[id] || 'Unknown';
9100}
9101
9102function getStorageTypeId(storageTypeName) {
9103    for (const [id, name] of Object.entries(STORAGE_TYPES)) {
9104        if (name === storageTypeName) {
9105            return parseInt(id, 10);
9106        }
9107    }
9108    return 1; // Default to HTTP Cookie
9109}
9110
9111function showCookieDetailsModal(cookie) {
9112    const modal = document.createElement('div');
9113    modal.classList.add('modal', 'fade');
9114    modal.tabIndex = -1;
9115
9116    // Get selected language shortCode for description display
9117    const languageDropdown = document.getElementById('select_language');
9118    let selectedLanguageCode = 'en_gb'; // default fallback to English
9119
9120    if (languageDropdown && languageDropdown.selectedOptions.length > 0) {
9121        const selectedOption = languageDropdown.selectedOptions[0];
9122        const selectedId = selectedOption.value;
9123
9124        // Try to get shortCode from data attribute first, then lookup by ID
9125        selectedLanguageCode = selectedOption.dataset.shortcode ||
9126                             selectedOption.getAttribute('data-shortcode') ||
9127                             getLanguageShortCodeById(selectedId) ||
9128                             'en_gb';
9129    }
9130
9131    console.log('Details modal showing description for language:', selectedLanguageCode);
9132
9133    // Get description for selected language with fallbacks
9134    let displayDescription = 'No description provided';
9135    if (cookie.description) {
9136        if (typeof cookie.description === 'object') {
9137            displayDescription = cookie.description[selectedLanguageCode] ||
9138                                cookie.description['en_gb'] ||
9139                                cookie.description['en'] ||
9140                                Object.values(cookie.description)[0] ||
9141                                'No description provided';
9142        } else {
9143            // Handle legacy string descriptions
9144            displayDescription = cookie.description;
9145        }
9146    }
9147
9148    // Convert numeric storage type to display name
9149    const storageTypeDisplay = getStorageTypeName(cookie.storageType);
9150
9151    modal.innerHTML = `
9152        <div class="modal-dialog modal-lg">
9153            <div class="modal-content">
9154                <div class="modal-header">
9155                    <h5 class="modal-title">
9156                        ${translations['cookieDetails'] || 'Cookie Details:'} ${cookie.name}
9157                    </h5>
9158                    <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
9159                </div>
9160                <div class="modal-body">
9161                    <div class="row">
9162                        <div class="col-md-6">
9163                            <h6 class="text-primary">${translations['basicInformation'] || 'Basic Information'}</h6>
9164                            <dl class="row">
9165                                <dt class="col-sm-5">${translations['displayName'] || 'Display Name'} :</dt>
9166                                <dd class="col-sm-7">
9166${cookie.displayName || 'N/A'}</dd>
9167                                <dt class="col-sm-5">${translations['name'] || 'Name'} :</dt>
9168                                <dd class="col-sm-7">${cookie.name || 'N/A'}</dd>
9169                                <dt class="col-sm-5">${translations['type'] || 'Type'} :</dt>
9170                                <dd class="col-sm-7">${cookie.type || 'N/A'}</dd>
9171                                <dt class="col-sm-5">${translations['retentionPeriod'] || 'Retention'} :</dt>
9172                                <dd class="col-sm-7">${cookie.retentionPeriod || 'Session'}</dd>
9173                                <dt class="col-sm-5">${translations['storageType'] || 'Storage Type'} :</dt>
9174                                <dd class="col-sm-7">${storageTypeDisplay}</dd>
9175                            </dl>
9176                        </div>
9177                        <div class="col-md-6">
9178                            <h6 class="text-primary">${translations['properties'] || 'Properties'}</h6>
9179                            <dl class="row">
9180                                <dt class="col-sm-3">${translations['custom'] || 'Custom'} :</dt>
9181                                <dd class="col-sm-9">${cookie.custom ? '✓ Yes' : '✗ No'}</dd>
9182                                ${cookie.gateOnly ? `<dt class="col-sm-3">${translations['serverSetCookie'] || 'Server-set'} :</dt>
9183                                <dd class="col-sm-9">${translations['serverSetCookieHint'] || 'HttpOnly cookie set by a response header — blocked by refusing the request, not deletable by script'}</dd>` : ''}
9184                                <dt class="col-sm-3">ID:</dt>
9185                                <dd class="col-sm-9"><small class="font-monospace">${cookie.id || 'N/A'}</small></dd>
9186                            </dl>
9187                        </div>
9188                    </div>
9189
9190                    <div class="row mt-3">
9191                        <div class="col-12">
9192                            <h6 class="text-primary">${translations['description'] || 'Description'} <span class="badge bg-success">${getLanguageNameByShortCode(selectedLanguageCode)}</span></h6>
9193                            <p class="bg-light p-3 rounded">${displayDescription}</p>
9194                            ${typeof cookie.description === 'object' ?
9195                                `<small class="text-muted mb-3 d-block">${translations['availableLanguages'] || 'Available languages'} : ${Object.keys(cookie.description).map(code => getLanguageNameByShortCode(code)).join(', ')}</small>` : ''}
9196                        </div>
9197                    </div>
9198
9199                    <div class="row">
9200                        <div class="col-md-6">
9201                            <h6 class="text-primary">${translations['vendorInformation'] || 'Vendor Information'}</h6>
9202                            <dl class="row">
9203                                <dt class="col-sm-5">${translations['vendor'] || 'Vendor'} :</dt>
9204                                <dd class="col-sm-7">${cookie.vendor || 'N/A'}</dd>
9205                                <dt class="col-sm-5">${translations['privacyUrl'] || 'Privacy URL'}:</dt>
9206                                <dd class="col-sm-7">
9207                                    ${cookie.privacyUrl ?
9208                                        `<a href="${cookie.privacyUrl}" target="_blank" class="btn btn-sm btn-outline-primary">
9209                                            <i class="fas fa-external-link-alt me-1"></i>${translations['viewPolicy'] || 'View Policy'}
9210                                        </a>` : 'N/A'}
9211                                </dd>
9212                            </dl>
9213                        </div>
9214                    </div>
9215
9216                    ${cookie.script ? `
9217                        <div class="row mt-3">
9218                            <div class="col-12">
9219                                <h6 class="text-primary">${translations['associatedScript'] || 'Associated Script'}</h6>
9220                                <pre class="bg-dark text-light p-3 rounded"><code>${cookie.script}</code></pre>
9221                            </div>
9222                        </div>
9223                    ` : ''}
9224                </div>
9225                <div class="modal-footer">
9226                    <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">${translations['close'] || 'Close'}</button>
9227                    <button type="button" class="btn btn-primary" onclick="showEditCookieModal(${JSON.stringify(cookie).replace(/"/g, '&quot;')}, arguments[1], arguments[2])" data-bs-dismiss="modal">
9228                        <i class="fas fa-edit me-2"></i>${translations['editCookie'] || 'Edit Cookie'}
9229                    </button>
9230                </div>
9231            </div>
9232        </div>
9233    `;
9234
9235    document.body.appendChild(modal);
9236    const bsModal = new bootstrap.Modal(modal);
9237    bsModal.show();
9238
9239    modal.addEventListener('hidden.bs.modal', () => {
9240        document.body.removeChild(modal);
9241    });
9242}
9243
9244function renderCookieTable(cookies, data, domainId, sortKey = null, sortDirection = 'asc') {
9245    const tbody = document.getElementById('cookiesTableBody');
9246    tbody.innerHTML = '';
9247
9248    let sortedCookies = [...cookies];
9249
9250    if (sortKey) {
9251        sortedCookies.sort((a, b) => {
9252            let aVal = a[sortKey];
9253            let bVal = b[sortKey];
9254
9255            // Handle boolean values
9256            if (typeof aVal === 'boolean') {
9257                aVal = aVal ? 1 : 0;
9258                bVal = bVal ? 1 : 0;
9259            }
9260
9261            // Handle string values
9262            if (typeof aVal === 'string') {
9263                aVal = aVal.toLowerCase();
9264                bVal = bVal ? bVal.toLowerCase() : '';
9265            }
9266
9267            if (aVal < bVal) return sortDirection === 'asc' ? -1 : 1;
9268            if (aVal > bVal) return sortDirection === 'asc' ? 1 : -1;
9269            return 0;
9270        });
9271    }
9272
9273    if (sortedCookies.length === 0) {
9274        const row = document.createElement('tr');
9275        const cell = document.createElement('td');
9276        cell.colSpan = 5; // Updated column count
9277        cell.classList.add('text-center', 'text-muted', 'py-5');
9278        cell.innerHTML = `
9279            <div class="d-flex flex-column align-items-center">
9280                <i class="fas fa-cookie-bite fa-3x mb-3 text-muted"></i>
9281                <h5>${translations['noCookiesFound'] || 'No cookies found'}</h5>
9282                <p class="mb-3">${translations['addCookieNote'] || 'Click "Add Cookie" to create your first cookie entry.'} </p>
9283                <button class="btn btn-primary" onclick="showAddCookieModal(arguments[0], arguments[1])">
9284                    <i class="fa fa-plus me-2"></i>${translations['addCookie'] || 'Add Cookie'}
9285                </button>
9286            </div>
9287        `;
9288        row.appendChild(cell);
9289        tbody.appendChild(row);
9290    } else {
9291        sortedCookies.forEach(cookie => {
9292            const row = createEnhancedCookieRow(cookie, data, domainId);
9293            tbody.appendChild(row);
9294        });
9295    }
9296}
9297
9298function filterAndSearch(allCookies, data, domainId) {
9299    const searchInput = document.querySelector('#cookie_manager_container input[placeholder="Search cookies..."]');
9300    const filterSelect = document.querySelector('#cookie_manager_container select');
9301
9302    if (!searchInput || !filterSelect) {
9303        console.error('Search input or filter select not found');
9304        return;
9305    }
9306
9307    const searchTerm = searchInput.value.toLowerCase();
9308    const filterType = filterSelect.value;
9309
9310    let filteredCookies = allCookies.filter(cookie => {
9311        // Type filter
9312        if (filterType !== 'all' && cookie.type !== filterType) {
9313            return false;
9314        }
9315
9316        // Search filter - Updated search fields with multilingual description support
9317        if (searchTerm) {
9318            const searchFields = [
9319                cookie.name,
9320                cookie.displayName,
9321                cookie.vendor,
9322                cookie.type
9323            ].map(field => (field || '').toLowerCase());
9324
9325            // Add description search (check all language versions)
9326            if (cookie.description) {
9327                if (typeof cookie.description === 'object') {
9328                    // Search all language versions
9329                    Object.values(cookie.description).forEach(desc => {
9330                        if (desc) searchFields.push(desc.toLowerCase());
9331                    });
9332                } else {
9333                    // Handle legacy string descriptions
9334                    searchFields.push(cookie.description.toLowerCase());
9335                }
9336            }
9337
9338            return searchFields.some(field => field.includes(searchTerm));
9339        }
9340
9341        return true;
9342    });
9343
9344    renderCookieTable(filteredCookies, data, domainId);
9345}
9346
9347let currentSort = { key: null, direction: 'asc' };
9348
9349function sortTable(key) {
9350    const direction = currentSort.key === key && currentSort.direction === 'asc' ? 'desc' : 'asc';
9351    currentSort = { key, direction };
9352
9353    // Update sort indicators
9354    document.querySelectorAll('th i.fa-sort, th i.fa-sort-up, th i.fa-sort-down').forEach(icon => {
9355        icon.className = 'fas fa-sort text-muted ms-1';
9356    });
9357
9358    const header = document.querySelector(`th:nth-child(${getColumnIndex(key) + 1}) i:last-child`);
9359    if (header) {
9360        header.className = `fas fa-sort-${direction === 'asc' ? 'up' : 'down'} text-primary ms-1`;
9361    }
9362
9363    // Re-render with sorting - Fixed selectors
9364    const searchInput = document.querySelector('#cookie_manager_container input[placeholder="Search cookies..."]');
9365    const filterSelect = document.querySelector('#cookie_manager_container select');
9366
9367    // Get current filtered cookies
9368    const allCookies = window.currentCookies || [];
9369    let filteredCookies = allCookies.filter(cookie => {
9370        const filterType = filterSelect ? filterSelect.value : 'all';
9371        const searchTerm = searchInput ? searchInput.value.toLowerCase() : '';
9372
9373        if (filterType !== 'all' && cookie.type !== filterType) {
9374            return false;
9375        }
9376
9377        if (searchTerm) {
9378            const searchFields = [
9379                cookie.name,
9380                cookie.displayName,
9381                cookie.vendor,
9382                cookie.type
9383            ].map(field => (field || '').toLowerCase());
9384
9385            // Add description search (check all language versions)
9386            if (cookie.description) {
9387                if (typeof cookie.description === 'object') {
9388                    // Search all language versions
9389                    Object.values(cookie.description).forEach(desc => {
9390                        if (desc) searchFields.push(desc.toLowerCase());
9391                    });
9392                } else {
9393                    // Handle legacy string descriptions
9394                    searchFields.push(cookie.description.toLowerCase());
9395                }
9396            }
9397
9398            return searchFields.some(field => field.includes(searchTerm));
9399        }
9400
9401        return true;
9402    });
9403
9404    renderCookieTable(filteredCookies, window.currentData, window.currentDomainId, key, direction);
9405}
9406
9407function getColumnIndex(key) {
9408    const columns = ['displayName', 'name', 'type', 'retentionPeriod', 'actions'];
9409    return columns.indexOf(key);
9410}
9411
9412function showImportModal(data, domainId) {
9413    const modal = document.createElement('div');
9414    modal.classList.add('modal', 'fade');
9415    modal.tabIndex = -1;
9416
9417    modal.innerHTML = `
9418        <div class="modal-dialog">
9419            <div class="modal-content">
9420                <div class="modal-header">
9421                    <h5 class="modal-title">
9422                        <i class="fa fa-upload me-2"></i>Import Cookies from JSON
9423                    </h5>
9424                    <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
9425                </div>
9426                <div class="modal-body">
9427                    <div id="importErrors" class="alert alert-danger d-none"></div>
9428                    <div id="importWarnings" class="alert alert-warning d-none"></div>
9429                    <div class="mb-3">
9430                        <label for="importFile" class="form-label">Select JSON file</label>
9431                        <input type="file" class="form-control" id="importFile" accept=".json">
9432                        <div class="form-text">Choose a JSON file exported from the cookie manager</div>
9433                    </div>
9434                    <div class="mb-3">
9435                        <div class="form-check">
9436                            <input class="form-check-input" type="checkbox" id="replaceExisting" checked>
9437                            <label class="form-check-label" for="replaceExisting">
9438                                Replace existing cookies with same name
9439                            </label>
9440                        </div>
9441                    </div>
9442                    <div id="importPreview" class="d-none">
9443                        <h6>Preview:</h6>
9444                        <div id="previewContent" class="bg-light p-3 rounded">
9445                            <small class="text-muted">File preview will appear here...</small>
9446                        </div>
9447                    </div>
9448                </div>
9449                <div class="modal-footer">
9450                    <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
9451                    <button type="button" class="btn btn-primary" id="importBtn" disabled>
9452                        <i class="fa fa-upload me-2"></i>Import Cookies
9453                    </button>
9454                </div>
9455            </div>
9456        </div>
9457    `;
9458
9459    // Add import functionality
9460    const fileInput = modal.querySelector('#importFile');
9461    const importBtn = modal.querySelector('#importBtn');
9462    const errorContainer = modal.querySelector('#importErrors');
9463    const warningContainer = modal.querySelector('#importWarnings');
9464    const previewContainer = modal.querySelector('#importPreview');
9465    const previewContent = modal.querySelector('#previewContent');
9466    const replaceCheckbox = modal.querySelector('#replaceExisting');
9467
9468    let importData = null;
9469
9470    fileInput.addEventListener('change', (event) => {
9471        const file = event.target.files[0];
9472        if (!file) {
9473            importBtn.disabled = true;
9474            previewContainer.classList.add('d-none');
9475            return;
9476        }
9477
9478        if (!file.name.endsWith('.json')) {
9479            showImportError('Please select a valid JSON file');
9480            return;
9481        }
9482
9483        const reader = new FileReader();
9484        reader.onload = (e) => {
9485            try {
9486                importData = JSON.parse(e.target.result);
9487
9488                if (!Array.isArray(importData)) {
9489                    showImportError('Invalid format: JSON must contain an array of cookies');
9490                    return;
9491                }
9492
9493                // Validate cookie structure
9494                const validCookies = importData.filter(cookie => {
9495                    return cookie.name && cookie.name.trim() !== '' &&
9496                           cookie.type && cookie.type.trim() !== '';
9497                });
9498
9499                const invalidCount = importData.length - validCookies.length;
9500
9501                if (validCookies.length === 0) {
9502                    showImportError('No valid cookies found in the file. Cookies must have name and type.');
9503                    return;
9504                }
9505
9506                if (invalidCount > 0) {
9507                    const warningMessage = `Found ${invalidCount}
9507 invalid cookie${invalidCount > 1 ? 's' : ''} that will be skipped. Valid cookies: ${validCookies.length}`;
9508                    showImportWarning(warningMessage);
9509                }
9510
9511                // Show preview
9512                hideImportError();
9513                showPreview(validCookies, invalidCount);
9514                importBtn.disabled = false;
9515
9516                // Store only valid cookies for import
9517                importData = validCookies;
9518
9519            } catch (error) {
9520                showImportError('Invalid JSON file: ' + error.message);
9521            }
9522        };
9523
9524        reader.readAsText(file);
9525    });
9526
9527    importBtn.addEventListener('click', async () => {
9528        if (!importData) return;
9529
9530        try {
9531            importBtn.disabled = true;
9532            importBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Importing...';
9533
9534            await importCookies(importData, data, domainId, replaceCheckbox.checked);
9535
9536            bootstrap.Modal.getInstance(modal).hide();
9537            displayAlertCookie(`Successfully imported ${importData.length} valid cookies!`, 'success');
9538
9539            // Reload the cookie manager
9540            loadCookiesForDomain(domainId);
9541
9542        } catch (error) {
9543            console.error('Import error:', error);
9544            showImportError('Error importing cookies: ' + error.message);
9545            importBtn.disabled = false;
9546            importBtn.innerHTML = '<i class="fa fa-upload me-2"></i>Import Cookies';
9547        }
9548    });
9549
9550    function showImportError(message) {
9551        errorContainer.textContent = message;
9552        errorContainer.classList.remove('d-none');
9553        warningContainer.classList.add('d-none');
9554        previewContainer.classList.add('d-none');
9555        importBtn.disabled = true;
9556    }
9557
9558    function showImportWarning(message) {
9559        warningContainer.textContent = message;
9560        warningContainer.classList.remove('d-none');
9561        errorContainer.classList.add('d-none');
9562    }
9563
9564    function hideImportError() {
9565        errorContainer.classList.add('d-none');
9566        warningContainer.classList.add('d-none');
9567    }
9568
9569    function showPreview(cookies, invalidCount = 0) {
9570        const typeCount = {};
9571        cookies.forEach(cookie => {
9572            typeCount[cookie.type] = (typeCount[cookie.type] || 0) + 1;
9573        });
9574
9575        const previewHtml = `
9576            <div><strong>Found ${cookies.length} valid cookies to import:</strong></div>
9577            ${invalidCount > 0 ? `<div class="text-warning"><small>${invalidCount} invalid cookie${invalidCount > 1 ? 's' : ''} will be skipped</small></div>` : ''}
9578            <ul class="mt-2 mb-0">
9579                ${Object.entries(typeCount).map(([type, count]) =>
9580                    `<li>${count} ${type} cookie${count > 1 ? 's' : ''}</li>`
9581                ).join('')}
9582            </ul>
9583        `;
9584
9585        previewContent.innerHTML = previewHtml;
9586        previewContainer.classList.remove('d-none');
9587    }
9588
9589    document.body.appendChild(modal);
9590    const bsModal = new bootstrap.Modal(modal);
9591    bsModal.show();
9592
9593    modal.addEventListener('hidden.bs.modal', () => {
9594        document.body.removeChild(modal);
9595    });
9596}
9597
9598async function importCookies(cookiesData, data, domainId, replaceExisting = true) {
9599    // Get current cookies from the backend first
9600    const loadData = {
9601        sid: getSid(),
9602        domainId: parseInt(domainId)
9603    };
9604
9605    const loadResponse = await postRequest(endpoint + '/cmp/page-cookies', loadData);
9606
9607    if (!loadResponse.ok) {
9608        throw new Error('Failed to load current cookies: ' + loadResponse.status);
9609    }
9610
9611    const loadResponseBody = await loadResponse.json();
9612    if (loadResponseBody.statusCode !== 0) {
9613        throw new Error('Error loading current cookies: ' + (loadResponseBody.statusMessage || 'Unknown error'));
9614    }
9615
9616    // Get current resources
9617    let resources = loadResponseBody.resources || [];
9618
9619    // Process imported cookies
9620    const processedCookies = cookiesData.map(cookie => {
9621        // Ensure required fields and set defaults
9622        const processedCookie = {
9623            name: cookie.name,
9624            displayName: cookie.displayName || cookie.name,
9625            type: cookie.type || 'other',
9626            retentionPeriod: cookie.retentionPeriod || 'Session',
9627            custom: true, // Mark imported cookies as custom
9628            description: cookie.description || {},
9629            vendor: cookie.vendor || '',
9630            privacyUrl: cookie.privacyUrl || '',
9631            script: cookie.script || '',
9632            id: generateCookieId() // Generate new ID
9633        };
9634
9635        return processedCookie;
9636    });
9637
9638    // Handle replacement or addition
9639    if (replaceExisting) {
9640        // Remove existing cookies with same names
9641        const importedNames = processedCookies.map(c => c.name);
9642        resources = resources.filter(r => !importedNames.includes(r.name));
9643    }
9644
9645    // Add imported cookies
9646    resources.push(...processedCookies);
9647
9648    // Save updated resources
9649    await saveCookieResources(resources, { value: { resources } }, domainId);
9650}
9651
9652function exportCookies(cookies) {
9653    const exportData = cookies.map(cookie => ({
9654        name: cookie.name,
9655        displayName: cookie.displayName,
9656        type: cookie.type,
9657        retentionPeriod: cookie.retentionPeriod,
9658        custom: cookie.custom,
9659        description: cookie.description,
9660        vendor: cookie.vendor,
9661        privacyUrl: cookie.privacyUrl,
9662        script: cookie.script,
9663        id: cookie.id
9664    }));
9665
9666    const jsonString = JSON.stringify(exportData, null, 2);
9667    const blob = new Blob([jsonString], { type: 'application/json' });
9668    const url = URL.createObjectURL(blob);
9669
9670    const link = document.createElement('a');
9671    link.href = url;
9672    link.download = `cookies-export-${new Date().toISOString().split('T')[0]}.json`;
9673    document.body.appendChild(link);
9674    link.click();
9675    document.body.removeChild(link);
9676    URL.revokeObjectURL(url);
9677
9678    displayAlertCookie('Cookies exported successfully!', 'success');
9679}
9680
9681async function addCookie(cookieData, data, domainId) {
9682    try {
9683        console.log('addCookie called with:');
9684        console.log('- cookieData:', cookieData);
9685        console.log('- domainId:', domainId);
9686        console.log('- data:', data);
9687
9688        // Validate domainId
9689        if (!domainId || domainId === 'null' || domainId === null) {
9690            console.error('Invalid domainId:', domainId);
9691
9692            // Try to get domainId from dropdown
9693            const domainDropdown = document.getElementById('select_domain');
9694            if (domainDropdown && domainDropdown.value) {
9695                domainId = domainDropdown.value;
9696                console.log('Retrieved domainId from dropdown:', domainId);
9697            } else {
9698                displayAlertCookie('Error: No domain selected. Please select a domain first.', 'error');
9699                return;
9700            }
9701        }
9702
9703        // Get current cookies from the backend first
9704        const loadData = {
9705            sid: getSid(),
9706            domainId: parseInt(domainId)
9707        };
9708
9709        const loadResponse = await postRequest(endpoint + '/cmp/page-cookies', loadData);
9710
9711        if (!loadResponse.ok) {
9712            throw new Error('Failed to load current cookies: ' + loadResponse.status);
9713        }
9714
9715        const loadResponseBody = await loadResponse.json();
9716        if (loadResponseBody.statusCode !== 0) {
9717            throw new Error('Error loading current cookies: ' + (loadResponseBody.statusMessage || 'Unknown error'));
9718        }
9719
9720        // Get current resources
9721        let resources = loadResponseBody.resources || [];
9722
9723        // Add new cookie
9724        resources.push(cookieData);
9725
9726        // Save updated resources
9727        await saveCookieResources(resources, { value: { resources } }, domainId);
9728        displayAlertCookie(translations['cookieAddedSuccessfully'] || 'Cookie added successfully!', 'success');
9729
9730        // Reload the cookie manager
9731        loadCookiesForDomain(domainId);
9732    } catch (error) {
9733        console.error('Error adding cookie:', error);
9734        displayAlertCookie('Error adding cookie: ' + error.message, 'error');
9735    }
9736}
9737
9738async function updateCookie(cookieData, data, domainId, originalCookie) {
9739    try {
9740        console.log('updateCookie called with:');
9741        console.log('- cookieData:', cookieData);
9742        console.log('- domainId:', domainId);
9743        console.log('- originalCookie:', originalCookie);
9744
9745        // Validate domainId
9746        if (!domainId || domainId === 'null' || domainId === null) {
9747            console.error('Invalid domainId:', domainId);
9748
9749            // Try to get domainId from dropdown
9750            const domainDropdown = document.getElementById('select_domain');
9751            if (domainDropdown && domainDropdown.value) {
9752                domainId = domainDropdown.value;
9753                console.log('Retrieved domainId from dropdown:', domainId);
9754            } else {
9755                displayAlertCookie('Error: No domain selected. Please select a domain first.', 'error');
9756                return;
9757            }
9758        }
9759
9760        // Get current cookies from the backend first
9761        const loadData = {
9762            sid: getSid(),
9763            domainId: parseInt(domainId)
9764        };
9765
9766        const loadResponse = await postRequest(endpoint + '/cmp/page-cookies', loadData);
9767
9768        if (!loadResponse.ok) {
9769            throw new Error('Failed to load current cookies: ' + loadResponse.status);
9770        }
9771
9772        const loadResponseBody = await loadResponse.json();
9773        if (loadResponseBody.statusCode !== 0) {
9774            throw new Error('Error loading current cookies: ' + (loadResponseBody.statusMessage || 'Unknown error'));
9775        }
9776
9777        // Get current resources
9778        let resources = loadResponseBody.resources || [];
9779
9780        // Find and update the cookie
9781        const index = resources.findIndex(r =>
9782            r.id === originalCookie.id ||
9783            (r.name === originalCookie.name && r.displayName === originalCookie.displayName)
9784        );
9785
9786        if (index !== -1) {
9787            resources[index] = { ...resources[index], ...cookieData };
9788        } else {
9789            resources.push(cookieData);
9790        }
9791
9792        // Save updated resources
9793        await saveCookieResources(resources, { value: { resources } }, domainId);
9794        displayAlertCookie(translations['cookieUpdatedSuccessfully'] || 'Cookie updated successfully!', 'success');
9795
9796        // Reload the cookie manager
9797        loadCookiesForDomain(domainId);
9798    } catch (error) {
9799        console.error('Error updating cookie:', error);
9800        displayAlertCookie('Error updating cookie: ' + error.message, 'error');
9801    }
9802}
9803
9804async function cookieManagerDeleteCookie(cookie, data, domainId) {
9805    try {
9806        // Get current cookies from the backend first
9807        const loadData = {
9808            sid: getSid(),
9809            domainId: parseInt(domainId)
9810        };
9811
9812        const loadResponse = await postRequest(endpoint + '/cmp/page-cookies', loadData);
9813
9814        if (!loadResponse.ok) {
9815            throw new Error('Failed to load current cookies: ' + loadResponse.status);
9816        }
9817
9818        const loadResponseBody = await loadResponse.json();
9819        if (loadResponseBody.statusCode !== 0) {
9820            throw new Error('Error loading current cookies: ' + (loadResponseBody.statusMessage || 'Unknown error'));
9821        }
9822
9823        // Get current resources
9824        let resources = loadResponseBody.resources || [];
9825
9826        // Remove the cookie
9827        const index = resources.findIndex(r => r.id === cookie.id ||
9828            (r.name === cookie.name && r.displayName === cookie.displayName));
9829
9830        if (index !== -1) {
9831            resources.splice(index, 1);
9832        }
9833
9834        // Save updated resources
9835        await saveCookieResources(resources, { value: { resources } }, domainId);
9836        displayAlertCookie(translations['cookieDeletedSuccessfully'] || 'Cookie deleted successfully!', 'success');
9837
9838        // Reload the cookie manager
9839        loadCookiesForDomain(domainId);
9840    } catch (error) {
9841        console.error('Error deleting cookie:', error);
9842        displayAlertCookie('Error deleting cookie: ' + error.message, 'error');
9843    }
9844}
9845
9846async function deleteCookieConfirm(cookie, data, domainId) {
9847    if (await showConfirm((translations['confirmDeleteCookie'] || 'Are you sure you want to delete this cookie?') + '\n' +
9848                (translations['cookieName'] || 'Cookie Name') + ': ' + cookie.name, translations['delete'] || 'Delete')) {
9849		cookieManagerDeleteCookie(cookie, data, domainId);
9850    }
9851}
9852
9853async function saveCookieResources(resources, originalData, domainId) {
9854    console.log('saveCookieResources called with domainId:', domainId);
9855
9856    // Validate domainId
9857    if (!domainId || domainId === 'null' || domainId === null) {
9858        throw new Error('Invalid domain ID: ' + domainId);
9859    }
9860
9861    const saveData = {
9862        sid: getSid(),
9863        domainId: parseInt(domainId), // Use domainId directly
9864        value: {
9865            resources: resources,
9866            lastUpdated: new Date().getTime()
9867        }
9868    };
9869
9870    console.log('Saving cookie data:', JSON.stringify(saveData, null, 2));
9871
9872    const response = await postRequest(endpoint + '/cmp/page-cookies', saveData);
9873
9874    if (!response.ok) {
9875        throw new Error('Failed to save cookie data: ' + response.status);
9876    }
9877
9878    const responseBody = await response.json();
9879    if (responseBody.statusCode !== 0) {
9880        throw new Error('Error saving cookie data: ' + (responseBody.statusMessage || 'Unknown error'));
9881    }
9882
9883    return responseBody;
9884}
9885
9886async function loadCookiesForDomain(domainId) {
9887    const container = document.getElementById('cookie_manager_container');
9888    container.innerHTML = '<div class="text-center py-5"><div class="spinner-border text-primary" role="status"><span class="visually-hidden">Loading...</span></div><p class="mt-3 text-muted">Loading cookies...</p></div>';
9889
9890    try {
9891        const data = {
9892            sid: getSid(),
9893            domainId: parseInt(domainId) // Use domainId directly for loading (no value parameter)
9894        };
9895
9896        const response = await postRequest(endpoint + '/cmp/page-cookies', data);
9897
9898        if (response.ok) {
9899            const responseBody = await response.json();
9900
9901            if (responseBody.statusCode === 0) {
9902                // Backend returns cookies in "resources" field directly
9903                const cookies = responseBody.resources || [];
9904
9905                // Create the data structure expected by the frontend
9906                const formattedResponse = {
9907                    value: {
9908                        resources: cookies
9909                    }
9910                };
9911
9912                // Store for global access
9913                window.currentCookies = cookies.filter(resource => {
9914                    if (!resource) return false;
9915                    const cookieTypes = ['Essential', 'Personalization', 'Analytics', 'Marketing', 'Security', 'Other'];
9916                    return !resource.type ||
9917                           resource.type === 'cookie' ||
9918                           cookieTypes.includes(resource.type) ||
9919                           (resource.name && resource.displayName);
9920                });
9921
9922                window.currentData = formattedResponse;
9923                window.currentDomainId = domainId; // Changed from currentProjectId to currentDomainId
9924
9925                displayEnhancedCookieManager(formattedResponse, domainId);
9926            } else {
9927                console.error('Error loading cookies:', responseBody.statusMessage);
9928                displayAlertCookie('Failed to load cookies: ' + responseBody.statusMessage, 'error');
9929            }
9930        } else {
9931            console.error('HTTP Error: ' + response.status + ' - ' + response.statusText);
9932            displayAlertCookie('Failed to load cookies: ' + response.statusText, 'error');
9933        }
9934    } catch (error) {
9935        console.error('Error loading cookies:', error);
9936        displayAlertCookie('Error loading cookies: ' + error.message, 'error');
9937    }
9938}
9939
9940function generateCookieId() {
9941    return Date.now() + Math.floor(Math.random() * 1000);
9942}
9943
9944function displayAlertCookie(message, type = 'info') {
9945    // Toast, not an inline alert (see displayAlertDocument). Cookie-manager messages
9946    // carry markup, so they render as HTML like before.
9947    showToast(message, type === 'error' ? 'error' : type === 'success' ? 'success' : 'info', { html: true });
9948}
9949
9950// Debug function to test modal form elements and domainId
9951function debugModalForm() {
9952    const modal = document.querySelector('.modal');
9953    if (!modal) {
9954        console.log('No modal found');
9955        return;
9956    }
9957
9958    const domainId = modal.getAttribute('data-domain-id');
9959    const domainDropdown = document.getElementById('select_domain');
9960    const resourceNameInput = modal.querySelector('#cookieResourceName');
9961    const typeSelect = modal.querySelector('#cookieType');
9962
9963    console.log('=== MODAL DEBUG INFO ===');
9964    console.log('Modal found:', !!modal);
9965    console.log('DomainId from modal:', domainId);
9966    console.log('DomainId from dropdown:', domainDropdown ? domainDropdown.value : 'Dropdown not found');
9967    console.log('Resource name input:', resourceNameInput);
9968    console.log('Resource name value:', resourceNameInput ? `"${resourceNameInput.value}"` : 'N/A');
9969    console.log('Type select:', typeSelect);
9970    console.log('Type value:', typeSelect ? `"${typeSelect.value}"` : 'N/A');
9971    console.log('Type options:', typeSelect ? Array.from(typeSelect.options).map(o => `${o.value}: ${o.text}`) : 'N/A');
9972
9973    // List all form inputs in the modal
9974    const allInputs = modal.querySelectorAll('input, select, textarea');
9975    console.log('All form elements in modal:');
9976    allInputs.forEach((input, index) => {
9977        console.log(`${index}: ${input.tagName} - ID: ${input.id} - Value: "${input.value}"`);
9978    });
9979
9980    console.log('=== END DEBUG INFO ===');
9981}
9982
9983
9984
9985// Cookie Scanner
9986
9987function showHideIpList(){
9988    showIpModal();
9989}
9990
9991function showIpModal() {
9992     // Create modal element
9993     const modal = document.createElement('div');
9994     modal.classList.add('modal', 'fade');
9995     modal.tabIndex = -1;
9996     modal.id = 'ipListModal';
9997
9998     modal.innerHTML = `
9999         <div class="modal-dialog modal-lg">
10000             <div class="modal-content">
10001                 <div class="modal-header">
10002                     <h5 class="modal-title">
10003                         <i class="fas fa-server me-2"></i>${translations['ipListTitle'] || 'Cookie Scanner IP Addresses'}
10004                     </h5>
10005                     <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="${translations['close'] || 'Close'}"></button>
10006                 </div>
10007                 <div class="modal-body">
10008                     <div class="mb-3">
10009                         <p class="text-muted">
10010                             ${translations['ipListDescription'] || 'Please add these IP addresses to your firewall whitelist to allow our cookie scanner to access your website:'}
10011                         </p>
10012                     </div>
10013                     <div class="mb-3">
10014                         <label for="ipListTextarea" class="form-label">
10015                             <i class="fas fa-list me-1"></i>${translations['ipAddresses'] || 'IP Addresses'}
10016                         </label>
10017                         <textarea
10018                             class="form-control font-monospace"
10019                             id="ipListTextarea"
10020                             rows="12"
10021                             readonly
10022                             style="font-size: 0.9em; line-height: 1.4;"
10023                         > 79.137.17.189
10024 178.32.101.196
10025 51.254.152.157
10026 188.165.5.60
10027 87.98.166.59
10028 176.31.11.245
10029 94.23.146.169
10030 217.182.28.236
10031 188.165.30.212
10032 178.32.43.167
10033 188.165.128.184
10034 79.137.17.188
10035 164.132.128.233
10036 178.32.102.33
10037 164.132.128.232
10038 54.37.60.131
10039 94.23.120.181
10040 51.254.238.200
10041 178.32.40.154
10042 79.137.17.187
10043 51.210.174.175
10044 147.135.189.8
10045 178.33.37.237
10046 51.38.17.124
10047 137.74.105.15
10048 137.74.105.12
10049 193.70.78.212
10050 46.105.228.180
10051 188.165.164.148
10052 149.202.132.28
10053 46.105.246.246
10054 79.137.24.177
10055 217.182.6.61
10056 178.32.102.74
10057 51.68.74.30
10058 79.137.17.191
10059 79.137.17.185
10060 94.23.120.174
10061 94.23.120.178
10062 176.31.9.177
10063 137.74.105.13
10064 46.105.246.245
10065 141.94.41.69
10066 164.132.88.84
10067 51.210.238.3
10068 5.135.237.225
10069 5.135.20.151
10070 91.134.170.84</textarea>
10071                     </div>
10072                     <div class="d-flex gap-2">
10073                         <button type="button" class="btn btn-primary" id="copyIpListBtn">
10074                             <i class="fas fa-copy me-2"></i>${translations['copyToClipboard'] || 'Copy to Clipboard'}
10075                         </button>
10076
10077                     <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
10078                         <i class="fas fa-times me-2"></i>${translations['close'] || 'Close'}
10079                     </button>
10080                 </div>
10081             </div>
10082         </div>
10083     `;
10084
10085     // Append modal to body
10086     document.body.appendChild(modal);
10087
10088     // Initialize Bootstrap modal
10089     const bsModal = new bootstrap.Modal(modal);
10090
10091     // Add event listeners after modal is shown
10092     modal.addEventListener('shown.bs.modal', () => {
10093         const copyBtn = document.getElementById('copyIpListBtn');
10094         const textarea = document.getElementById('ipListTextarea');
10095
10096         // Copy to clipboard functionality
10097         copyBtn.addEventListener('click', async () => {
10098                     try {
10099                         // Always copy the full textarea value, regardless of selection
10100                         await navigator.clipboard.writeText(textarea.value);
10101
10102                         // Show success feedback
10103                         const originalText = copyBtn.innerHTML;
10104                         copyBtn.innerHTML = '<i class="fas fa-check me-2"></i>' + (translations['copied'] || 'Copied!');
10105                         copyBtn.classList.remove('btn-primary');
10106                         copyBtn.classList.add('btn-success');
10107
10108                         setTimeout(() => {
10109                             copyBtn.innerHTML = originalText;
10110                             copyBtn.classList.remove('btn-success');
10111                             copyBtn.classList.add('btn-primary');
10112                         }, 2000);
10113
10114                     } catch (err) {
10115                         // Fallback for older browsers - select all then copy
10116                         textarea.select();
10117                         document.execCommand('copy');
10118
10119                         // Show success feedback
10120                         const originalText = copyBtn.innerHTML;
10121                         copyBtn.innerHTML = '<i class="fas fa-check me-2"></i>' + (translations['copied'] || 'Copied!');
10122                         copyBtn.classList.remove('btn-primary');
10123                         copyBtn.classList.add('btn-success');
10124
10125                         setTimeout(() => {
10126                             copyBtn.innerHTML = originalText;
10127                             copyBtn.classList.remove('btn-success');
10128                             copyBtn.classList.add('btn-primary');
10129                         }, 2000);
10130                     }
10131                 });
10132     });
10133
10134     // Show the modal
10135     bsModal.show();
10136
10137     // Clean up modal when closed
10138     modal.addEventListener('hidden.bs.modal', () => {
10139         document.body.removeChild(modal);
10140     });
10141 }
10142
10143function createCookieScannerSection(domainId) {
10144    const scannerContainer = document.createElement('div');
10145    scannerContainer.classList.add('card', 'mb-4');
10146    scannerContainer.id = 'cookie_scanner_section';
10147
10148    scannerContainer.innerHTML = `
10149        <div class="card-header">
10150            <h5 class="card-title mb-0">
10151                <i class="fas fa-search me-2"></i>${translations['cookieScanner'] || 'Cookie Scanner'}
10152            </h5>
10153        </div>
10154        <div class="card-body">
10155            <div class="row align-items-center">
10156                <div class="col-md-8">
10157                    <h6 class="text-primary mb-3">
10158                        <i class="fas fa-robot me-2"></i>${translations['automaticDetection'] || 'Automatic Cookie Detection'}
10159                    </h6>
10160                    <p class="text-muted mb-3">
10161                        ${translations['cookieScannerDescription'] || 'Scan your website to automatically detect cookies and update your cookie list. Our advanced scanner analyzes your web pages and identifies all cookies being used, including tracking pixels, analytics cookies, and third-party integrations.'}
10162                    </p>
10163                    <div class="mb-2">
10164                        <h6 class="text-success mb-2">
10165                            <i class="fas fa-check-circle me-2"></i>${translations['scanFeatures'] || 'What Our Scanner Detects:'}
10166                        </h6>
10167                        <ul class="list-unstyled mb-0">
10168                            <li class="mb-1">
10169                                <i class="fas fa-cookie-bite text-primary me-2"></i>
10170                                ${translations['feature1'] || 'Essential and functional cookies'}
10171                            </li>
10172                            <li class="mb-1">
10173                                <i class="fas fa-chart-line text-info me-2"></i>
10174                                ${translations['feature2'] || 'Analytics and performance tracking'}
10175                            </li>
10176                            <li class="mb-1">
10177                                <i class="fas fa-bullhorn text-warning me-2"></i>
10178                                ${translations['feature3'] || 'Marketing and advertising cookies'}
10179                            </li>
10180                            <li class="mb-1">
10181                                <i class="fas fa-user-cog text-secondary me-2"></i>
10182                                ${translations['feature4'] || 'Personalization and preference cookies'}
10183                            </li>
10184                            <li class="mb-0">
10185                                <i class="fas fa-puzzle-piece text-success me-2"></i>
10186                                ${translations['feature5'] || 'Third-party integrations and plugins'}
10187                            </li>
10188                        </ul>
10189                    </div>
10190                </div>
10191                <div class="col-md-4">
10192                    <div class="text-center">
10193                        <div class="mb-4">
10194                        <th><i class="fas fa-cogs me-1"></i><strong>${translations['scannerActions'] || 'Scanner Actions'}</strong></th>
10195                        </div>
10196                        <div class="d-grid gap-2">
10197                            <button type="button" class="btn btn-success btn-lg" id="newScanBtn">
10198                                <i class="fas fa-play me-2"></i>${translations['startNewScan'] || 'Start New Scan'}
10199                            </button>
10200                            <button type="button" class="btn btn-info" id="viewStatusBtn">
10201                                <i class="fas fa-list me-2"></i>${translations['viewStatus'] || 'View Status'}
10202                            </button>
10203                        </div>
10204
10205                        <div class="mt-3 pt-3 border-top">
10206                            <small class="text-muted">
10207                                <i class="fas fa-clock me-1"></i>
10208                                ${translations['scanTime'] || 'Typical scan time: 1-3 hours.'}
10209                            </small>
10210                        </div>
10211                    </div>
10212                </div>
10213            </div>
10214        </div>
10215    `;
10216
10217    return scannerContainer;
10218}
10219
10220function showScanUrlModal(domainId) {
10221    // Get default URL from selected domain
10222    const domainDropdown = document.getElementById('select_domain');
10223    let defaultUrl = 'https://example.com';
10224    if (domainDropdown && domainDropdown.selectedOptions.length > 0) {
10225        const selectedDomain = domainDropdown.options[domainDropdown.selectedIndex].text;
10226        defaultUrl = 'https://' + selectedDomain + '/';
10227    }
10228
10229    // Store domainId globally for the inline handlers
10230    window.currentScanDomainId = domainId;
10231
10232    const modal = document.createElement('div');
10233    modal.classList.add('modal', 'fade');
10234    modal.tabIndex = -1;
10235    modal.id = 'scanUrlModal';
10236
10237    modal.innerHTML = `
10238        <div class="modal-dialog modal-lg">
10239            <div class="modal-content">
10240                <div class="modal-header">
10241                    <h5 class="modal-title">
10242                        <i class="fas fa-cog me-2"></i>${translations['configureScan'] || 'Configure Scan'}
10243                    </h5>
10244                    <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="${translations['close'] || 'Close'}"></button>
10245                </div>
10246                <div class="modal-body">
10247                    <div class="mb-3">
10248                        <p class="text-muted">
10249                            ${translations['scanModalDescription'] || 'Enter the URL of the page you want to scan for cookies. Our scanner will analyze the page and detect all c
10249ookies being used.'}
10250                        </p>
10251                    </div>
10252                    <div class="mb-3">
10253                        <label for="modalScanUrl" class="form-label">
10254                            <i class="fas fa-link me-1"></i>${translations['downloadPageUrl'] || 'Download Page URL'} <span class="text-danger">*</span>
10255                        </label>
10256                        <input type="url" class="form-control" id="modalScanUrl"
10257                               value="${defaultUrl}"
10258                               placeholder="https://example.com"
10259                               onkeypress="handleScanUrlKeypress(event)"
10260                               oninput="validateScanUrl()"
10261                               required>
10262                        <div class="form-text">
10263                            ${translations['scanUrlHelper'] || 'Enter the URL of the page you want to scan for cookies'}
10264                        </div>
10265                        <div class="invalid-feedback">
10266                            Please enter a valid URL
10267                        </div>
10268                    </div>
10269
10270                    <div class="alert alert-info" role="alert">
10271                        <i class="fas fa-info-circle me-2"></i>
10272                        <strong>${translations['scanTip'] || 'Tip:'}</strong> ${translations['scanTipText'] || 'For best results, scan your homepage or a page that loads all your tracking scripts and cookies.'}
10273                    </div>
10274                </div>
10275                <div class="modal-footer">
10276                    <button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">
10277                        <i class="fas fa-times me-2"></i>${translations['cancel'] || 'Cancel'}
10278                    </button>
10279                    <button type="button" class="btn btn-success" id="modalScanBtn" onclick="executeScan()">
10280                        <i class="fas fa-play me-2"></i>${translations['startScan'] || 'Start Scan'}
10281                    </button>
10282                </div>
10283            </div>
10284        </div>
10285    `;
10286
10287    document.body.appendChild(modal);
10288    const bsModal = new bootstrap.Modal(modal);
10289
10290    // Store modal reference globally for cleanup
10291    window.currentScanModal = bsModal;
10292    window.currentScanModalElement = modal;
10293
10294    bsModal.show();
10295
10296    // Auto-focus and select after modal is shown
10297    modal.addEventListener('shown.bs.modal', () => {
10298        const urlInput = document.getElementById('modalScanUrl');
10299        if (urlInput) {
10300            setTimeout(() => {
10301                urlInput.focus();
10302                urlInput.select();
10303                validateScanUrl(); // Initial validation
10304            }, 100);
10305        }
10306    });
10307
10308    modal.addEventListener('hidden.bs.modal', () => {
10309        document.body.removeChild(modal);
10310        // Clean up global variables
10311        delete window.currentScanDomainId;
10312        delete window.currentScanModal;
10313        delete window.currentScanModalElement;
10314    });
10315}
10316
10317function validateScanUrl() {
10318    const urlInput = document.getElementById('modalScanUrl');
10319    const scanBtn = document.getElementById('modalScanBtn');
10320
10321    if (urlInput && scanBtn) {
10322        const isValid = urlInput.value.trim() !== '' && urlInput.checkValidity();
10323        scanBtn.disabled = !isValid;
10324
10325        if (isValid) {
10326            urlInput.classList.remove('is-invalid');
10327            urlInput.classList.add('is-valid');
10328        } else {
10329            urlInput.classList.remove('is-valid');
10330            if (urlInput.value.trim() !== '') {
10331                urlInput.classList.add('is-invalid');
10332            }
10333        }
10334    }
10335}
10336
10337function handleScanUrlKeypress(event) {
10338    if (event.key === 'Enter') {
10339        event.preventDefault();
10340        const scanBtn = document.getElementById('modalScanBtn');
10341        if (scanBtn && !scanBtn.disabled) {
10342            executeScan();
10343        }
10344    }
10345}
10346
10347async function executeScan() {
10348    const urlInput = document.getElementById('modalScanUrl');
10349    const scanBtn = document.getElementById('modalScanBtn');
10350
10351    if (!urlInput) {
10352        console.error('URL input not found');
10353        displayAlertCookie('Error: Form not found. Please try again.', 'error');
10354        return;
10355    }
10356
10357    if (!scanBtn) {
10358        console.error('Scan button not found');
10359        return;
10360    }
10361
10362    const url = urlInput.value.trim();
10363    const domainId = window.currentScanDomainId;
10364
10365    if (!url) {
10366        urlInput.classList.add('is-invalid');
10367        urlInput.focus();
10368        return;
10369    }
10370
10371    // Validate URL format
10372    try {
10373        new URL(url);
10374    } catch (e) {
10375        urlInput.classList.add('is-invalid');
10376        urlInput.focus();
10377        displayAlertCookie('Please enter a valid URL', 'error');
10378        return;
10379    }
10380
10381    if (!domainId) {
10382        console.error('Domain ID not found');
10383        displayAlertCookie('Error: Domain not selected', 'error');
10384        return;
10385    }
10386
10387    try {
10388        // Show loading state
10389        scanBtn.disabled = true;
10390        scanBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Starting Scan...';
10391
10392        const data = {
10393            sid: getSid(),
10394            domainId: parseInt(domainId),
10395            url: url
10396        };
10397
10398        console.log('Starting scan with data:', data);
10399
10400        const response = await postRequest(endpoint + '/cmp/page-cookies-scanner', data);
10401
10402        if (response.ok) {
10403            const responseBody = await response.json();
10404
10405            if (responseBody.statusCode === 0) {
10406                // Close modal
10407                if (window.currentScanModal) {
10408                    window.currentScanModal.hide();
10409                }
10410
10411                // Show success message
10412                //displayAlertCookie('Scan started successfully! Use "View Status" to check progress.', 'success');
10413
10414            } else {
10415                //displayAlertCookie('Failed to start scan: ' + (responseBody.statusMessage || 'Unknown error'), 'error');
10416            }
10417        } else {
10418            //displayAlertCookie('Failed to start scan: ' + response.statusText, 'error');
10419        }
10420    } catch (error) {
10421        console.error('Error starting scan:', error);
10422        displayAlertCookie('Error starting scan: ' + error.message, 'error');
10423    } finally {
10424        // Reset button state
10425        if (scanBtn) {
10426            scanBtn.disabled = false;
10427            scanBtn.innerHTML = '<i class="fas fa-play me-2"></i>' + (translations['startScan'] || 'Start Scan');
10428        }
10429    }
10430}
10431
10432function startNewScan(domainId) {
10433    showScanUrlModal(domainId);
10434}
10435
10436async function viewScanStatus(domainId) {
10437    const viewStatusBtn = document.getElementById('viewStatusBtn');
10438
10439    try {
10440        // Show loading state
10441        viewStatusBtn.disabled = true;
10442        viewStatusBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Loading...';
10443
10444        const data = {
10445            sid: getSid(),
10446            domainId: parseInt(domainId)
10447        };
10448
10449        console.log('Getting scan status with data:', data);
10450
10451        const response = await postRequest(endpoint + '/cmp/page-cookies-scanner', data);
10452
10453        if (response.ok) {
10454            const responseBody = await response.json();
10455
10456            if (responseBody.statusCode === 0) {
10457                const results = responseBody.results || [];
10458                showScanStatusModal(results);
10459            } else {
10460                displayAlertCookie('Failed to get scan status: ' + (responseBody.statusMessage || 'Unknown error'), 'error');
10461            }
10462        } else {
10463            displayAlertCookie('Failed to get scan status: ' + response.statusText, 'error');
10464        }
10465    } catch (error) {
10466        console.error('Error getting scan status:', error);
10467        displayAlertCookie('Error getting scan status: ' + error.message, 'error');
10468    } finally {
10469        // Reset button state
10470        viewStatusBtn.disabled = false;
10471        viewStatusBtn.innerHTML = '<i class="fas fa-list me-2"></i>' + (translations['viewStatus'] || 'View Status');
10472    }
10473}
10474
10475function generateScanStatusContent(results) {
10476    if (!results || results.length === 0) {
10477        return `
10478            <div class="text-center py-5">
10479                <i class="fas fa-search fa-3x text-muted mb-3"></i>
10480                <h5 class="text-muted">${translations['noScansFound'] || 'No scans found for this domain'}</h5>
10481                <p class="text-muted">${translations['startFirstScan'] || 'Start your first scan by entering a URL above'}</p>
10482            </div>
10483        `;
10484    }
10485
10486    // Create table for scan results
10487    return `
10488        <div class="mb-3">
10489            <div class="d-flex justify-content-between align-items-center">
10490                <h6 class="mb-0">${translations['scanHistory'] || 'Scan History'}</h6>
10491                <span class="badge bg-primary">${results.length} scan${results.length !== 1 ? 's' : ''}</span>
10492            </div>
10493        </div>
10494        <div class="table-responsive">
10495            <table class="table table-hover table-sm">
10496                <thead class="table-light">
10497                    <tr>
10498                        <th><i class="fas fa-image me-1"></i>${translations['preview'] || 'Preview'}</th>
10499                        <th><i class="fas fa-link me-1"></i>${translations['url'] || 'URL'}</th>
10500                        <th><i class="fas fa-flag me-1"></i>${translations['status'] || 'Status'}</th>
10501                        <th><i class="fas fa-calendar me-1"></i>${translations['created'] || 'Created'}</th>
10502                        <th><i class="fas fa-check me-1"></i>${translations['completed'] || 'Completed'}</th>
10503                        <th><i class="fas fa-cogs me-1"></i>${translations['actions'] || 'Actions'}</th>
10504                    </tr>
10505                </thead>
10506                <tbody>
10507                    ${results.map(result => createScanResultRow(result)).join('')}
10508                </tbody>
10509            </table>
10510        </div>
10511    `;
10512}
10513
10514function createScanResultRow(result) {
10515    const statusBadge = getScanStatusBadge(result.status);
10516
10517    // Handle empty URL case - don't show error for empty URLs in scan history
10518    const displayUrl = result.url && result.url.trim() !== '' ? result.url : 'Auto Scan';
10519    const truncatedUrl = displayUrl !== 'Auto Scan' && displayUrl.length > 50 ?
10520        displayUrl.substring(0, 50) + '...' : displayUrl;
10521
10522    // Create preview image cell
10523    const previewCell = createPreviewImageCell(result.previewImageUri, result.url);
10524
10525    return `
10526        <tr>
10527            <td>${previewCell}</td>
10528            <td>
10529                ${displayUrl !== 'Auto Scan' ?
10530                    `<a href="${result.url}" target="_blank" class="text-decoration-none" title="${result.url}">
10531                        ${truncatedUrl}
10532                        <i class="fas fa-external-link-alt fa-xs ms-1"></i>
10533                    </a>` :
10534                    `<span class="text-muted fst-italic">${truncatedUrl}</span>`
10535                }
10536            </td>
10537            <td>${statusBadge}</td>
10538            <td>
10539                <small class="text-muted">${result.createdDate || 'N/A'}</small>
10540            </td>
10541            <td>
10542                <small class="text-muted">${result.completedDate || 'Pending'}</small>
10543            </td>
10544            <td>
10545                <div class="btn-group btn-group-sm">
10546                    ${result.previewImageUri && result.previewImageUri.trim() !== '' ?
10547                        `<button class="btn btn-outline-primary btn-sm" onclick="showPreviewImageModal('${result.previewImageUri}','${displayUrl}')" title="View Preview">
10548                            <i class="fas fa-eye"></i>
10549                        </button>` : ''
10550                    }
10551                    ${result.status === 1 || result.status === 2 ?
10552                        `<button class="btn btn-outline-info btn-sm" onclick="refreshScanStatusModal()" title="Refresh Status">
10553                            <i class="fas fa-refresh"></i>
10554                        </button>` : ''
10555                    }
10556                </div>
10557            </td>
10558        </tr>
10559    `;
10560}
10561
10562function getFullImageUrl(imageUri) {
10563  if (!imageUri) return '';
10564  // Make sure we don't append to URLs that are already absolute
10565  if (imageUri.startsWith('http')) return imageUri;
10566  return imageUri;
10567}
10568
10569function createPreviewImageCell(previewImageUri, url) {
10570    if (!previewImageUri || previewImageUri.trim() === '') {
10571        return '<span class="text-muted small">No preview</span>';
10572    }
10573
10574    const fullImageUrl = getFullImageUrl(previewImageUri);
10575    return `<img src="${fullImageUrl}
10575" alt="Page Preview" class="img-thumbnail preview-thumbnail"
10576            onclick="showPreviewImageModal('${previewImageUri}','${url}')"
10577            style="max-height: 40px; cursor: pointer;" />`;
10578}
10579
10580function openImageInNewTab(imageUri) {
10581    if (imageUri && imageUri.trim() !== '') {
10582        window.open(imageUri, '_blank');
10583    }
10584}
10585
10586function downloadPreviewImage(imageUri, url) {
10587    if (!imageUri || imageUri.trim() === '') {
10588        displayAlertCookie('No image to download', 'error');
10589        return;
10590    }
10591
10592    try {
10593        // Create a temporary anchor element to trigger download
10594        const link = document.createElement('a');
10595        link.href = imageUri;
10596
10597        // Generate filename from URL and timestamp
10598        const urlObj = new URL(url);
10599        const domain = urlObj.hostname.replace(/[^a-z0-9]/gi, '_');
10600        const timestamp = new Date().toISOString().split('T')[0];
10601        link.download = `preview_${domain}_${timestamp}.png`;
10602
10603        // Trigger download
10604        document.body.appendChild(link);
10605        link.click();
10606        document.body.removeChild(link);
10607
10608        displayAlertCookie('Download started', 'success');
10609    } catch (error) {
10610        console.error('Download error:', error);
10611        displayAlertCookie('Could not download image. You can right-click and save the image manually.', 'info');
10612    }
10613}
10614
10615function refreshScanStatusModal() {
10616    const domainDropdown = document.getElementById('select_domain');
10617    if (domainDropdown && domainDropdown.value) {
10618        // Close current modal
10619        const currentModal = document.querySelector('.modal.show');
10620        if (currentModal) {
10621            bootstrap.Modal.getInstance(currentModal).hide();
10622        }
10623
10624        // Reopen with fresh data
10625        setTimeout(() => {
10626            viewScanStatus(domainDropdown.value);
10627        }, 300);
10628    }
10629}
10630
10631function getScanStatusBadge(status) {
10632    switch (status) {
10633        case 1:
10634            return '<span class="badge bg-warning"><i class="fas fa-clock me-1"></i>Waiting</span>';
10635        case 2:
10636            return '<span class="badge bg-info"><i class="fas fa-spinner fa-spin me-1"></i>Processing</span>';
10637        case 3:
10638            return '<span class="badge bg-success"><i class="fas fa-check me-1"></i>Finished</span>';
10639        default:
10640            return '<span class="badge bg-secondary">Unknown</span>';
10641    }
10642}
10643
10644function importScanResults(url) {
10645    displayAlertCookie('Scan results import functionality will be implemented soon', 'info');
10646    // TODO: Implement scan results import
10647    // This would typically fetch the detected cookies from the scan and add them to the cookie list
10648}
10649
10650let expandedCookiesList = {};
10651
10652async function handleViewStatus() {
10653    console.log('handleViewStatus called - redirecting to showScanStatusModal');
10654    await showScanStatusModal();
10655}
10656
10657function createScanStatusModal() {
10658    const modal = document.createElement('div');
10659    modal.classList.add('modal', 'fade');
10660    modal.tabIndex = -1;
10661
10662    modal.innerHTML = `
10663        <div class="modal-dialog modal-lg">
10664            <div class="modal-content">
10665                <div class="modal-header">
10666                    <h5 class="modal-title">
10667                        <i class="fas fa-list-ul me-2"></i>
10668                        ${translations['scanStatus'] || 'Scan Status'}
10669                    </h5>
10670                    <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
10671                </div>
10672                <div class="modal-body">
10673                    <div id="scanStatusContent">
10674                        <div class="text-center py-5">
10675                            <div class="spinner-border text-primary" role="status">
10676                                <span class="visually-hidden">Loading...</span>
10677                            </div>
10678                            <p class="mt-3 text-muted">${translations['loadingStatus'] || 'Loading scan status...'}</p>
10679                        </div>
10680                    </div>
10681                </div>
10682                <div class="modal-footer">
10683                    <button type="button" class="btn btn-secondary" onclick="refreshScanStatus()" id="refreshStatusBtn">
10684                        <i class="fas fa-refresh me-2"></i>${translations['refresh'] || 'Refresh'}
10685                    </button>
10686                    <button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">
10687                        ${translations['close'] || 'Close'}
10688                    </button>
10689                </div>
10690            </div>
10691        </div>
10692    `;
10693
10694    return modal;
10695}
10696
10697function toggleCookieList(index) {
10698    const isExpanded = expandedCookiesList[index] || false;
10699    expandedCookiesList[index] = !isExpanded;
10700
10701    // Update the UI
10702    const cookieToggle = document.getElementById(`cookieToggle_${index}`);
10703    const cookieList = document.getElementById(`cookieList_${index}`);
10704
10705    if (cookieToggle && cookieList) {
10706        cookieToggle.innerHTML = `
10707            <span class="badge bg-success me-2" style="cursor: pointer;">
10708                ${translations['cookiesFound'] || 'Cookies Found'} ${!isExpanded ? '▼' : '▶'}
10709            </span>
10710        `;
10711        cookieList.style.display = !isExpanded ? 'block' : 'none';
10712    }
10713}
10714
10715function renderCookieInfo(result, index) {
10716    let cookieInfoHtml = '';
10717
10718    // Only show cookie info if scan is completed (status 3)
10719    if (result.status === 3 && (result.cookiesFound || result.numCookies !== undefined)) {
10720        cookieInfoHtml = `
10721            <div class="mb-2">
10722                <div class="d-flex gap-2 align-items-center flex-wrap">
10723        `;
10724
10725        // Show expandable cookie names if available
10726        if (result.cookiesFound) {
10727            const isExpanded = expandedCookiesList[index] || false;
10728            cookieInfoHtml += `
10729                <div>
10730                    <div id="cookieToggle_${index}" onclick="toggleCookieList('${index}')" style="cursor: pointer;">
10731                        <span class="badge bg-success" style="cursor: pointer;">
10732                            ${translations['cookiesFound'] || 'Cookies Found'} ${isExpanded ? '▼' : '▶'}
10733                        </span>
10734                    </div>
10735                    <div id="cookieList_${index}" class="mt-2" style="display: ${isExpanded ? 'block' : 'none'};">
10736                        <div class="p-2 bg-light rounded border">
10737                            <small class="text-muted fw-bold">${translations['cookieNames'] || 'Cookie Names'}:</small>
10738                            <div class="mt-1 d-flex flex-wrap gap-1">
10739                                ${renderCookieNames(result.cookiesFound)}
10740                            </div>
10741                        </div>
10742                    </div>
10743                </div>
10744            `;
10745        }
10746
10747        // Show cookie count if available
10748        if (result.numCookies !== undefined) {
10749            cookieInfoHtml += `
10750                <span class="badge bg-info">
10751                    ${result.numCookies} ${translations['cookie'] || 'cookie'}${result.numCookies !== 1 ? 's' : ''}
10752                </span>
10753            `;
10754        }
10755
10756        cookieInfoHtml += `
10757                </div>
10758            </div>
10759        `;
10760    }
10761
10762    return cookieInfoHtml;
10763}
10764
10765function renderCookieNames(cookiesFound) {
10766    let cookieNamesHtml = '';
10767
10768    if (Array.isArray(cookiesFound)) {
10769        // Handle array format
10770        cookiesFound.forEach((cookieName, cookieIndex) => {
10771            cookieNamesHtml += `
10772                <span class="badge bg-info bg-opacity-75 text-dark">
10773                    ${String(cookieName)}
10774                </span>
10775            `;
10776        });
10777    } else if (typeof cookiesFound === 'string') {
10778        // Handle string format (comma-separated)
10779        const cookieNames = cookiesFound.split(',');
10780        cookieNames.forEach((cookieName, cookieIndex) => {
10781            const trimmedName = cookieName.trim();
10782            if (trimmedName) {
10783                cookieNamesHtml += `
10784                    <span class="badge bg-info bg-opacity-75 text-dark">
10785                        ${trimmedName}
10786                    </span>
10787                `;
10788            }
10789        });
10790    } else {
10791        cookieNamesHtml = '<small class="text-muted">No cookie names available</small>';
10792    }
10793
10794    return cookieNamesHtml;
10795}
10796
10797function getStatusBadge(status) {
10798    switch (status) {
10799        case 1:
10800            return `
10801                <span class="badge bg-warning d-flex align-items-center gap-1">
10802                    <i class="fas fa-hourglass-half"></i>
10803                    Waiting
10804                </span>
10805            `;
10806        case 2:
10807            return `
10808                <span class="badge bg-info d-flex align-items-center gap-1">
10809                    <div class="spinner-border spinner-border-sm" role="status">
10810                        <span class="visually-hidden">Loading...</span>
10811                    </div>
10812                    Processing
10813                </span>
10814            `;
10815        case 3:
10816            return `
10817                <span class="badge bg-success d-flex align-items-center gap-1">
10818                    <i class="fas fa-check-circle"></i>
10819                    Finished
10820                </span>
10821            `;
10822        default:
10823            return `<span class="badge bg-secondary">Unknown</span>`;
10824    }
10825}
10826
10827function getPreviewImageHtml(previewImageUri, url) {
10828
10829    if (previewImageUri) {
10830        return `
10831            <div class="position-relative" style="width: 80px; height: 60px; cursor: pointer;"
10832                 onclick="showPreviewImageModal('${previewImageUri}', '${url}')">
10833                <img src="${previewImageUri}"
10834                     alt="Page Preview"
10835                     class="w-100 h-100 rounded border"
10836                     style="object-fit: cover;"
10837                     onerror="this.parentElement.innerHTML='<div class=\\'d-flex align-items-center justify-content-center w-100 h-100 bg-light rounded border\\' style=\\'color: #6c757d; font-size: 24px;\\'>🖼️</div>';">
10838                <div class="position-absolute top-0 end-0 m-1 rounded-circle d-flex align-items-center justify-content-center"
10839                     style="background: rgba(0,0,0,0.7); width: 16px; height: 16px;">
10840                    <span style="color: white; font-size: 10px;">🔍</span>
10841                </div>
10842            </div>
10843        `;
10844    } else {
10845        return `
10846            <div class="d-flex align-items-center justify-content-center bg-light rounded border"
10847                 style="width: 80px; height: 60px; color: #6c757d; font-size: 24px;">
10848                🖼️
10849            </div>
10850        `;
10851    }
10852}
10853
10854function showPreviewImageModal(imageUri, url) {
10855    const modal = document.createElement('div');
10856    modal.classList.add('modal', 'fade');
10857    modal.tabIndex = -1;
10858
10859    modal.innerHTML = `
10860        <div class="modal-dialog modal-lg">
10861            <div class="modal-content">
10862                <div class="modal-header">
10863                    <h5 class="modal-title">
10864                        <i class="fas fa-image me-2"></i>
10865                        ${translations['pagePreview'] || 'Page Preview'}
10866                    </h5>
10867                    <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
10868                </div>
10869                <div class="modal-body text-center">
10870                    ${url ? `
10871                        <div class="alert alert-info">
10872                            <strong>${translations['scannedUrl'] || 'Scanned URL'}:</strong>
10873                            <a href="${url}" target="_blank" class="ms-2">${url}</a>
10874                        </div>
10875                    ` : ''}
10876                    <img src="${imageUri}"
10877                         alt="Page Preview"
10878                         class="img-fluid rounded shadow"
10879                         style="height:60vh;width:100vw;object-fit:contain;"
10880                         onerror="this.parentElement.innerHTML='<div class=\\'alert alert-warning\\'>Preview image could not be loaded</div>';">
10881                </div>
10882                <div class="modal-footer">
10883                    <button type="button" class="btn btn-primary" onclick="window.open('${imageUri}', '_blank')">
10884                        <i class="fas fa-external-link-alt me-2"></i>
10885                        ${translations['openInNewTab'] || 'Open in New Tab'}
10886                    </button>
10887                    <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
10888                        ${translations['close'] || 'Close'}
10889                    </button>
10890                </div>
10891            </div>
10892        </div>
10893    `;
10894
10895    document.body.appendChild(modal);
10896    const bsModal = new bootstrap.Modal(modal);
10897    bsModal.show();
10898
10899    modal.addEventListener('hidden.bs.modal', () => {
10900        document.body.removeChild(modal);
10901    });
10902}
10903
10904async function refreshScanStatus() {
10905    const statusContent = document.getElementById('scanStatusContent');
10906    const refreshBtn = document.getElementById('refreshStatusBtn');
10907
10908    if (statusContent && refreshBtn) {
10909        // Show loading state
10910        statusContent.innerHTML = `
10911            <div class="text-center py-5">
10912                <div class="spinner-border text-primary" role="status">
10913                    <span class="visually-hidden">Loading...</span>
10914                </div>
10915                <p class="mt-3 text-muted">${translations['loadingStatus'] || 'Loading scan status...'}</p>
10916            </div>
10917        `;
10918
10919        refreshBtn.disabled = true;
10920
10921        try {
10922            // Get domain ID
10923            const domainDropdown = document.getElementById('select_domain');
10924            const domainId = domainDropdown ? domainDropdown.value : null;
10925
10926            if (!domainId) {
10927                throw new Error('No domain selected');
10928            }
10929
10930            // Fetch updated results
10931            const data = {
10932                sid: getSid(),
10933                domainId: parseInt(domainId)
10934            };
10935
10936            console.log('Refreshing scan status with data:', data);
10937            const response = await postRequest(endpoint + '/cmp/page-cookies-scanner', data);
10938
10939            if (response.ok) {
10940                const responseBody = await response.json();
10941                console.log('Refresh scan status response:', responseBody);
10942
10943                if (responseBody.statusCode === 0) {
10944                    const results = responseBody.results || [];
10945					statusContent.innerHTML = renderScanStatusResults(results);
10946                } else {
10947                    throw new Error(responseBody.statusMessage || 'Unknown error');
10948                }
10949            } else {
10950                const errorText = await response.text();
10951                console.error('HTTP Error Response:', errorText);
10952                throw new Error('Failed to refresh scan status: ' + response.status + ' - ' + response.statusText);
10953            }
10954        } catch (error) {
10955            console.error('Error refreshing scan status:', error);
10956            statusContent.innerHTML = `
10957                <div class="text-center py-5">
10958                    <div class="alert alert-danger">
10959                        <i class="fas fa-exclamation-triangle me-2"></i>
10960                        Error refreshing scan status: ${error.message}
10961                    </div>
10962                    <button class="btn btn-outline-primary mt-2" onclick="refreshScanStatus()">
10963                        <i class="fas fa-retry me-2"></i>Try Again
10964                    </button>
10965                </div>
10966            `;
10967        } finally {
10968            refreshBtn.disabled = false;
10969        }
10970    }
10971}
10972
10973async function showScanStatusModal() {
10974    // Create and show modal
10975    const modal = createScanStatusModal();
10976    document.body.appendChild(modal);
10977    const bsModal = new bootstrap.Modal(modal);
10978    bsModal.show();
10979
10980    // Clean up when modal is hidden
10981    modal.addEventListener('hidden.bs.modal', () => {
10982        document.body.removeChild(modal);
10983        expandedCookiesList = {}; // Reset expanded state when modal closes
10984    });
10985
10986    try {
10987        // Fetch scan status results
10988        const domainDropdown = document.getElementById('select_domain');
10989        const domainId = domainDropdown ? domainDropdown.value : null;
10990
10991        if (!domainId) {
10992            throw new Error('No domain selected');
10993        }
10994
10995        // Use the correct endpoint and parameters based on the REST provider
10996        const data = {
10997            sid: getSid(),
10998            domainId: parseInt(domainId)
10999            // Note: url parameter is not included, so it will return scan results list
11000        };
11001
11002        console.log('Fetching scan status with data:', data);
11003        const response = await postRequest(endpoint + '/cmp/page-cookies-scanner', data);
11004
11005        if (response.ok) {
11006            const responseBody = await response.json();
11007            console.log('Scan status response:', responseBody);
11008
11009            if (responseBody.statusCode === 0) {
11010                const results = responseBody.results || [];
11011
11012                // Update modal content with results
11013                const statusContent = document.getElementById('scanStatusContent');
11014                const refreshBtn = document.getElementById('refreshStatusBtn');
11015
11016                if (statusContent && refreshBtn) {
11017                    statusContent.innerHTML = renderScanStatusResults(results);
11018                    refreshBtn.disabled = false;
11019                }
11020            } else {
11021                throw new Error(responseBody.statusMessage || 'Unknown error');
11022            }
11023        } else {
11024            const errorText = await response.text();
11025            console.error('HTTP Error Response:', errorText);
11026            throw new Error('Failed to fetch scan status: ' + response.status + ' - ' + response.statusText);
11027        }
11028    } catch (error) {
11029        console.error('Error loading scan status:', error);
11030
11031        const statusContent = document.getElementById('scanStatusContent');
11032        const refreshBtn = document.getElementById('refreshStatusBtn');
11033
11034        if (statusContent && refreshBtn) {
11035            statusContent.innerHTML = `
11036                <div class="text-center py-5">
11037                    <div class="alert alert-danger">
11038                        <i class="fas fa-exclamation-triangle me-2"></i>
11039                        Error loading scan status: ${error.message}
11040                    </div>
11041                    <button class="btn btn-outline-primary mt-2" onclick="refreshScanStatus()">
11042                        <i class="fas fa-retry me-2"></i>Try Again
11043                    </button>
11044                </div>
11045            `;
11046            refreshBtn.disabled = false;
11047        }
11048    }
11049}
11050
11051function renderScanStatusResults(results) {
11052    if (!results || results.length === 0) {
11053        return `
11054            <div class="text-center py-5">
11055                <div class="mb-3">
11056                    <i class="fas fa-search fa-3x text-muted"></i>
11057                </div>
11058                <h5 class="mb-3">${translations['noScansFound'] || 'No scans found for this domain'}</h5>
11059                <p class="text-muted">${translations['startFirstScan'] || 'Start your first scan by clicking "Start New Scan" button.'}</p>
11060            </div>
11061        `;
11062    }
11063
11064    let resultsHtml = `
11065        <div class="mb-3">
11066            <div class="d-flex justify-content-between align-items-center">
11067                <h6 class="mb-0">${translations['scanHistory'] || 'Scan History'}</h6>
11068                <span class="badge bg-info">${results.length} scan${results.length !== 1 ? 's' : ''}</span>
11069            </div>
11070        </div>
11071        <div class="border rounded overflow-hidden" style="background-color: #f6f6f7;">
11072    `;
11073
11074    results.forEach((result, index) => {
11075        const statusBadge = getStatusBadge(result.status);
11076        const previewImageHtml = getPreviewImageHtml(result.previewImageUri, result.url);
11077        const cookieInfoHtml = renderCookieInfo(result, index);
11078
11079        // Better URL handling
11080        const urlDisplay = getUrlDisplay(result.url);
11081
11082        resultsHtml += `
11083            <div class="p-3 bg-white ${index < results.length - 1 ? 'border-bottom' : ''}" style="border-color: #e1e5e9;">
11084                <div class="d-flex gap-3 align-items-start">
11085                    <!-- Preview Image -->
11086                    <div class="flex-shrink-0">
11087                        ${previewImageHtml}
11088                    </div>
11089
11090                    <!-- Scan Details -->
11091                    <div class="flex-fill min-width-0">
11092                        <div class="d-flex justify-content-between align-items-start mb-2">
11093                            <div class="flex-fill min-width-0 me-3">
11094                                ${urlDisplay.titleHtml}
11095                                ${urlDisplay.linkHtml}
11096                            </div>
11097                            <div class="flex-shrink-0">
11098                                ${statusBadge}
11099                            </div>
11100                        </div>
11101
11102                        <!-- Cookie Information -->
11103                        ${cookieInfoHtml}
11104
11105                        <!-- Timestamps -->
11106                        <div class="d-flex gap-3 small text-muted">
11107                            <span>
11108                                <i class="fas fa-clock me-1"></i>
11109                                Created: ${result.createdDate || 'N/A'}
11110                            </span>
11111                            ${result.completedDate ? `
11112                                <span>
11113                                    <i class="fas fa-check-circle me-1"></i>
11114                                    Completed: ${result.completedDate}
11115                                </span>
11116                            ` : ''}
11117                        </div>
11118                    </div>
11119                </div>
11120            </div>
11121        `;
11122    });
11123
11124    resultsHtml += '</div>';
11125    return resultsHtml;
11126}
11127
11128function getUrlDisplay(url) {
11129    if (!url || url.trim() === '') {
11130        return {
11131            titleHtml: '<div class="fw-bold">Auto Scan</div>',
11132            linkHtml: ''
11133        };
11134    }
11135
11136    // Clean and validate the URL
11137    const cleanUrl = url.trim();
11138    let isValidUrl = false;
11139    let displayUrl = cleanUrl;
11140
11141    try {
11142        // Validate URL
11143        new URL(cleanUrl);
11144        isValidUrl = true;
11145    } catch (e) {
11146        console.warn('Invalid URL detected:', cleanUrl);
11147        isValidUrl = false;
11148    }
11149
11150    // Create truncated version for display
11151    const maxLength = 60;
11152    let truncatedUrl = displayUrl;
11153    if (displayUrl.length > maxLength) {
11154        truncatedUrl = displayUrl.substring(0, maxLength) + '...';
11155    }
11156
11157    // Generate unique ID for tooltip
11158    const tooltipId = `url-tooltip-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
11159
11160    const titleHtml = `
11161        <div class="fw-bold position-relative">
11162            <span
11163                id="${tooltipId}"
11164                class="text-truncate d-inline-block"
11165                style="max-width: 100%; cursor: help;"
11166                title="${displayUrl}"
11167                data-bs-toggle="tooltip"
11168                data-bs-placement="top"
11169            >
11170                ${truncatedUrl}
11171            </span>
11172        </div>
11173    `;
11174
11175    const linkHtml = isValidUrl ? `
11176        <div class="mt-1">
11177            <a href="${cleanUrl}"
11178               target="_blank"
11179               rel="noopener noreferrer"
11180               class="text-decoration-none small text-muted"
11181               onclick="handleUrlClick(event, '${cleanUrl.replace(/'/g, "\\'")}')"
11182            >
11183                <i class="fas fa-external-link-alt me-1"></i>View page
11184            </a>
11185            <button
11186                class="btn btn-link btn-sm p-0 ms-2 text-muted"
11187                style="font-size: 0.75rem; text-decoration: none;"
11188                onclick="copyUrlToClipboard('${cleanUrl.replace(/'/g, "\\'")}')"
11189                title="Copy URL to clipboard"
11190            >
11191                <i class="fas fa-copy"></i>
11192            </button>
11193        </div>
11194    ` : `
11195        <div class="mt-1">
11196            <span class="small text-warning">
11197                <i class="fas fa-exclamation-triangle me-1"></i>Invalid URL format
11198            </span>
11199        </div>
11200    `;
11201
11202    // Initialize tooltip after a short delay to ensure element exists
11203    setTimeout(() => {
11204        const tooltipElement = document.getElementById(tooltipId);
11205        if (tooltipElement && window.bootstrap) {
11206            new bootstrap.Tooltip(tooltipElement);
11207        }
11208    }, 100);
11209
11210    return { titleHtml, linkHtml };
11211}
11212
11213function handleUrlClick(event, url) {
11214    try {
11215        // Validate URL before opening
11216        new URL(url);
11217        // Let the default behavior proceed (opening in new tab)
11218        return true;
11219    } catch (e) {
11220        event.preventDefault();
11221        console.error('Invalid URL:', url);
11222        displayAlertCookie('Invalid URL format. Cannot open page.', 'error');
11223        return false;
11224    }
11225}
11226
11227async function copyUrlToClipboard(url) {
11228    try {
11229        await navigator.clipboard.writeText(url);
11230        displayAlertCookie('URL copied to clipboard!', 'success');
11231    } catch (err) {
11232        // Fallback for older browsers
11233        const textArea = document.createElement('textarea');
11234        textArea.value = url;
11235        textArea.style.position = 'fixed';
11236        textArea.style.opacity = '0';
11237        document.body.appendChild(textArea);
11238        textArea.select();
11239
11240        try {
11241            document.execCommand('copy');
11242            displayAlertCookie('URL copied to clipboard!', 'success');
11243        } catch (e) {
11244            displayAlertCookie('Failed to copy URL. Please copy manually.', 'error');
11245        }
11246
11247        document.body.removeChild(textArea);
11248    }
11249}
11250
11251function isValidUrl(string) {
11252    try {
11253        const url = new URL(string);
11254        return url.protocol === 'http:' || url.protocol === 'https:';
11255    } catch (_) {
11256        return false;
11257    }
11258}
11259
11260function showFullUrlModal(url, title = 'Full URL') {
11261    const modal = document.createElement('div');
11262    modal.classList.add('modal', 'fade');
11263    modal.tabIndex = -1;
11264
11265    modal.innerHTML = `
11266        <div class="modal-dialog">
11267            <div class="modal-content">
11268                <div class="modal-header">
11269                    <h5 class="modal-title">
11270                        <i class="fas fa-link me-2"></i>${title}
11271                    </h5>
11272                    <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
11273                </div>
11274                <div class="modal-body">
11275                    <div class="mb-3">
11276                        <label class="form-label small text-muted">Complete URL:</label>
11277                        <div class="border rounded p-2 bg-light" style="word-break: break-all; font-family: monospace; font-size: 0.9em;">
11278                            ${url}
11279                        </div>
11280                    </div>
11281                    <div class="d-flex gap-2">
11282                        <button type="button" class="btn btn-primary btn-sm" onclick="copyUrlToCl
11282ipboard('${url.replace(/'/g, "\\'")}')">
11283                            <i class="fas fa-copy me-1"></i>Copy URL
11284                        </button>
11285                        ${isValidUrl(url) ? `
11286                            <a href="${url}" target="_blank" rel="noopener noreferrer" class="btn btn-outline-primary btn-sm">
11287                                <i class="fas fa-external-link-alt me-1"></i>Open Page
11288                            </a>
11289                        ` : ''}
11290                    </div>
11291                </div>
11292                <div class="modal-footer">
11293                    <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
11294                </div>
11295            </div>
11296        </div>
11297    `;
11298
11299    document.body.appendChild(modal);
11300    const bsModal = new bootstrap.Modal(modal);
11301    bsModal.show();
11302
11303    modal.addEventListener('hidden.bs.modal', () => {
11304        document.body.removeChild(modal);
11305    });
11306}
11307
11308function addCookieDeclarationButton(buttonsContainer) {
11309    const declarationButton = document.createElement('button');
11310    declarationButton.classList.add('btn', 'btn-info');
11311    declarationButton.innerHTML = '<i class="fas fa-code me-2"></i>' + (translations['cookieDeclarationScript'] || 'Cookie Declaration Script');
11312    declarationButton.addEventListener('click', () => showCookieDeclarationModal());
11313    buttonsContainer.appendChild(declarationButton);
11314}
11315
11316document.addEventListener('DOMContentLoaded', function() {
11317    // Add event delegation for copy button
11318    document.addEventListener('click', async function(e) {
11319        if (e.target && e.target.id === 'copyScriptBtn') {
11320            const textarea = document.getElementById('scriptCodeArea');
11321            const copyBtn = e.target;
11322
11323            try {
11324                await navigator.clipboard.writeText(textarea.value);
11325
11326                // Show success feedback
11327                const originalText = copyBtn.innerHTML;
11328                copyBtn.innerHTML = `<i class="fas fa-check me-2"></i>${translations['copied'] || 'Copied!'}`;
11329                copyBtn.classList.remove('btn-primary');
11330                copyBtn.classList.add('btn-success');
11331
11332                setTimeout(() => {
11333                    copyBtn.innerHTML = originalText;
11334                    copyBtn.classList.remove('btn-success');
11335                    copyBtn.classList.add('btn-primary');
11336                }, 2000);
11337
11338            } catch (err) {
11339                // Fallback
11340                textarea.select();
11341                document.execCommand('copy');
11342
11343                const originalText = copyBtn.innerHTML;
11344                copyBtn.innerHTML = `<i class="fas fa-check me-2"></i>${translations['copied'] || 'Copied!'}`;
11345                copyBtn.classList.remove('btn-primary');
11346                copyBtn.classList.add('btn-success');
11347
11348                setTimeout(() => {
11349                    copyBtn.innerHTML = originalText;
11350                    copyBtn.classList.remove('btn-success');
11351                    copyBtn.classList.add('btn-primary');
11352                }, 2000);
11353            }
11354        }
11355    });
11356
11357    // Handle tab switching to preview
11358    document.addEventListener('shown.bs.tab', function (e) {
11359        if (e.target.id === 'preview-tab') {
11360            loadPreview();
11361        }
11362    });
11363});
11364
11365function findProjectIdByDomainId(domainId, projects) {
11366    if (!domainId || !projects || !Array.isArray(projects)) {
11367        return null;
11368    }
11369
11370    // Convert domainId to number for comparison
11371    const targetDomainId = parseInt(domainId);
11372
11373    // Search through all projects
11374    for (const project of projects) {
11375        if (project.domains && Array.isArray(project.domains)) {
11376            // Check if this project contains the domain
11377            const foundDomain = project.domains.find(domain =>
11378                parseInt(domain.id) === targetDomainId
11379            );
11380
11381            if (foundDomain) {
11382                return project.id;
11383            }
11384        }
11385    }
11386
11387    return null; // Domain not found in any project
11388}
11389
11390function debugProjectDomainMapping() {
11391    getProjects().then(projects => {
11392        console.log('=== Project-Domain Mapping ===');
11393        projects.forEach(project => {
11394            console.log(`Project: ${project.projectName} (ID: ${project.id})`);
11395            if (project.domains) {
11396                project.domains.forEach(domain => {
11397                    console.log(`  - Domain: ${domain.domainName} (ID: ${domain.id})`);
11398                });
11399            }
11400        });
11401        console.log('===============================');
11402    });
11403}
11404
11405async function loadPreviewFromScript(scriptCode, projectId) {
11406    const previewContainer = document.getElementById('previewContainer');
11407    if (!previewContainer) return;
11408
11409    try {
11410        // Extract script URL from the script tag (e.g., <script src="..."></script>)
11411        const scriptUrlMatch = scriptCode.match(/src=["']([^"']+)["']/);
11412        if (!scriptUrlMatch) {
11413            throw new Error(translations['couldNotExtractURL'] || 'Could not extract script URL');
11414        }
11415
11416        const scriptUrl = scriptUrlMatch[1];
11417
11418        // Clear the preview container and prepare it for script execution
11419        previewContainer.innerHTML = '<div id="cookie-declaration"></div>';
11420
11421        // Create and load the script dynamically
11422        const script = document.createElement('script');
11423        script.src = scriptUrl;
11424        script.async = true;
11425
11426        // Handle script load success
11427        script.onload = function() {
11428            console.log('Cookie declaration script loaded successfully');
11429            // The script should automatically render in the cookie-declaration div
11430        };
11431
11432        // Handle script load error
11433        script.onerror = function() {
11434            console.error('Failed to load cookie declaration script');
11435            previewContainer.innerHTML = `
11436                <div class="alert alert-warning">
11437                    <i class="fas fa-exclamation-triangle me-2"></i>
11438                    <strong>${translations['previewUnavailable'] || 'Preview Unavailable'}:</strong> ${translations['previewUnavailableDesc'] || 'Unable to load the script in preview mode due to CORS restrictions.'}
11439                    <br><br>
11440                    <div class="bg-light p-3 rounded mt-2">
11441                        <strong>${translations['yourScript'] || 'Your Script'}:</strong><br>
11442                        <code>${scriptCode}</code>
11443                    </div>
11444                    <br>
11445                    <small class="text-muted">
11446                        ${translations['scriptWorksOnWebsite'] || 'The script will work perfectly when implemented on your website. This preview limitation doesn\'t affect the actual functionality.'}
11447                    </small>
11448                </div>
11449            `;
11450        };
11451
11452        // Append script to document body to execute it
11453        document.body.appendChild(script);
11454
11455        // Clean up script after some time
11456        setTimeout(() => {
11457            if (document.body.contains(script)) {
11458                document.body.removeChild(script);
11459            }
11460        }, 10000);
11461
11462    } catch (error) {
11463        console.error('Error loading preview:', error);
11464        previewContainer.innerHTML = `
11465            <div class="alert alert-info">
11466                <i class="fas fa-info-circle me-2"></i>
11467                <strong>${translations['preview'] || 'Preview'}:</strong> ${translations['scriptReadyToUse'] || 'Your cookie declaration script is ready to use.'}
11468                <br><br>
11469                <div class="bg-light p-3 rounded mt-2">
11470                    <strong>${translations['yourScript'] || 'Your Script'}:</strong><br>
11471                    <code>${scriptCode}</code>
11472                </div>
11473                <br>
11474                <small class="text-muted">
11475                    ${translations['scriptDisplaysTablesDesc'] || 'When implemented on your website, this script will automatically display your cookie declaration tables.'}
11476                </small>
11477            </div>
11478        `;
11479    }
11480}
11481
11482document.addEventListener('shown.bs.tab', function (e) {
11483    if (e.target.id === 'preview-tab') {
11484        const previewContainer = document.getElementById('previewContainer');
11485        if (previewContainer && previewContainer.innerHTML.includes(translations['loadingPreviewDesc'] || 'Loading cookie declaration preview')) {
11486            const scriptCodeArea = document.getElementById('scriptCodeArea');
11487            const domainDropdown = document.getElementById('select_domain');
11488
11489            if (scriptCodeArea && domainDropdown) {
11490                const projects = getProjects();
11491                const projectId = findProjectIdByDomainId(domainDropdown.value, projects);
11492                loadPreviewFromScript(scriptCodeArea.value, projectId);
11493            }
11494        }
11495    }
11496});
11497
11498
11499
11500
11501// Cookie Declaration Script
11502function createCookieDeclarationCard(domainId) {
11503    const declarationCard = document.createElement('div');
11504    declarationCard.classList.add('card', 'mb-4');
11505    declarationCard.id = 'cookie_declaration_section';
11506
11507    declarationCard.innerHTML = `
11508        <div class="card-header">
11509            <div class="d-flex justify-content-between align-items-center">
11510                <h5 class="card-title mb-0">
11511                    <i class="fas fa-code me-2"></i>${translations['cookieDeclarationScript'] || 'Cookie Declaration Script'}
11512                </h5>
11513                <button type="button" class="btn btn-outline-secondary btn-sm" data-bs-toggle="collapse" data-bs-target="#declarationCardBody" aria-expanded="false" aria-controls="declarationCardBody">
11514                    <i class="fas fa-chevron-down" id="declarationToggleIcon"></i>
11515                </button>
11516            </div>
11517        </div>
11518        <div class="collapse" id="declarationCardBody">
11519            <div class="card-body">
11520                <div class="row align-items-center">
11521                    <div class="col-md-8">
11522                        <h6 class="text-primary mb-3">
11523                            <i class="fas fa-file-code me-2"></i>${translations['generateScriptForWebsite'] || 'Generate Script for Your Website'}
11524                        </h6>
11525                        <p class="text-muted mb-3">
11526                            ${translations['declarationScriptDescription'] || 'Generate a script tag that displays your cookie declaration tables on your website. This helps you comply with GDPR, CCPA, and other privacy regulations by providing transparent information about all cookies used on your site.'}
11527                        </p>
11528                        <div class="mb-2">
11529                            <h6 class="text-success mb-2">
11530                                <i class="fas fa-check-circle me-2"></i>${translations['scriptFeatures'] || 'Script Features:'}
11531                            </h6>
11532                            <ul class="list-unstyled mb-0">
11533                                <li class="mb-1">
11534                                    <i class="fas fa-table text-primary me-2"></i>
11535                                    ${translations['feature1'] || 'Automatically organized cookie tables by category'}
11536                                </li>
11537                                <li class="mb-1">
11538                                    <i class="fas fa-mobile-alt text-info me-2"></i>
11539                                    ${translations['feature2'] || 'Mobile-responsive design'}
11540                                </li>
11541                                <li class="mb-1">
11542                                    <i class="fas fa-sync text-warning me-2"></i>
11543                                    ${translations['feature3'] || 'Auto-updates when you modify cookies'}
11544                                </li>
11545                                <li class="mb-1">
11546                                    <i class="fas fa-shield-alt text-secondary me-2"></i>
11547                                    ${translations['feature4'] || 'GDPR and CCPA compliant formatting'}
11548                                </li>
11549                                <li class="mb-0">
11550                                    <i class="fas fa-link text-success me-2"></i>
11551                                    ${translations['feature5'] || 'Ready to link from your privacy policy'}
11552                                </li>
11553                            </ul>
11554                        </div>
11555                    </div>
11556                    <div class="col-md-4">
11557                        <div class="text-center">
11558                            <div class="mb-4">
11559                                <i class="fas fa-code fa-3x text-primary mb-3"></i>
11560                                <h6><strong>${translations['scriptActions'] || 'Script Actions'}</strong></h6>
11561                            </div>
11562                            <div class="d-grid gap-2">
11563                                <button type="button" class="btn btn-primary btn-lg" id="generateScriptBtn">
11564                                    <i class="fas fa-code me-2"></i>${translations['cookieDeclarationScript'] || 'Cookie Declaration Script'}
11565                                </button>
11566                                <button type="button" class="btn btn-success" id="previewDeclarationBtn">
11567                                    <i class="fas fa-eye me-2"></i>${translations['previewDeclaration'] || 'Preview Declaration'}
11568                                </button>
11569                                <button type="button" class="btn btn-info" id="viewInstructionsBtn">
11570                                    <i class="fas fa-info-circle me-2"></i>${translations['viewInformation'] || 'View Information'}
11571                                </button>
11572                            </div>
11573                            <div class="mt-3 pt-3 border-top">
11574                                <small class="text-muted">
11575                                    <i class="fas fa-info-circle me-1"></i>
11576                                    ${translations['oneTimeSetup'] || 'One-time setup, automatic updates.'}
11577                                </small>
11578                            </div>
11579                        </div>
11580                    </div>
11581                </div>
11582
11583                <!-- Script Content Area (Initially Hidden) -->
11584                <div id="scriptContentArea" class="mt-4" style="display: none;">
11585                    <div class="border-top pt-4">
11586                        <div id="scriptLoadingContent">
11587                            <div class="text-center py-4">
11588                                <div class="spinner-border text-primary" role="status">
11589                                    <span class="visually-hidden">${translations['loading'] || 'Loading...'}</span>
11590                                </div>
11591                                <p class="mt-3 text-muted">${translations['loadingScript'] || 'Loading script...'}</p>
11592                            </div>
11593                        </div>
11594                    </div>
11595                </div>
11596            </div>
11597        </div>
11598    `;
11599
11600    return declarationCard;
11601}
11602
11603function displayEnhancedCookieManager(data, domainId) {
11604    console.log('displayEnhancedCookieManager called');
11605
11606    const container = document.getElementById('cookie_manager_container');
11607
11608    // Check if scanner section already exists
11609    const existingScanner = document.getElementById('cookie_scanner_section');
11610    const existingDeclaration = document.getElementById('cookie_declaration_section');
11611
11612    if (existingScanner && existingDeclaration) {
11613        console.log('Scanner and Declaration sections already exist, not recreating');
11614        // Don't clear the container if both exist, just update the cookie list part
11615        const existingCookieCard = container.querySelector('.card:not(#cookie_scanner_section):not(#cookie_declaration_section)');
11616        if (existingCookieCard) {
11617            existingCookieCard.remove();
11618        }
11619    } else {
11620        console.log('Creating new sections');
11621        container.innerHTML = '';
11622
11623        // Add cookie scanner section first (Collapsed by default)
11624        const scannerSection = createCookieScannerSection(domainId);
11625        container.appendChild(scannerSection);
11626
11627        // Add event listeners for scanner buttons
11628        const newScanBtn = document.getElementById('newScanBtn');
11629        const viewStatusBtn = document.getElementById('viewStatusBtn');
11630
11631        if (newScanBtn) {
11632            newScanBtn.addEventListener('click', () => startNewScan(domainId));
11633        }
11634
11635        if (viewStatusBtn) {
11636            viewStatusBtn.addEventListener('click', () => viewScanStatus(domainId));
11637        }
11638
11639        // Add cookie declaration card (Collapsed by default)
11640        const declarationCard = createCookieDeclarationCard(domainId);
11641        container.appendChild(declarationCard);
11642
11643        // Add event listeners for declaration card
11644        const generateScriptBtn = document.getElementById('generateScriptBtn');
11645        const previewDeclarationBtn = document.getElementById('previewDeclarationBtn');
11646        const viewInstructionsBtn = document.getElementById('viewInstructionsBtn');
11647
11648        if (generateScriptBtn) {
11649            generateScriptBtn.addEventListener('click', () => loadCookieDeclarationScript(domainId));
11650        }
11651
11652        if (previewDeclarationBtn) {
11653            previewDeclarationBtn.addEventListener('click', () => showCookieDeclarationPreview(domainId));
11654        }
11655
11656        if (viewInstructionsBtn) {
11657            viewInstructionsBtn.addEventListener('click', () => showInstructionsInCard());
11658        }
11659
11660        // Add toggle icon rotation for collapse/expand
11661        setupCollapseToggle('declarationCardBody', 'declarationToggleIcon');
11662        setupCollapseToggle('scannerCardBody', 'scannerToggleIcon');
11663        setupCollapseToggle('cookieListCardBody', 'cookieListToggleIcon');
11664    }
11665
11666    // Create main cookie list card
11667    createCookieListCard(container, data, domainId);
11668}
11669
11670function setupCollapseToggle(bodyId, iconId) {
11671    const cardBody = document.getElementById(bodyId);
11672    const toggleIcon = document.getElementById(iconId);
11673
11674    if (cardBody && toggleIcon) {
11675        cardBody.addEventListener('show.bs.collapse', function () {
11676            toggleIcon.classList.remove('fa-chevron-down');
11677            toggleIcon.classList.add('fa-chevron-up');
11678        });
11679
11680        cardBody.addEventListener('hide.bs.collapse', function () {
11681            toggleIcon.classList.remove('fa-chevron-up');
11682            toggleIcon.classList.add('fa-chevron-down');
11683        });
11684    }
11685}
11686
11687function createCookieListCard(container, data, domainId) {
11688    const card = document.createElement('div');
11689    card.classList.add('card', 'shadow-sm');
11690    container.appendChild(card);
11691
11692    // Cookie List Card Header
11693    const cardHeader = document.createElement('div');
11694    cardHeader.classList.add('card-header');
11695    card.appendChild(cardHeader);
11696
11697    const headerContainer = document.createElement('div');
11698    headerContainer.classList.add('d-flex', 'justify-content-between', 'align-items-center');
11699    cardHeader.appendChild(headerContainer);
11700
11701    const headerTitle = document.createElement('h5');
11702    headerTitle.classList.add('card-title', 'mb-0');
11703    headerTitle.innerHTML = '<i class="fas fa-list me-2"></i>' + (translations['cookieList'] || 'Cookie List');
11704    headerContainer.appendChild(headerTitle);
11705
11706    const headerToggleBtn = document.createElement('button');
11707    headerToggleBtn.type = 'button';
11708    headerToggleBtn.classList.add('btn', 'btn-outline-secondary', 'btn-sm');
11709    headerToggleBtn.setAttribute('data-bs-toggle', 'collapse');
11710    headerToggleBtn.setAttribute('data-bs-target', '#cookieListCardBody');
11711    headerToggleBtn.setAttribute('aria-expanded', 'false');
11712    headerToggleBtn.setAttribute('aria-controls', 'cookieListCardBody');
11713    headerToggleBtn.innerHTML = '<i class="fas fa-chevron-down" id="cookieListToggleIcon"></i>';
11714    headerContainer.appendChild(headerToggleBtn);
11715
11716    // Cookie List Collapsible Body
11717    const cardBodyCollapse = document.createElement('div');
11718    cardBodyCollapse.classList.add('collapse');
11719    cardBodyCollapse.id = 'cookieListCardBody';
11720    card.appendChild(cardBodyCollapse);
11721
11722    const cardBody = document.createElement('div');
11723    cardBody.classList.add('card-body');
11724    cardBodyCollapse.appendChild(cardBody);
11725
11726    // Get cookies with proper filtering
11727    let cookies = [];
11728    if (data && data.value && Array.isArray(data.value.resources)) {
11729        cookies = data.value.resources.filter(resource => {
11730            if (!resource) return false;
11731            const cookieTypes = ['Essential', 'Personalization', 'Analytics', 'Marketing', 'Security', 'Other'];
11732            return !resource.type ||
11733                   resource.type === 'cookie' ||
11734                   cookieTypes.includes(resource.type) ||
11735                   (resource.name && resource.displayName);
11736        });
11737    }
11738
11739    // Add stats badges
11740    const statsContainer = document.createElement('div');
11741    statsContainer.classList.add('d-flex', 'gap-2', 'mb-4');
11742
11743    const totalBadge = document.createElement('span');
11744    totalBadge.classList.add('badge', 'bg-primary', 'fs-6');
11745    totalBadge.textContent = cookies.length + ' Total';
11746    statsContainer.appendChild(totalBadge);
11747
11748    const systemCount = cookies.filter(c => !c.custom).length;
11749    if (systemCount > 0) {
11750        const systemBadge = document.createElement('span');
11751        systemBadge.classList.add('badge', 'bg-outline-secondary', 'text-secondary', 'border', 'border-secondary', 'fs-6');
11752        systemBadge.textContent = systemCount + ' System';
11753        systemBadge.style.backgroundColor = 'transparent';
11754        statsContainer.appendChild(systemBadge);
11755    }
11756
11757    const customCount = cookies.filter(c => c.custom).length;
11758    if (customCount > 0) {
11759        const customBadge = document.createElement('span');
11760        customBadge.classList.add('badge', 'bg-success', 'fs-6');
11761        customBadge.textContent = customCount + ' Custom';
11762        statsContainer.appendChild(customBadge);
11763    }
11764
11765    cardBody.appendChild(statsContainer);
11766
11767    // Action buttons and search container
11768    const actionsContainer = document.createElement('div');
11769    actionsContainer.classList.add('d-flex', 'justify-content-between', 'align-items-center', 'mb-4');
11770
11771    const buttonsContainer = document.createElement('div');
11772    buttonsContainer.classList.add('d-flex', 'gap-2');
11773
11774    const addButton = document.createElement('button');
11775    addButton.classList.add('btn', 'btn-primary');
11776    addButton.innerHTML = '<i class="fa fa-plus me-2"></i>' + (translations['addCookie'] || 'Add Cookie');
11777    addButton.addEventListener('click', () => showAddCookieModal(data, domainId));
11778    buttonsContainer.appendChild(addButton);
11779
11780    const refreshButton = document.createElement('button');
11781    refreshButton.classList.add('btn', 'btn-outline-secondary');
11782    refreshButton.innerHTML = '<i class="fa fa-refresh me-2"></i>' + (translations['refresh'] || 'Refresh');
11783    refreshButton.addEventListener('click', () => loadCookiesForDomain(domainId));
11784    buttonsContainer.appendChild(refreshButton);
11785
11786    actionsContainer.appendChild(buttonsContainer);
11787
11788    // Search container
11789    const searchContainer = document.createElement('div');
11790    searchContainer.classList.add('d-flex', 'gap-2');
11791
11792    const searchInput = document.createElement('input');
11793    searchInput.type = 'text';
11794    searchInput.classList.add('form-control');
11795    searchInput.placeholder = 'Search cookies...';
11796    searchInput.style.width = '250px';
11797    searchContainer.appendChild(searchInput);
11798
11799    const filterSelect = document.createElement('select');
11800    filterSelect.classList.add('form-select');
11801    filterSelect.style.width = '150px';
11802    filterSelect.innerHTML = `
11803        <option value="all">${translations['allTypes'] || 'All Types'}</option>
11804        <option value="Essential">${translations['essential'] || 'Essential'}</option>
11805        <option value="Personalization">${translations['personalization'] || 'Personalization'}</option>
11806        <option value="Analytics">${translations['analytics'] || 'Analytics'}</option>
11807        <option value="Marketing">${translations['marketing'] || 'Marketing'}</option>
11808        <option value="Security">${translations['security'] || 'Security'}</option>
11809        <option value="Other">${translations['other'] || 'Other'}</option>
11810    `;
11811    searchContainer.appendChild(filterSelect);
11812
11813    actionsContainer.appendChild(searchContainer);
11814    cardBody.appendChild(actionsContainer);
11815
11816    // Create table
11817    const tableContainer = document.createElement('div');
11818    tableContainer.classList.add('table-responsive');
11819    cardBody.appendChild(tableContainer);
11820
11821    const table = document.createElement('table');
11822    table.classList.add('table', 'table-hover', 'cookie-manager-table', 'mb-0');
11823    table.id = 'cookiesTable';
11824    tableContainer.appendChild(table);
11825
11826    const thead = document.createElement('thead');
11827    thead.classList.add('table-dark');
11828    table.appendChild(thead);
11829    const headerRow = document.createElement('tr');
11830    thead.appendChild(headerRow);
11831
11832    const headers = [
11833        { key: 'displayName', text: 'Display Name', icon: 'fa-file-code', sortable: true },
11834        { key: 'name', text: 'Name', icon: 'fa-tag', sortable: true },
11835        { key: 'type', text: 'Type', icon: 'fa-layer-group', sortable: true },
11836        { key: 'retentionPeriod', text: 'Retention', icon: 'fa-clock', sortable: true },
11837        { key: 'actions', text: 'Actions', icon: 'fa-cogs', sortable: false }
11838    ];
11839
11840    headers.forEach(header => {
11841        const th = document.createElement('th');
11842        th.classList.add('user-select-none');
11843        if (header.sortable) {
11844            th.classList.add('cursor-pointer');
11845            th.style.cursor = 'pointer';
11846        }
11847
11848        const content = `<i class="fa ${header.icon} me-2"></i>` + (translations[header.key] || header.text);
11849        if (header.sortable) {
11850            th.innerHTML = content + ' <i class="fas fa-sort text-muted ms-1"></i>';
11851            th.addEventListener('click', () => sortTable(header.key));
11852        } else {
11853            th.innerHTML = content;
11854        }
11855
11856        headerRow.appendChild(th);
11857    });
11858
11859    const tbody = document.createElement('tbody');
11860    tbody.id = 'cookiesTableBody';
11861    table.appendChild(tbody);
11862
11863    // Store cookies globally for search/filter
11864    window.currentCookies = cookies;
11865
11866    // Render initial table
11867    renderCookieTable(cookies, data, domainId);
11868
11869    // Add search and filter functionality
11870    let searchTimeout;
11871    searchInput.addEventListener('input', () => {
11872        clearTimeout(searchTimeout);
11873        searchTimeout = setTimeout(() => {
11874            filterAndSearch(cookies, data, domainId);
11875        }, 300);
11876    });
11877
11878    filterSelect.addEventListener('change', () => {
11879        filterAndSearch(cookies, data, domainId);
11880    });
11881}
11882
11883async function loadCookieDeclarationScript(domainId) {
11884    const scriptContentArea = document.getElementById('scriptContentArea');
11885    const scriptLoadingContent = document.getElementById('scriptLoadingContent');
11886
11887    if (!scriptContentArea || !scriptLoadingContent) {
11888        console.error('Script content area not found');
11889        return;
11890    }
11891
11892    scriptContentArea.style.display = 'block';
11893
11894    try {
11895        const domainDropdown = document.getElementById('select_domain');
11896        if (!domainDropdown || !domainDropdown.value) {
11897            throw new Error(translations['noDomainSelected'] || 'No domain selected');
11898        }
11899
11900        const currentDomainId = domainDropdown.value;
11901        const projects = await getProjects();
11902
11903        if (!projects || !projects.length) {
11904            throw new Error(translations['noProjectsAvailable'] || 'No projects available');
11905        }
11906
11907        const projectId = findProjectIdByDomainId(currentDomainId, projects);
11908        if (!projectId) {
11909            throw new Error(translations['projectNotFound'] || 'Project not found for selected domain');
11910        }
11911
11912        const data = {
11913            sid: getSid(),
11914            projectId: projectId
11915        };
11916
11917        const response = await postRequest(endpoint + '/scriptcode', data);
11918
11919        if (response && response.ok) {
11920            const responseBody = await response.json();
11921            if (responseBody.statusCode === 0) {
11922                scriptLoadingContent.innerHTML = generateScriptCardContent(responseBody.cookiesScriptCode);
11923                window.currentCookieScript = responseBody.cookiesScriptCode;
11924            } else {
11925                throw new Error(responseBody.statusMessage || 'Server error');
11926            }
11927        } else {
11928            throw new Error(translations['failedToLoadScript'] || 'Failed to load script');
11929        }
11930    } catch (error) {
11931        console.error('Error loading cookie declaration script:', error);
11932        scriptLoadingContent.innerHTML = `
11933            <div class="alert alert-danger">
11934                <i class="fas fa-exclamation-triangle me-2"></i>
11935                <strong>${translations['error'] || 'Error'}:</strong> ${error.message}
11936            </div>
11937        `;
11938    }
11939}
11940
11941function generateScriptCardContent(scriptCode) {
11942    return `
11943        <div class="mb-4">
11944            <div class="d-flex justify-content-between align-items-center mb-3">
11945                <h6 class="mb-0 text-primary">
11946                    <i class="fas fa-code me-2"></i>${translations['scriptTagForWebsite'] || 'Script Tag for Your Website'}
11947                </h6>
11948                <button type="button" class="btn btn-primary btn-sm" onclick="copyScriptToClipboard()">
11949                    <i class="fas fa-copy me-2"></i>${translations['copyScriptTag'] || 'Copy Script Tag'}
11950                </button>
11951            </div>
11952
11953            <div class="alert alert-info mb-3">
11954                <i class="fas fa-info-circle me-2"></i>
11955                <strong>${translations['simpleIntegration'] || 'Simple Integration'}:</strong>
11956                ${translations['simpleIntegrationDesc'] || 'Just copy this single script tag and paste it into your website\'s HTML head section. This will automatically generate the cookie declaration tables on your page.'}
11957            </div>
11958
11959            <div class="bg-light p-3 rounded border">
11960                <label class="form-label small text-muted">${translations['htmlScriptTag'] || 'HTML Script Tag'}:</label>
11961                <textarea
11962                    class="form-control font-monospace"
11963                    id="scriptCodeArea"
11964                    rows="3"
11965                    readonly
11966                    style="font-size: 0.9em; line-height: 1.4; background: white;"
11967                >${scriptCode}</textarea>
11968            </div>
11969
11970            <div class="mt-4">
11971                <h6 class="fw-bold mb-2">${translations['howToAddToWebsite'] || 'How to Add to Your Website'}:</h6>
11972                <div class="bg-light p-3 rounded">
11973                    <p class="mb-2 small"><strong>${translations['option1AddToHead'] || 'Option 1 - Add to HTML Head'}:</strong></p>
11974                    <pre class="mb-3" style="font-size: 0.85em;"><code>&lt;html&gt;
11975&lt;head&gt;
11976    &lt;title&gt;Your Website&lt;/title&gt;
11977    ${scriptCode.replace(/</g, '&lt;').replace(/>/g, '&gt;')}
11978&lt;/head&gt;
11979&lt;body&gt;
11980    &lt;div id="cookie-declaration"&gt;&lt;/div&gt;
11981&lt;/body&gt;
11982&lt;/html&gt;</code></pre>
11983
11984                    <p class="mb-2 small"><strong>${translations['option2AddToPage'] || 'Option 2 - Add to Specific Page'}:</strong></p>
11985                    <pre style="font-size: 0.85em;"><code>&lt;div id="cookie-declaration"&gt;
11986    ${scriptCode.replace(/</g, '&lt;').replace(/>/g, '&gt;')}
11987&lt;/div&gt;</code></pre>
11988                </div>
11989            </div>
11990
11991            <div class="mt-3">
11992                <small class="text-muted">
11993                    <i class="fas fa-shield-alt me-1"></i>
11994                    ${translations['scriptSecurityDesc'] || 'This script is hosted securely and will automatically display your current cookie configuration. It updates automatically when you modify cookies in this manager.'}
11995                </small>
11996            </div>
11997        </div>
11998    `;
11999}
12000
12001async function copyScriptToClipboard() {
12002    const textarea = document.getElementById('scriptCodeArea');
12003    if (!textarea) {
12004        console.error('Script code area not found');
12005        return;
12006    }
12007
12008    try {
12009        await navigator.clipboard.writeText(textarea.value);
12010        displayAlertCookie(translations['scriptCopied'] || 'Script copied to clipboard!', 'success');
12011    } catch (err) {
12012        // Fallback for older browsers
12013        textarea.select();
12014        document.execCommand('copy');
12015        displayAlertCookie(translations['scriptCopied'] || 'Script copied to clipboard!', 'success');
12016    }
12017}
12018
12019function loadScriptInPreview(scriptCode, containerId) {
12020    const container = document.getElementById(containerId);
12021    if (!container) {
12022        console.error('Preview container not found');
12023        return;
12024    }
12025
12026    try {
12027        // Extract script URL
12028        const scriptUrlMatch = scriptCode.match(/src=["']([^"']+)["']/);
12029
12030        if (!scriptUrlMatch) {
12031            container.innerHTML = `
12032                <div class="alert alert-warning">
12033                    <i class="fas fa-exclamation-triangle me-2"></i>
12034                    <strong>${translations['previewUnavailable'] || 'Preview Unavailable'}:</strong> ${translations['couldNotExtractURL'] || 'Could not extract script URL'}
12035                </div>
12036            `;
12037            return;
12038        }
12039
12040        const scriptUrl = scriptUrlMatch[1];
12041
12042        // Try to load the script via iframe for better isolation
12043        const iframe = document.createElement('iframe');
12044        iframe.style.width = '100%';
12045        iframe.style.height = '600px';
12046        iframe.style.border = 'none';
12047        iframe.srcdoc = `
12048            <!DOCTYPE html>
12049            <html>
12050            <head>
12051                <style>
12052                    body { font-family: Arial, sans-serif; padding: 20px; }
12053                    table { width: 100%; border-collapse: collapse; margin: 20px 0; }
12054                    th, td { padding: 10px; text-align: left; border: 1px solid #ddd; }
12055                    th { background-color: #f5f5f5; }
12056                </style>
12057            </head>
12058            <body>
12059                <div id="cookie-declaration"></div>
12060                ${scriptCode}
12061            </body>
12062            </html>
12063        `;
12064
12065        // Add error handling for iframe
12066        iframe.onerror = function() {
12067            // If iframe fails, try direct script loading
12068            container.innerHTML = '<div id="cookie-declaration"></div>';
12069
12070            const script = document.createElement('script');
12071            script.src = scriptUrl;
12072            script.async = true;
12073
12074            script.onload = function() {
12075                console.log('Cookie declaration script loaded successfully');
12076            };
12077
12078            script.onerror = function() {
12079                console.error('Failed to load cookie declaration script');
12080                container.innerHTML = `
12081                    <div class="alert alert-warning">
12082                        <i class="fas fa-exclamation-triangle me-2"></i>
12083                        <strong>${translations['previewUnavailable'] || 'Preview Unavailable'}:</strong> ${translations['previewUnavailableDesc'] || 'Unable to load the script in preview mode due to CORS restrictions.'}
12084                        <br><br>
12085                        <div class="bg-light p-3 rounded mt-2">
12086                            <strong>${translations['yourScript'] || 'Your Script'}:</strong><br>
12087                            <code>${escapeHtml(scriptCode)}</code>
12088                        </div>
12089                        <br>
12090                        <small class="text-muted">
12091                            ${translations['scriptWorksOnWebsite'] || 'The script will work perfectly when implemented on your website. This preview limitation doesn\'t affect the actual functionality.'}
12092                        </small>
12093                    </div>
12094                `;
12095            };
12096
12097            document.body.appendChild(script);
12098
12099            // Clean up script after some time
12100            setTimeout(() => {
12101                if (document.body.contains(script)) {
12102                    document.body.removeChild(script);
12103                }
12104            }, 10000);
12105        };
12106
12107        container.innerHTML = '';
12108        container.appendChild(iframe);
12109
12110    } catch (error) {
12111        console.error('Error loading preview:', error);
12112        container.innerHTML = `
12113            <div class="alert alert-info">
12114                <i class="fas fa-info-circle me-2"></i>
12115                <strong>${translations['preview'] || 'Preview'}:</strong> ${translations['scriptReadyToUse'] || 'Your cookie declaration script is ready to use.'}
12116                <br><br>
12117                <div class="bg-light p-3 rounded mt-2">
12118                    <strong>${translations['yourScript'] || 'Your Script'}:</strong><br>
12119                    <code>${escapeHtml(scriptCode)}</code>
12120                </div>
12121                <br>
12122                <small class="text-muted">
12123                    ${translations['scriptDisplaysTablesDesc'] || 'When implemented on your website, this script will automatically display your cookie declaration tables.'}
12124                </small>
12125            </div>
12126        `;
12127    }
12128}
12129
12130function showInstructionsInCard() {
12131    const scriptContentArea = document.getElementById('scriptContentArea');
12132    const scriptLoadingContent = document.getElementById('scriptLoadingContent');
12133
12134    if (!scriptContentArea || !scriptLoadingContent) {
12135        console.error('Script content area not found');
12136        return;
12137    }
12138
12139    scriptContentArea.style.display = 'block';
12140
12141    scriptLoadingContent.innerHTML = `
12142        <div class="row">
12143            <div class="col-md-8">
12144                <div class="alert alert-info mb-4">
12145                    <h6 class="alert-heading">
12146                        <i class="fas fa-info-circle me-2"></i>${translations['whyNeedThis'] || 'Why Do You Need This?'}
12147                    </h6>
12148                    <p class="mb-2">${translations['legalComplianceDesc'] || 'Legal compliance regulations like GDPR, CCPA, and other privacy laws require websites to provide transparent information about all cookies they use. This script generates professional cookie declaration tables that:'}
12149                    </p>
12150                    <ul class="mb-0 small">
12151                        <li>${translations['listAllCookies'] || 'Lists all cookies used on your website in organized tables'}</li>
12152                        <li>${translations['showCookiePurposes'] || 'Shows cookie purposes, retention periods, and providers'}</li>
12153                        <li>${translations['helpComplyTransparency'] || 'Helps you comply with transparency requirements'}</li>
12154                        <li>${translations['linkFromPrivacyPolicy'] || 'Can be linked from your privacy policy or cookie policy'}</li>
12155                        <li>${translations['updatesAutomatically'] || 'Updates automatically as you modify your cookie configuration'}</li>
12156                    </ul>
12157                </div>
12158
12159                <h6 class="text-primary mb-3">
12160                    <i class="fas fa-rocket me-2"></i>${translations['howToAddDeclaration'] || 'How to Add Cookie Declaration to Your Website'}
12161                </h6>
12162
12163                <div class="mb-4">
12164                    <h6 class="fw-bold mb-2">${translations['step1CopyScript'] || 'Step 1: Copy the Script Tag'}</h6>
12165                    <p class="text-muted">${translations['step1Desc'] || 'Copy the script tag from the "Script Co
12165de" tab. This is a simple one-line HTML tag that you\'ll add to your website.'}</p>
12166                </div>
12167
12168                <div class="mb-4">
12169                    <h6 class="fw-bold mb-2">${translations['step2AddToHTML'] || 'Step 2: Add to Your Website\'s HTML'}</h6>
12170                    <p class="text-muted">${translations['step2Desc'] || 'Paste the script tag into your Cookie Declaration HTML where you want the tables to appear:'}</p>
12171                    <pre class="bg-light p-3 rounded"><code>&lt;!DOCTYPE html&gt;
12172&lt;html&gt;
12173&lt;head&gt;
12174    &lt;title&gt;Cookie Declaration&lt;/title&gt;
12175&lt;/head&gt;
12176&lt;body&gt;
12177    &lt;h1&gt;Our Cookie Declaration&lt;/h1&gt;
12178    &lt;div id="cookie-declaration"&gt;
12179        &lt;!-- Script will be inserted here --&gt;
12180    &lt;/div&gt;
12181&lt;/body&gt;
12182&lt;/html&gt;</code></pre>
12183                </div>
12184
12185                <div class="mb-4">
12186                    <h6 class="fw-bold mb-2">${translations['step3LinkFromPolicy'] || 'Step 3: Link from Your Privacy Policy'}</h6>
12187                    <p class="text-muted">${translations['step3Desc'] || 'Add a link to your cookie declaration page from your privacy policy, cookie policy, or website footer:'}</p>
12188                    <pre class="bg-light p-3 rounded"><code>&lt;a href="/cookies.html"&gt;View Our Cookie Declaration&lt;/a&gt;</code></pre>
12189                </div>
12190
12191                <div class="mb-4">
12192                    <h6 class="fw-bold mb-2">${translations['step4AutoUpdates'] || 'Step 4: Automatic Updates'}</h6>
12193                    <p class="text-muted">${translations['step4Desc'] || 'The script automatically updates when you modify your cookie configuration in this manager. No need to manually update your website - it stays current automatically!'}</p>
12194                </div>
12195            </div>
12196
12197            <div class="col-md-4">
12198                <div class="card bg-light">
12199                    <div class="card-body">
12200                        <h6 class="card-title text-success">
12201                            <i class="fas fa-shield-check me-2"></i>${translations['legalComplianceBenefits'] || 'Legal Compliance Benefits'}
12202                        </h6>
12203                        <ul class="list-unstyled mb-3 small">
12204                            <li class="mb-2"><i class="fas fa-check text-success me-2"></i>${translations['gdprCompliance'] || 'GDPR Article 13 compliance'}</li>
12205                            <li class="mb-2"><i class="fas fa-check text-success me-2"></i>${translations['ccpaCompliance'] || 'CCPA transparency requirements'}</li>
12206                            <li class="mb-2"><i class="fas fa-check text-success me-2"></i>${translations['cookieLawCompliance'] || 'Cookie law disclosure obligations'}</li>
12207                            <li class="mb-0"><i class="fas fa-check text-success me-2"></i>${translations['isoCompliance'] || 'ISO 27001 documentation standards'}</li>
12208                        </ul>
12209
12210                        <h6 class="card-title text-info mt-4">
12211                            <i class="fas fa-star me-2"></i>${translations['features'] || 'Features'}
12212                        </h6>
12213                        <ul class="list-unstyled mb-0 small">
12214                            <li class="mb-2"><i class="fas fa-mobile-alt text-primary me-2"></i>${translations['mobileResponsive'] || 'Mobile responsive design'}</li>
12215                            <li class="mb-2"><i class="fas fa-palette text-info me-2"></i>${translations['professionalStyling'] || 'Professional styling'}</li>
12216                            <li class="mb-2"><i class="fas fa-layer-group text-warning me-2"></i>${translations['organizedByCategories'] || 'Organized by categories'}</li>
12217                            <li class="mb-2"><i class="fas fa-link text-success me-2"></i>${translations['privacyPolicyLinks'] || 'Privacy policy links'}</li>
12218                            <li class="mb-0"><i class="fas fa-sync text-secondary me-2"></i>${translations['autoUpdatesWithChanges'] || 'Auto-updates with changes'}</li>
12219                        </ul>
12220                    </div>
12221                </div>
12222
12223                <div class="alert alert-warning mt-3">
12224                    <i class="fas fa-lightbulb me-2"></i>
12225                    <strong>${translations['important'] || 'Important'}:</strong> ${translations['scriptGeneratesTablesDesc'] || 'This script generates tables based on your current cookie configuration.'}
12226                </div>
12227
12228                <div class="alert alert-info mt-3">
12229                    <i class="fas fa-users me-2"></i>
12230                    <strong>${translations['userFriendly'] || 'User-Friendly'}:</strong> ${translations['userFriendlyDesc'] || 'Your website visitors will see clean, organized tables showing exactly what cookies your site uses and why - building trust and meeting legal requirements.'}
12231                </div>
12232            </div>
12233        </div>
12234    `;
12235}
12236
12237function escapeHtml(text) {
12238    const map = {
12239        '&': '&amp;',
12240        '<': '&lt;',
12241        '>': '&gt;',
12242        '"': '&quot;',
12243        "'": '&#039;'
12244    };
12245    return text.replace(/[&<>"']/g, m => map[m]);
12246}
12247
12248function hideCookiePreview() {
12249    const scriptContentArea = document.getElementById('scriptContentArea');
12250    if (scriptContentArea) {
12251        scriptContentArea.style.display = 'none';
12252    }
12253}
12254
12255function copyScriptFromPreview() {
12256    const scriptCode = window.currentCookieScript;
12257
12258    if (!scriptCode) {
12259        displayAlertCookie(translations['noScriptToCopy'] || 'No script to copy', 'error');
12260        return;
12261    }
12262
12263    navigator.clipboard.writeText(scriptCode).then(() => {
12264        displayAlertCookie(translations['scriptCopied'] || 'Script copied to clipboard!', 'success');
12265    }).catch(() => {
12266        // Fallback copy method
12267        const textarea = document.createElement('textarea');
12268        textarea.value = scriptCode;
12269        textarea.style.position = 'fixed';
12270        textarea.style.opacity = '0';
12271        document.body.appendChild(textarea);
12272        textarea.select();
12273        document.execCommand('copy');
12274        document.body.removeChild(textarea);
12275        displayAlertCookie(translations['scriptCopied'] || 'Script copied to clipboard!', 'success');
12276    });
12277}
12278
12279async function showCookieDeclarationPreview(domainId) {
12280    const scriptContentArea = document.getElementById('scriptContentArea');
12281    const scriptLoadingContent = document.getElementById('scriptLoadingContent');
12282
12283    if (!scriptContentArea || !scriptLoadingContent) {
12284        console.error('Script content area not found');
12285        return;
12286    }
12287
12288    scriptContentArea.style.display = 'block';
12289
12290    // Show loading state
12291    scriptLoadingContent.innerHTML = `
12292        <div class="text-center py-4">
12293            <div class="spinner-border text-primary" role="status">
12294                <span class="visually-hidden">${translations['loading'] || 'Loading...'}</span>
12295            </div>
12296            <p class="mt-3 text-muted">${translations['loadingPreview'] || 'Loading preview...'}</p>
12297        </div>
12298    `;
12299
12300    try {
12301        const domainDropdown = document.getElementById('select_domain');
12302        if (!domainDropdown || !domainDropdown.value) {
12303            throw new Error(translations['noDomainSelected'] || 'No domain selected');
12304        }
12305
12306        const currentDomainId = domainDropdown.value;
12307        const projects = await getProjects();
12308
12309        if (!projects || !projects.length) {
12310            throw new Error(translations['noProjectsAvailable'] || 'No projects available');
12311        }
12312
12313        const projectId = findProjectIdByDomainId(currentDomainId, projects);
12314        if (!projectId) {
12315            throw new Error(translations['projectNotFound'] || 'Project not found for selected domain');
12316        }
12317
12318        const data = {
12319            sid: getSid(),
12320            projectId: projectId
12321        };
12322
12323        const response = await postRequest(endpoint + '/scriptcode', data);
12324
12325        if (response && response.ok) {
12326            const responseBody = await response.json();
12327            if (responseBody.statusCode === 0) {
12328                // Store both scripts globally
12329                window.currentCookieScript = responseBody.cookiesScriptCode;
12330                window.currentPageCode = responseBody.pageCode;
12331
12332                // Display the preview with both scripts
12333                scriptLoadingContent.innerHTML = generatePreviewContent(
12334                    responseBody.cookiesScriptCode,
12335                    responseBody.pageCode
12336                );
12337
12338            } else {
12339                throw new Error(responseBody.statusMessage || 'Server error');
12340            }
12341        } else {
12342            throw new Error(translations['failedToLoadScript'] || 'Failed to load script');
12343        }
12344    } catch (error) {
12345        console.error('Error loading preview:', error);
12346        scriptLoadingContent.innerHTML = `
12347            <div class="alert alert-danger">
12348                <i class="fas fa-exclamation-triangle me-2"></i>
12349                <strong>${translations['error'] || 'Error'}:</strong> ${error.message}
12350            </div>
12351        `;
12352    }
12353}
12354
12355function generatePreviewContent(scriptCode, pageCode) {
12356    const uniquePreviewId = 'cookie-declaration-preview-' + Date.now();
12357
12358    const previewHtml = `
12359        <div class="mb-4">
12360            <div class="d-flex justify-content-between align-items-center mb-3">
12361                <h6 class="mb-0 text-primary">
12362                    <i class="fas fa-eye me-2"></i>${translations['livePreviewTitle'] || 'Live Preview - Exactly How It Will Look on Your Website'}
12363                </h6>
12364                <button type="button" class="btn btn-sm btn-outline-primary" onclick="hideCookiePreview()">
12365                    <i class="fas fa-times me-2"></i>${translations['closePreview'] || 'Close Preview'}
12366                </button>
12367            </div>
12368
12369            <div class="alert alert-info mb-3">
12370                <i class="fas fa-info-circle me-2"></i>
12371                <strong>${translations['previewNote'] || 'Preview Note'}:</strong>
12372                ${translations['livePreviewDesc'] || 'This preview shows exactly how the cookie declaration will appear on your website.'}
12373            </div>
12374
12375            <!-- Script Code Display -->
12376            <div class="mb-3">
12377                <div class="d-flex justify-content-between align-items-center mb-2">
12378                    <h6 class="text-muted mb-0">
12379                        <i class="fas fa-code me-1"></i>${translations['yourScriptCode'] || 'Your Script Code:'}
12380                    </h6>
12381                    <button class="btn btn-sm btn-primary" onclick="copyScriptFromPreview()">
12382                        <i class="fas fa-copy me-1"></i>${translations['copyScript'] || 'Copy Script'}
12383                    </button>
12384                </div>
12385                <div class="bg-light p-3 rounded border">
12386                    <code class="d-block" style="white-space: pre-wrap; word-break: break-all; font-size: 0.85em;">
12387                        ${escapeHtml(scriptCode)}
12388                    </code>
12389                </div>
12390            </div>
12391
12392            <!-- Preview Container -->
12393            <div class="border rounded p-4 bg-white" style="max-height: 600px; overflow-y: auto;">
12394                <div id="${uniquePreviewId}">
12395                    <div class="text-center py-4">
12396                        <div class="spinner-border text-primary" role="status">
12397                            <span class="visually-hidden">${translations['loadingPreview'] || 'Loading preview...'}</span>
12398                        </div>
12399                        <p class="mt-2 text-muted">${translations['loadingPreviewDesc'] || 'Loading cookie declaration preview...'}</p>
12400                    </div>
12401                </div>
12402            </div>
12403
12404            <div class="mt-3">
12405                <div class="alert alert-success">
12406                    <i class="fas fa-check-circle me-2"></i>
12407                    <strong>${translations['readyToUse'] || 'Ready to Use'}:</strong> ${translations['readyToUseDesc'] || 'This is exactly what your website visitors will see.'}
12408                </div>
12409            </div>
12410        </div>
12411    `;
12412
12413    // Load the complex script after a short delay
12414    setTimeout(() => {
12415        loadComplexScriptPreview(scriptCode, pageCode, uniquePreviewId);
12416    }, 100);
12417
12418    return previewHtml;
12419}
12420
12421function loadComplexScriptPreview(cookiesScriptCode, pageCode, containerId) {
12422    const container = document.getElementById(containerId);
12423    if (!container) {
12424        console.error('Preview container not found');
12425        return;
12426    }
12427
12428    try {
12429        // Always treat as complex script
12430        const iframe = document.createElement('iframe');
12431        iframe.style.width = '100%';
12432        iframe.style.minHeight = '500px';
12433        iframe.style.border = 'none';
12434        iframe.style.backgroundColor = 'white';
12435
12436        // Use minimal sandboxing to allow the scripts to work
12437        iframe.sandbox = 'allow-scripts allow-same-origin allow-popups allow-forms';
12438
12439        // Build the complete HTML with both pageCode and cookiesScriptCode
12440        const iframeContent = `
12441            <!DOCTYPE html>
12442            <html>
12443            <head>
12444                <meta charset="UTF-8">
12445                <meta name="viewport" content="width=device-width, initial-scale=1.0">
12446                <style>
12447                    * {
12448                        margin: 0;
12449                        padding: 0;
12450                        box-sizing: border-box;
12451                    }
12452                    body {
12453                        font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
12454                        padding: 20px;
12455                        background: white;
12456                        color: #333;
12457                        line-height: 1.6;
12458                    }
12459                    .cookie-declaration-wrapper {
12460                        max-width: 1200px;
12461                        margin: 0 auto;
12462                    }
12463                    table {
12464                        width: 100%;
12465                        border-collapse: collapse;
12466                        margin: 20px 0;
12467                        background: white;
12468                        box-shadow: 0 1px 3px rgba(0,0,0,0.1);
12469                    }
12470                    th, td {
12471                        padding: 12px 15px;
12472                        text-align: left;
12473                        border: 1px solid #e0e0e0;
12474                    }
12475                    th {
12476                        background-color: #f8f9fa;
12477                        font-weight: 600;
12478                        color: #2c3e50;
12479                        text-transform: uppercase;
12480                        font-size: 0.85em;
12481                        letter-spacing: 0.5px;
12482                    }
12483                    tr:nth-child(even) {
12484                        background-color: #fafafa;
12485                    }
12486                    tr:hover {
12487                        background-color: #f0f0f0;
12488                    }
12489                    .cookie-category {
12490                        margin: 30px 0;
12491                    }
12492                    .category-title {
12493                        font-size: 1.4em;
12494                        font-weight: 600;
12495                        margin-bottom: 15px;
12496                        color: #2c3e50;
12497                        border-bottom: 2px solid #3498db;
12498                        padding-bottom: 10px;
12499                    }
12500                    .cookie-description {
12501                        margin: 10px 0;
12502                        color: #666;
12503                        font-size: 0.95em;
12504                    }
12505                    /* Specific styles for cookie declaration elements */
12506                    #cookie-declaration {
12507                        min-height: 100px;
12508                    }
12509                    .cookie-table-container {
12510                        margin: 20px 0;
12511                    }
12512                    .cookie-policy-link {
12513                        color: #3498db;
12514                        text-decoration: none;
12515                    }
12516                    .cookie-policy-link:hover {
12517                        text-decoration: underline;
12518                    }
12519                    /* Loading state */
12520                    .loading {
12521                        text-align: center;
12522                        padding: 40px;
12523                        color: #999;
12524                    }
12525                    /* Error state */
12526                    .error-message {
12527                        background: #fee;
12528                        border: 1px solid #fcc;
12529                        color: #c33;
12530                        padding: 15px;
12531                        border-radius: 4px;
12532                        margin: 20px 0;
12533                    }
12534                </style>
12535                ${pageCode ? `
12536                <!-- Page Code (Additional Scripts/Styles) -->
12537                ${pageCode}
12538                ` : ''}
12539            </head>
12540            <body>
12541                <div class="cookie-declaration-wrapper">
12542                    <div id="cookie-declaration">
12543                        <!-- Cookie tables will be injected here -->
12544                    </div>
12545                </div>
12546
12547                <!-- Cookie Declaration Script -->
12548                ${cookiesScriptCode}
12549
12550                <!-- Fallback detection script -->
12551                <script>
12552                    // Check if content was loaded after 3 seconds
12553                    setTimeout(function() {
12554                        var container = document.getElementById('cookie-declaration');
12555                        if (container && container.innerHTML.trim() === '') {
12556                            container.innerHTML = '<div class="error-message">Cookie declaration is loading... If this message persists, please check your browser console for errors.</div>';
12557                        }
12558                    }, 3000);
12559
12560                    // Auto-adjust iframe height
12561                    function adjustHeight() {
12562                        if (window.parent && window.parent.postMessage) {
12563                            var height = document.body.scrollHeight;
12564                            window.parent.postMessage({
12565                                type: 'adjustHeight',
12566                                height: height
12567                            }, '*');
12568                        }
12569                    }
12570
12571                    // Adjust height on load and after content changes
12572                    window.addEventListener('load', adjustHeight);
12573                    setTimeout(adjustHeight, 1000);
12574                    setTimeout(adjustHeight, 2000);
12575                    setTimeout(adjustHeight, 5000);
12576
12577                    // Watch for DOM changes
12578                    var observer = new MutationObserver(adjustHeight);
12579                    observer.observe(document.body, {
12580                        childList: true,
12581                        subtree: true
12582                    });
12583                </script>
12584            </body>
12585            </html>
12586        `;
12587
12588        iframe.srcdoc = iframeContent;
12589
12590        // Listen for height adjustment messages
12591        window.addEventListener('message', function(e) {
12592            if (e.data && e.data.type === 'adjustHeight') {
12593                iframe.style.height = (e.data.height + 50) + 'px';
12594            }
12595        });
12596
12597        iframe.onload = function() {
12598            console.log('Preview iframe loaded successfully');
12599        };
12600
12601        iframe.onerror = function() {
12602            console.error('Preview iframe failed to load');
12603            showFallbackPreview(container, cookiesScriptCode);
12604        };
12605
12606        container.innerHTML = '';
12607        container.appendChild(iframe);
12608
12609    } catch (error) {
12610        console.error('Error loading preview:', error);
12611        showFallbackPreview(container, cookiesScriptCode);
12612    }
12613}
12614
12615function showFallbackPreview(container, scriptCode) {
12616    container.innerHTML = `
12617        <div class="alert alert-warning">
12618            <h6 class="alert-heading">
12619                <i class="fas fa-exclamation-triangle me-2"></i>${translations['previewLimitations'] || 'Preview Limitations'}
12620            </h6>
12621            <p>${translations['previewCannotBeDisplayed'] || 'The preview cannot be displayed due to browser security restrictions, but your script is ready and will work perfectly on your website.'}</p>
12622            <div class="bg-light p-3 rounded mt-3">
12623                <strong>${translations['yourGeneratedScript'] || 'Your Generated Script'}:</strong>
12624                <pre class="mb-0 mt-2" style="white-space: pre-wrap; word-break: break-all;">
12625                    <code>${escapeHtml(scriptCode)}</code>
12626                </pre>
12627            </div>
12628            <hr>
12629            <div class="d-flex justify-content-between align-items-center">
12630                <small class="text-muted">
12631                    <i class="fas fa-shield-alt me-1"></i>
12632                    ${translations['browserSecurityFeature'] || 'This is a browser security feature and doesn\'t affect the actual functionality.'}
12633                </small>
12634                <button class="btn btn-sm btn-primary" onclick="copyScriptFromPreview()">
12635                    <i class="fas fa-copy me-1"></i>${translations['copyScript'] || 'Copy Script'}
12636                </button>
12637            </div>
12638        </div>
12639    `;
12640}
12641
12642// ── Banner copy experiment ──────────────────────────────────────────────────
12643// Two arms only in the UI: A is the project's own text, B is a variant. The
12644// server's model allows more, but a merchant comparing three wordings on consent
12645// copy will not reach a usable sample on any of them, and the report would spend
12646// its life saying "collecting".
12647function fcLoadExperiment(exp) {
12648	const field = document.getElementById('experiment_field');
12649	const weight = document.getElementById('experiment_weight');
12650	const running = document.getElementById('experiment_running');
12651	const valueB = document.getElementById('experiment_value_b');
12652	if (!field || !weight || !running || !valueB) return;
12653
12654	exp = exp || {};
12655	const arms = exp.arms || [];
12656	const b = arms.length > 1 ? arms[1] : null;
12657	field.value = exp.field === 'homeDescription' ? 'homeDescription' : 'homeTitle';
12658	running.checked = exp.status === 'running';
12659	valueB.value = b && b.value != null ? b.value : '';
12660	// Round to the nearest offered split rather than showing a value the select
12661	// cannot represent — an unrepresentable <select> value posts "" (the bug the
12662	// consent-mode normalizer exists for).
12663	const pct = b && arms[0] ? Math.round((b.weight * 100) / (b.weight + arms[0].weight)) : 50;
12664	weight.value = [10, 25, 50].reduce((a, c) => (Math.abs(c - pct) < Math.abs(a - pct) ? c : a), 50);
12665	// Keep the id across saves so the stat rows already collected stay comparable;
12666	// mint one only when there is no test yet.
12667	fcExperimentId = exp.id || '';
12668}
12669
12670let fcExperimentId = '';
12671
12672function fcCollectExperiment() {
12673	const field = document.getElementById('experiment_field');
12674	const weight = document.getElementById('experiment_weight');
12675	const running = document.getElementById('experiment_running');
12676	const valueB = document.getElementById('experiment_value_b');
12677	if (!field || !weight || !running || !valueB) return undefined;
12678
12679	const b = parseInt(weight.value, 10) || 50;
12680	const text = valueB.value.trim();
12681	// A running test with no variant text is not a test — B would be identical to A
12682	// and the report would compare a wording with itself.
12683	if (!text) return {};
12684	if (!fcExperimentId) fcExperimentId = 'exp-' + Date.now().toString(36);
12685	return {
12686		id: fcExperimentId,
12687		field: field.value,
12688		status: running.checked ? 'running' : 'stopped',
12689		arms: [{ id: 'a', weight: 100 - b, value: null }, { id: 'b', weight: b, value: text }]
12690	};
12691}
12692
12693// Report. Renders only what the server was willing to state: an arm below the
12694// visitor floor shows no rate at all, and 'no-difference' is printed as plainly as
12695// a winner would be.
12696async function loadExperimentReport(projectId, startDate, endDate) {
12697	const box = document.getElementById('experiment_report');
12698	if (!box) return;
12699	box.innerHTML = '';
12700	if (!projectId || projectId === '0') return;
12701
12702	const response = await postRequest(endpoint + '/cmp/data-stats-experiment',
12703		{ sid: getSid(), projectId: projectId, startDate: startDate, endDate: endDate });
12704	if (!response || !response.ok) return;
12705	const body = await response.json();
12706	if (body.statusCode !== 0 || !body.arms || !body.arms.length) return;
12707
12708	const card = document.createElement('div');
12709	card.classList.add('card', 'card-std');
12710	const inner = document.createElement('div');
12711	inner.classList.add('card-body');
12712	card.appendChild(inner);
12713
12714	const h = document.createElement('h6');
12715	h.textContent = translations['experimentResultsTitle'] || 'Results';
12716	inner.appendChild(h);
12717
12718	const table = document.createElement('table');
12719	table.classList.add('table', 'table-sm', 'mb-2');
12720	const head = document.createElement('tr');
12721	[translations['experimentArm'] || 'Version',
12722	 translations['visitors'] || 'Visitors',
12723	 translations['experimentDecided'] || 'Made a choice',
12724	 translations['experimentCompletion'] || 'Completion rate'].forEach(function (t, i) {
12725		const th = document.createElement('th');
12726		th.textContent = t;
12727		if (i > 0) th.classList.add('text-end');
12728		head.appendChild(th);
12729	});
12730	const thead = document.createElement('thead');
12731	thead.appendChild(head);
12732	table.appendChild(thead);
12733	const tbody = document.createElement('tbody');
12734	table.appendChild(tbody);
12735
12736	body.arms.forEach(function (a) {
12737		const tr = document.createElement('tr');
12738		const name = document.createElement('td');
12739		name.textContent = a.arm === 'a'
12740			? (translations['experimentVersionA'] || 'A (current text)')
12741			: (translations['experimentVersionBShort'] || 'B (variant)');
12742		tr.appendChild(name);
12743		[String(a.clients), String(a.decided),
12744		 a.enoughData
12745			? Math.round(a.completionRate * 1000) / 10 + '% ±' + Math.round(a.completionMargin * 1000) / 10
12746			: (translations['experimentNotEnough'] || 'not enough data yet')
12747		].forEach(function (v) {
12748			const td = document.createElement('td');
12749			td.classList.add('text-end');
12750			td.textContent = v;
12751			tr.appendChild(td);
12752		});
12753		tbody.appendChild(tr);
12754	});
12755	inner.appendChild(table);
12756
12757	const verdict = document.createElement('p');
12758	verdict.classList.add('mb-0');
12759	const v = body.verdict || {};
12760	if (v.state === 'leader') {
12761		verdict.textContent = (translations['experimentLeader'] || 'More visitors made a choice with version [Arm].')
12762			.replace('[Arm]', v.arm === 'a' ? 'A' : 'B');
12763	} else if (v.state === 'no-difference') {
12764		verdict.textContent = translations['experimentNoDifference']
12765			|| 'No meaningful difference between the two versions yet.';
12766	} else {
12767		verdict.textContent = translations['experimentCollecting']
12768			|| 'Still collecting — each version needs at least 100 visitors before a result is shown.';
12769	}
12770	verdict.classList.add('text-muted');
12771	verdict.style.fontSize = '.9rem';
12772	inner.appendChild(verdict);
12773
12774	box.appendChild(card);
12775}
12776
12777// ───────────────────────────────────────────────── §9 privacy requests (DSAR)
12778//
12779// The merchant is the data controller; this is the tool that lets them answer a
12780// visitor's access or erasure request. Everything server-side is scoped to a project
12781// they own, and a key that belongs to somebody else's project answers exactly the same
12782// as a key that does not exist — so nothing here can be used to probe for one.
12783
12784function fcPrivacyBox() {
12785	return document.getElementById('privacy_result');
12786}
12787
12788function fcPrivacyMessage(text, kind) {
12789	const box = fcPrivacyBox();
12790	if (!box) return;
12791	box.innerHTML = '';
12792	const div = document.createElement('div');
12793	div.classList.add('alert', kind || 'alert-secondary', 'mb-0', 'py-2');
12794	div.style.fontSize = '.9rem';
12795	div.textContent = text;
12796	box.appendChild(div);
12797}
12798
12799function fcPrivacyRow(table, label, value) {
12800	if (value === null || value === undefined || value === '') return;
12801	const tr = document.createElement('tr');
12802	const th = document.createElement('th');
12803	th.style.width = '45%';
12804	th.style.fontWeight = '500';
12805	th.textContent = label;
12806	const td = document.createElement('td');
12807	td.textContent = String(value);
12808	tr.appendChild(th);
12809	tr.appendChild(td);
12810	table.appendChild(tr);
12811}
12812
12813async function fcPrivacyLookup() {
12814	const input = document.getElementById('privacy_client_key');
12815	// The page identifies its project by the ?id= query param (see the loader above);
12816	// there is no #project_id element to read.
12817	const projectId = new URLSearchParams(window.location.search).get('id');
12818	if (!input) return;
12819	const key = (input.value || '').trim().toLowerCase();
12820	input.value = key;
12821
12822	// Checked here as well as on the server, so an obvious typo costs no round trip.
12823	if (!/^[a-z0-9]{64}$/.test(key)) {
12824		fcPrivacyMessage(translations['privacyKeyInvalid']
12825			|| 'That does not look like a visitor key. It is exactly 64 characters, letters and digits.',
12826			'alert-warning');
12827		return;
12828	}
12829	fcPrivacyMessage(translations['privacyLooking'] || 'Looking…');
12830
12831	const response = await postRequest(endpoint + '/cmp/privacy-lookup', { sid: getSid(), projectId: projectId, clientKey: key });
12832	if (!response || !response.ok) {
12833		fcPrivacyMessage(translations['privacyLookupFailed'] || 'Could not reach the server. Try again.', 'alert-danger');
12834		return;
12835	}
12836	const body = await response.json();
12837	if (body.statusCode !== 0) {
12838		fcPrivacyMessage(body.statusMessage || (translations['privacyLookupFailed'] || 'Lookup failed.'), 'alert-danger');
12839		return;
12840	}
12841	if (!body.found) {
12842		fcPrivacyMessage(translations['privacyNotFound'] || 'No record for that key on this project.', 'alert-secondary');
12843		return;
12844	}
12845
12846	const box = fcPrivacyBox();
12847	box.innerHTML = '';
12848	const card = document.createElement('div');
12849	card.classList.add('card', 'card-std');
12850	const inner = document.createElement('div');
12851	inner.classList.add('card-body');
12852	card.appendChild(inner);
12853
12854	const h = document.createElement('h6');
12855	h.textContent = translations['privacyRecordFound'] || 'Record found';
12856	inner.appendChild(h);
12857
12858	const table = document.createElement('table');
12859	table.classList.add('table', 'table-sm', 'mb-3');
12860	fcPrivacyRow(table, translations['privacyFirstSeen'] || 'First seen', body.firstSeen);
12861	fcPrivacyRow(table, translations['privacyLastSeen'] || 'Last seen', body.lastSeen);
12862	fcPrivacyRow(table, translations['privacyDomains'] || 'Domains', body.domains);
12863	fcPrivacyRow(table, translations['privacyActivityDays'] || 'Days with activity', body.activityDays);
12864	fcPrivacyRow(table, translations['privacyConsentRecords'] || 'Consent records', body.consentRecords);
12865	inner.appendChild(table);
12866
12867	const actions = document.createElement('div');
12868	actions.classList.add('d-flex', 'gap-2');
12869
12870	const exportBtn = document.createElement('button');
12871	exportBtn.type = 'button';
12872	exportBtn.classList.add('btn', 'btn-sm', 'btn-outline-primary');
12873	exportBtn.textContent = translations['privacyDownload'] || 'Download their data (JSON)';
12874	exportBtn.addEventListener('click', function () { fcPrivacyExport(key, projectId); });
12875	actions.appendChild(exportBtn);
12876
12877	const eraseBtn = document.createElement('button');
12878	eraseBtn.type = 'button';
12879	eraseBtn.classList.add('btn', 'btn-sm', 'btn-outline-danger');
12880	eraseBtn.textContent = translations['privacyErase'] || 'Erase this record';
12881	eraseBtn.addEventListener('click', function () { fcPrivacyErase(key, projectId); });
12882	actions.appendChild(eraseBtn);
12883
12884	inner.appendChild(actions);
12885	box.appendChild(card);
12886}
12887
12888async function fcPrivacyExport(key, projectId) {
12889	const response = await postRequest(endpoint + '/cmp/privacy-export', { sid: getSid(), projectId: projectId, clientKey: key });
12890	if (!response || !response.ok) {
12891		fcPrivacyMessage(translations['privacyLookupFailed'] || 'Could not reach the server. Try again.', 'alert-danger');
12892		return;
12893	}
12894	const body = await response.json();
12895	if (body.statusCode !== 0 || !body.found || !body.export) {
12896		fcPrivacyMessage(translations['privacyNotFound'] || 'No record for that key on this project.', 'alert-secondary');
12897		return;
12898	}
12899	// Hand the merchant a file they can forward to the person who asked. The key is in
12900	// the filename because they may be handling several requests at once, truncated
12901	// because the whole thing is not a useful filename.
12902	const blob = new Blob([JSON.stringify(body.export, null, 2)], { type: 'application/json' });
12903	const a = document.createElement('a');
12904	a.href = URL.createObjectURL(blob);
12905	a.download = 'visitor-data-' + key.slice(0, 12) + '.json';
12906	document.body.appendChild(a);
12907	a.click();
12908	document.body.removeChild(a);
12909	setTimeout(function () { URL.revokeObjectURL(a.href); }, 0);
12910}
12911
12912async function fcPrivacyErase(key, projectId) {
12913	const warning = translations['privacyEraseConfirm']
12914		|| 'Erase this visitor\'s record? Their identifier and consent choice are deleted and cannot be recovered. '
12915		+ 'Your daily reports keep their numbers.';
12916	if (!window.confirm(warning)) return;
12917
12918	const response = await postRequest(endpoint + '/cmp/privacy-erase', { sid: getSid(), projectId: projectId, clientKey: key });
12919	if (!response || !response.ok) {
12920		fcPrivacyMessage(translations['privacyEraseFailed'] || 'Erase failed. Nothing was deleted.', 'alert-danger');
12921		return;
12922	}
12923	const body = await response.json();
12924	if (body.statusCode !== 0 || !body.found) {
12925		fcPrivacyMessage(translations['privacyNotFound'] || 'No record for that key on this project.', 'alert-secondary');
12926		return;
12927	}
12928
12929	const r = body.receipt || {};
12930	const box = fcPrivacyBox();
12931	box.innerHTML = '';
12932	const div = document.createElement('div');
12933	div.classList.add('alert', 'alert-success', 'mb-0');
12934	div.style.fontSize = '.9rem';
12935
12936	const strong = document.createElement('div');
12937	strong.style.fontWeight = '600';
12938	strong.textContent = translations['privacyErased'] || 'Record erased.';
12939	div.appendChild(strong);
12940
12941	const counts = document.createElement('div');
12942	counts.classList.add('mt-1');
12943	const deleted = r.rowsDeleted || {};
12944	// The translation pipeline strips trailing whitespace from every value, so the space before
12945	// each number is added here rather than trusted to the text.
12946	counts.textContent = (translations['privacyErasedCounts'] || 'Consent records deleted:').trimEnd() + ' '
12947		+ (deleted.client_consent || 0) + ' · '
12948		+ (translations['privacyStatRowsKept'] || 'daily rows kept and now anonymous:').trimEnd() + ' '
12949		+ (r.statRowsRetained || 0);
12950	div.appendChild(counts);
12951
12952	// Said here rather than left to be discovered: the key is in the visitor's browser
12953	// and we cannot clear it. A merchant who assumes otherwise would see the "erased"
12954	// visitor reappear and conclude the erasure failed.
12955	if (r.visitorMayReturn) {
12956		const note = document.createElement('div');
12957		note.classList.add('mt-2', 'text-muted');
12958		note.style.fontSize = '.85rem';
12959		note.textContent = translations['privacyVisitorMayReturn'] || r.visitorMayReturn;
12960		div.appendChild(note);
12961	}
12962	box.appendChild(div);
12963}
12964
12965// ───────────────────────────────────────────────── §10 cookie policy from the real scan
12966//
12967// The generated cookie policy used to describe a site that used cookies without ever
12968// saying which. This builds that section from what the scanner actually found, and is
12969// appended to the template output rather than requiring a {trackerList} placeholder in
12970// every one of the stored template translations — a placeholder-only design would sit
12971// inert until somebody edited all of them, which is exactly how the embed-placeholder
12972// switch stayed dead for a fortnight.
12973
12974function fcPolicyEsc(v) {
12975	return escapeHtml(String(v === null || v === undefined ? '' : v));
12976}
12977
12978/**
12979 * Read the short code off the selected language option. The dropdown's VALUE is a numeric
12980 * language id, but each option already carries its shortCode as a data attribute, so no
12981 * id-to-code mapping has to be invented or kept in sync here.
12982 */
12983function fcSelectedLanguageCode() {
12984	const sel = document.getElementById('select_language');
12985	if (!sel || sel.selectedIndex < 0) return '';
12986	const opt = sel.options[sel.selectedIndex];
12987	return (opt && opt.dataset && opt.dataset.languageCode) ? String(opt.dataset.languageCode).toLowerCase() : '';
12988}
12989
12990/** Fetch the composed policy for a domain. Returns null when there is nothing to add. */
12991async function fcFetchCookiePolicy(domainId, lang) {
12992	if (!domainId) return null;
12993	const response = await postRequest(endpoint + '/cmp/cookie-policy',
12994		{ sid: getSid(), domainId: domainId, lang: lang || '' });
12995	if (!response || !response.ok) return null;
12996	const body = await response.json();
12997	if (body.statusCode !== 0 || !body.policy) return null;
12998	return body.policy;
12999}
13000
13001/**
13002 * Render the tracker section as HTML for the document.
13003 *
13004 * Renders the freshness note in every state, including — especially — the states with no
13005 * trackers to show. A merchant whose site has never been scanned must not receive a
13006 * document that silently reads as "this site sets no cookies".
13007 */
13008// Escaping keeps a value inside the attribute, but href="javascript:..." is still a
13009// javascript: URL once escaped. Only http and https reach an href; anything else drops the
13010// link and keeps the provider name, which is the part that carries meaning.
13011function fcPolicyHref(url) {
13012	const u = String(url || '').trim();
13013	return /^https?:\/\//i.test(u) ? u : '';
13014}
13015
13016function fcCookiePolicySectionHtml(policy) {
13017	if (!policy) return '';
13018	const t = (k, d) => translations[k] || d;
13019	const f = policy.freshness || {};
13020	let html = '<h2>' + fcPolicyEsc(t('policyTrackersHeading', 'Cookies and trackers on this site')) + '</h2>';
13021
13022	if (f.note) {
13023		const warn = (f.state === 'never' || f.state === 'stale' || f.state === 'empty');
13024		html += '<p style="' + (warn ? 'padding:8px;border-left:3px solid #b8860b;background:#fdf6e3;' : '')
13025			+ 'font-size:.95em;">' + fcPolicyEsc(f.note) + '</p>';
13026	}
13027
13028	const sections = policy.sections || [];
13029	for (let i = 0; i < sections.length; i++) {
13030		const s = sections[i];
13031		html += '<h3>' + fcPolicyEsc(t('policyCategory_' + s.category, s.category)) + '</h3>';
13032		html += '<p>' + fcPolicyEsc(s.purpose) + '</p>';
13033		html += '<p><em>' + fcPolicyEsc(s.consentRequired
13034			? t('policyConsentRequired', 'These run only after you agree.')
13035			: t('policyNoConsentRequired', 'These run without asking, because the site cannot work without them.'))
13036			+ '</em></p>';
13037		html += '<table border="1" cellspacing="0" cellpadding="6" style="border-collapse:collapse;width:100%;font-size:.9em;">'
13038			+ '<thead><tr>'
13039			+ '<th align="left">' + fcPolicyEsc(t('policyColName', 'Name')) + '</th>'
13040			+ '<th align="left">' + fcPolicyEsc(t('policyColProvider', 'Provider')) + '</th>'
13041			+ '<th align="left">' + fcPolicyEsc(t('policyColPurpose', 'Purpose')) + '</th>'
13042			+ '<th align="left">' + fcPolicyEsc(t('policyColStorage', 'Type')) + '</th>'
13043			+ '<th align="left">' + fcPolicyEsc(t('policyColRetention', 'Retention')) + '</th>'
13044			+ '</tr></thead><tbody>';
13045		const rows = s.trackers || [];
13046		for (let j = 0; j < rows.length; j++) {
13047			const r = rows[j];
13048			let provider = fcPolicyEsc(r.provider);
13049			const href = fcPolicyHref(r.providerPrivacyUrl);
13050			if (href)
13051				provider += ' (<a href="' + fcPolicyEsc(href) + '" rel="noopener noreferrer">'
13052					+ fcPolicyEsc(t('policyProviderPolicy', 'privacy policy')) + '</a>)';
13053			let name = fcPolicyEsc(r.name);
13054			if (r.httpOnly)
13055				// Worth flagging: a visitor cannot inspect or clear these from the page.
13056				name += ' <small>' + fcPolicyEsc(t('policyHttpOnly', '(server-set)')) + '</small>';
13057			html += '<tr><td>' + name + '</td><td>' + provider + '</td><td>' + fcPolicyEsc(r.purpose)
13058				+ '</td><td>' + fcPolicyEsc(r.storage) + '</td><td>' + fcPolicyEsc(r.retention) + '</td></tr>';
13059		}
13060		html += '</tbody></table>';
13061	}
13062
13063	// The store covers the names it has been run over, not every tracker on every site. A
13064	// merchant told nothing publishes a half-translated document believing it finished.
13065	const lg = policy.language;
13066	if (lg && lg.note && lg.fellBackToSiteText > 0)
13067		html += '<p style="font-size:.85em;color:#555;">' + fcPolicyEsc(lg.note) + '</p>';
13068
13069	if (policy.disclaimer)
13070		html += '<p style="font-size:.85em;color:#555;margin-top:16px;">' + fcPolicyEsc(policy.disclaimer) + '</p>';
13071	return html;
13072}
13073
13074/**
13075 * Fold the section into the generated document: replace a {trackerList} placeholder if the
13076 * template has one, otherwise append. Appending is what keeps this working against the
13077 * templates as they stand today.
13078 */
13079function fcApplyCookiePolicySection(documentContent, policy) {
13080	const section = fcCookiePolicySectionHtml(policy);
13081	if (!section) return documentContent;
13082	if (documentContent && documentContent.indexOf('{trackerList}') >= 0)
13083		return documentContent.split('{trackerList}').join(section);
13084	return (documentContent || '') + section;
13085}
13086
13087// ───────────────────────────────────────────────── §12 the site's own banner wording
13088//
13089// Two fields over the shared translated library. Empty means default, so clearing a box
13090// restores our wording rather than publishing a blank banner — the server stores an
13091// override that normalises to nothing as absent, and the page leaves the library alone.
13092
13093function fcLoadBannerText(bt) {
13094	const title = document.getElementById('banner_text_title');
13095	const desc = document.getElementById('banner_text_description');
13096	if (!title || !desc) return;
13097	const t = bt && typeof bt === 'object' ? bt : {};
13098	title.value = t['homeTitle'] || '';
13099	desc.value = t['homeDescription'] || '';
13100}
13101
13102function fcCollectBannerText() {
13103	const title = document.getElementById('banner_text_title');
13104	const desc = document.getElementById('banner_text_description');
13105	const out = {};
13106	// Only non-empty fields travel. The server whitelists and re-checks anyway; sending
13107	// blanks would just be asking it to store an instruction to show nothing.
13108	if (title && title.value.trim()) out['homeTitle'] = title.value.trim();
13109	if (desc && desc.value.trim()) out['homeDescription'] = desc.value.trim();
13110	return out;
13111}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.