@mahe_pkm/buzl-capi 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Buzl_GoogleAppsScript_Template.gs +948 -0
- package/CHANGELOG.md +83 -0
- package/LICENSE +21 -0
- package/README.md +253 -0
- package/assets/js/buzl-tracking.js +390 -0
- package/bin/cli.js +155 -0
- package/package.json +54 -0
- package/src/cli/terminal.js +388 -0
- package/src/core/injector.js +321 -0
- package/src/core/rollback.js +281 -0
- package/src/core/scanner.js +506 -0
- package/src/core/tester.js +357 -0
- package/src/gui/public/app.js +942 -0
- package/src/gui/public/index.html +634 -0
- package/src/gui/public/style.css +1130 -0
- package/src/gui/server.js +232 -0
- package/src/index.js +47 -0
- package/src/templates/GoogleAppsScript.gs +948 -0
- package/src/templates/buzl-tracking.js +475 -0
- package/src/templates/gtm-meta-snippets.js +149 -0
|
@@ -0,0 +1,942 @@
|
|
|
1
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
2
|
+
const form = document.getElementById('trackingForm');
|
|
3
|
+
const btnSubmit = document.getElementById('btnSubmit');
|
|
4
|
+
const btnScan = document.getElementById('btnScan');
|
|
5
|
+
const fileList = document.getElementById('fileList');
|
|
6
|
+
const fileBadge = document.getElementById('fileBadge');
|
|
7
|
+
const testConsole = document.getElementById('testConsole');
|
|
8
|
+
const testSummaryBadge = document.getElementById('testSummaryBadge');
|
|
9
|
+
|
|
10
|
+
// Live State Bar elements
|
|
11
|
+
const liveStateRootDir = document.getElementById('liveStateRootDir');
|
|
12
|
+
const liveStateChips = document.getElementById('liveStateChips');
|
|
13
|
+
const backupsCountBadge = document.getElementById('backupsCountBadge');
|
|
14
|
+
|
|
15
|
+
// Quick Test Lead elements
|
|
16
|
+
const btnTestSubmit = document.getElementById('btnTestSubmit');
|
|
17
|
+
const liveTestBadge = document.getElementById('liveTestBadge');
|
|
18
|
+
const testDispatchResults = document.getElementById('testDispatchResults');
|
|
19
|
+
const testLeadPhone = document.getElementById('testLeadPhone');
|
|
20
|
+
const testLeadName = document.getElementById('testLeadName');
|
|
21
|
+
const testLeadService = document.getElementById('testLeadService');
|
|
22
|
+
|
|
23
|
+
[testLeadPhone, testLeadName, testLeadService].forEach(el => {
|
|
24
|
+
if (el) el.addEventListener('input', () => { el.dataset.userEdited = 'true'; });
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// Individual Form elements
|
|
28
|
+
const formsCountBadge = document.getElementById('formsCountBadge');
|
|
29
|
+
const individualFormsList = document.getElementById('individualFormsList');
|
|
30
|
+
|
|
31
|
+
// Backups Modal elements
|
|
32
|
+
const btnOpenBackups = document.getElementById('btnOpenBackups');
|
|
33
|
+
const backupsModal = document.getElementById('backupsModal');
|
|
34
|
+
const closeBackupsModal = document.getElementById('closeBackupsModal');
|
|
35
|
+
const btnCreateNamedBackup = document.getElementById('btnCreateNamedBackup');
|
|
36
|
+
const customBackupName = document.getElementById('customBackupName');
|
|
37
|
+
const backupsTableBody = document.getElementById('backupsTableBody');
|
|
38
|
+
|
|
39
|
+
// Uninstall Modal elements
|
|
40
|
+
const btnUninstallTracking = document.getElementById('btnUninstallTracking');
|
|
41
|
+
const uninstallModal = document.getElementById('uninstallModal');
|
|
42
|
+
const closeUninstallModal = document.getElementById('closeUninstallModal');
|
|
43
|
+
const btnCancelUninstall = document.getElementById('btnCancelUninstall');
|
|
44
|
+
const btnConfirmUninstall = document.getElementById('btnConfirmUninstall');
|
|
45
|
+
|
|
46
|
+
// Script Modal elements
|
|
47
|
+
const scriptModal = document.getElementById('scriptModal');
|
|
48
|
+
const openScriptModal = document.getElementById('openScriptModal');
|
|
49
|
+
const closeScriptModal = document.getElementById('closeScriptModal');
|
|
50
|
+
const copyScriptCode = document.getElementById('copyScriptCode');
|
|
51
|
+
const scriptCodeDisplay = document.getElementById('scriptCodeDisplay');
|
|
52
|
+
|
|
53
|
+
let scriptTemplateText = '';
|
|
54
|
+
|
|
55
|
+
const siteLocationInput = document.getElementById('siteLocation');
|
|
56
|
+
const columnHierarchyPreview = document.getElementById('columnHierarchyPreview');
|
|
57
|
+
const dynamicFieldsChecklist = document.getElementById('dynamicFieldsChecklist');
|
|
58
|
+
|
|
59
|
+
let activeDynamicFields = [];
|
|
60
|
+
let currentScanData = null;
|
|
61
|
+
|
|
62
|
+
function renderColumnHierarchy() {
|
|
63
|
+
if (!columnHierarchyPreview) return;
|
|
64
|
+
columnHierarchyPreview.innerHTML = '';
|
|
65
|
+
|
|
66
|
+
const cols = [
|
|
67
|
+
{ name: 'Name', type: 'contact' },
|
|
68
|
+
{ name: 'Location', type: 'contact' },
|
|
69
|
+
{ name: 'Phone', type: 'contact' }
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
activeDynamicFields.forEach(f => {
|
|
73
|
+
cols.push({ name: f, type: 'dynamic' });
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const crmCols = ['Lead Stage', 'Event Time', 'Is Qualified', 'Qualified Date', 'Is Spam', 'Handled By', 'Comments'];
|
|
77
|
+
crmCols.forEach(c => cols.push({ name: c, type: 'crm' }));
|
|
78
|
+
|
|
79
|
+
const attrCols = ['Action Source', 'Source', 'UTM Source', 'UTM Campaign', 'Lead ID'];
|
|
80
|
+
attrCols.forEach(a => cols.push({ name: a, type: 'attrib' }));
|
|
81
|
+
|
|
82
|
+
cols.forEach((col, idx) => {
|
|
83
|
+
const pill = document.createElement('span');
|
|
84
|
+
pill.className = `col-pill col-pill-${col.type}`;
|
|
85
|
+
pill.textContent = `${idx + 1}. ${col.name}`;
|
|
86
|
+
columnHierarchyPreview.appendChild(pill);
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// 1. Fetch project scan & live state
|
|
91
|
+
async function loadScan() {
|
|
92
|
+
fileBadge.textContent = 'Scanning...';
|
|
93
|
+
fileBadge.className = 'badge badge-neutral';
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
const res = await fetch('/api/scan');
|
|
97
|
+
const data = await res.json();
|
|
98
|
+
currentScanData = data;
|
|
99
|
+
|
|
100
|
+
const dirCount = (data.directories && data.directories.length > 1) ? ` · ${data.directories.length} dirs` : '';
|
|
101
|
+
fileBadge.textContent = `${data.totalHtmlFiles} HTML File(s)${dirCount}`;
|
|
102
|
+
fileBadge.className = 'badge badge-info';
|
|
103
|
+
fileList.innerHTML = '';
|
|
104
|
+
|
|
105
|
+
// Update Live State Bar
|
|
106
|
+
updateLiveStateBar(data.liveState, data.rootDir);
|
|
107
|
+
|
|
108
|
+
// Auto-detect site location
|
|
109
|
+
if (data.detectedLocation && siteLocationInput && !siteLocationInput.dataset.userEdited) {
|
|
110
|
+
siteLocationInput.value = data.detectedLocation;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Auto-detect phone and service for quick test dispatch
|
|
114
|
+
if (testLeadPhone && !testLeadPhone.dataset.userEdited) {
|
|
115
|
+
testLeadPhone.value = data.detectedWhatsapp || (data.liveState && data.liveState.whatsapp && data.liveState.whatsapp.detectedNumber) || '';
|
|
116
|
+
}
|
|
117
|
+
if (testLeadService && !testLeadService.dataset.userEdited) {
|
|
118
|
+
testLeadService.value = data.detectedService || 'General Inquiry';
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Populate dynamic fields checklist
|
|
122
|
+
if (dynamicFieldsChecklist && data.uniqueFields) {
|
|
123
|
+
dynamicFieldsChecklist.innerHTML = '';
|
|
124
|
+
const customFields = data.uniqueFields.filter(f => !f.isCore);
|
|
125
|
+
|
|
126
|
+
activeDynamicFields = customFields.map(f => f.name.charAt(0).toUpperCase() + f.name.slice(1));
|
|
127
|
+
|
|
128
|
+
if (customFields.length === 0) {
|
|
129
|
+
dynamicFieldsChecklist.innerHTML = '<span class="helper-text">No custom fields detected (core: Name, Phone).</span>';
|
|
130
|
+
} else {
|
|
131
|
+
customFields.forEach(f => {
|
|
132
|
+
const formatted = f.name.charAt(0).toUpperCase() + f.name.slice(1);
|
|
133
|
+
const label = document.createElement('label');
|
|
134
|
+
label.className = 'field-checkbox-item';
|
|
135
|
+
label.innerHTML = `
|
|
136
|
+
<input type="checkbox" checked data-field="${formatted}">
|
|
137
|
+
<span><strong>${formatted}</strong> (${f.tag}) — <small class="helper-text">Placed next to Phone (Col D)</small></span>
|
|
138
|
+
`;
|
|
139
|
+
|
|
140
|
+
label.querySelector('input').addEventListener('change', (e) => {
|
|
141
|
+
if (e.target.checked) {
|
|
142
|
+
if (!activeDynamicFields.includes(formatted)) activeDynamicFields.push(formatted);
|
|
143
|
+
} else {
|
|
144
|
+
activeDynamicFields = activeDynamicFields.filter(x => x !== formatted);
|
|
145
|
+
}
|
|
146
|
+
renderColumnHierarchy();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
dynamicFieldsChecklist.appendChild(label);
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
renderColumnHierarchy();
|
|
155
|
+
|
|
156
|
+
// Render Discovered Project Files
|
|
157
|
+
if (data.files.length === 0) {
|
|
158
|
+
fileList.innerHTML = '<li class="file-item"><span class="name">No HTML files discovered in root</span></li>';
|
|
159
|
+
} else {
|
|
160
|
+
data.files.forEach(f => {
|
|
161
|
+
const li = document.createElement('li');
|
|
162
|
+
li.className = 'file-item';
|
|
163
|
+
|
|
164
|
+
const tags = [];
|
|
165
|
+
if (f.hasHead) tags.push('<span class="badge badge-neutral"><head></span>');
|
|
166
|
+
if (f.hasBody) tags.push('<span class="badge badge-neutral"><body></span>');
|
|
167
|
+
if (f.forms.length > 0) tags.push(`<span class="badge badge-success">${f.forms.length} form(s)</span>`);
|
|
168
|
+
if (f.existingGtmId) tags.push(`<span class="badge badge-info">${f.existingGtmId}</span>`);
|
|
169
|
+
|
|
170
|
+
li.innerHTML = `
|
|
171
|
+
<span class="name">${f.relativePath}</span>
|
|
172
|
+
<div class="file-tags">${tags.join('')}</div>
|
|
173
|
+
`;
|
|
174
|
+
fileList.appendChild(li);
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Render Individual Forms for independent testing
|
|
179
|
+
renderIndividualForms(data.forms || [], data.formArchetypes || []);
|
|
180
|
+
|
|
181
|
+
// Pre-fill inputs strictly from live project state
|
|
182
|
+
applyLiveConfigToForm(data);
|
|
183
|
+
|
|
184
|
+
// Refresh Backups Count
|
|
185
|
+
if (data.backups && backupsCountBadge) {
|
|
186
|
+
backupsCountBadge.textContent = data.backups.length;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
} catch (err) {
|
|
190
|
+
fileBadge.textContent = 'Scan Error';
|
|
191
|
+
fileBadge.className = 'badge badge-danger';
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function updateLiveStateBar(liveState, rootDir) {
|
|
196
|
+
if (!liveState) return;
|
|
197
|
+
if (liveStateRootDir) liveStateRootDir.textContent = rootDir || 'Project Root';
|
|
198
|
+
|
|
199
|
+
const locBadge = document.getElementById('siteLocationBadge');
|
|
200
|
+
if (locBadge) {
|
|
201
|
+
locBadge.innerHTML = currentScanData && currentScanData.detectedLocation
|
|
202
|
+
? `<svg class="icon-xs"><use href="#icon-pin"/></svg> <span>${currentScanData.detectedLocation}</span>`
|
|
203
|
+
: '<svg class="icon-xs"><use href="#icon-pin"/></svg> <span>Location: Not detected</span>';
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Toggle per-card remove buttons
|
|
207
|
+
const btnGtm = document.getElementById('btnRemoveGtm');
|
|
208
|
+
if (btnGtm) btnGtm.classList.toggle('hidden', !(liveState.gtm && liveState.gtm.active));
|
|
209
|
+
|
|
210
|
+
const btnMeta = document.getElementById('btnRemoveMeta');
|
|
211
|
+
if (btnMeta) btnMeta.classList.toggle('hidden', !(liveState.meta && liveState.meta.active));
|
|
212
|
+
|
|
213
|
+
const btnCapi = document.getElementById('btnRemoveCapi');
|
|
214
|
+
if (btnCapi) btnCapi.classList.toggle('hidden', !(liveState.buzlCapi && liveState.buzlCapi.active));
|
|
215
|
+
|
|
216
|
+
const btnSheets = document.getElementById('btnRemoveSheets');
|
|
217
|
+
if (btnSheets) btnSheets.classList.toggle('hidden', !(liveState.googleSheets && liveState.googleSheets.active));
|
|
218
|
+
|
|
219
|
+
const btnZoho = document.getElementById('btnRemoveZoho');
|
|
220
|
+
if (btnZoho) btnZoho.classList.toggle('hidden', !(liveState.zoho && liveState.zoho.active));
|
|
221
|
+
|
|
222
|
+
const btnWa = document.getElementById('btnRemoveWhatsapp');
|
|
223
|
+
if (btnWa) btnWa.classList.toggle('hidden', !(liveState.whatsapp && liveState.whatsapp.active));
|
|
224
|
+
|
|
225
|
+
if (liveStateChips) {
|
|
226
|
+
liveStateChips.innerHTML = '';
|
|
227
|
+
|
|
228
|
+
const items = [
|
|
229
|
+
{ serviceKey: 'gtm', label: 'GTM', active: liveState.gtm.active, text: liveState.gtm.id ? `GTM: ${liveState.gtm.id}` : 'GTM: Inactive' },
|
|
230
|
+
{ serviceKey: 'meta', label: 'Meta', active: liveState.meta.active, text: liveState.meta.id ? `Meta: ${liveState.meta.id}` : 'Meta: Inactive' },
|
|
231
|
+
{ serviceKey: 'capi', label: 'Buzl CAPI', active: liveState.buzlCapi.active, text: liveState.buzlCapi.active ? 'Buzl CAPI: Connected' : 'Buzl CAPI: Inactive' },
|
|
232
|
+
{ serviceKey: 'sheets', label: 'Google Sheets', active: liveState.googleSheets.active, text: liveState.googleSheets.active ? 'Sheets CRM: Connected' : 'Sheets CRM: Inactive' },
|
|
233
|
+
{ serviceKey: 'zoho', label: 'Zoho CRM', active: liveState.zoho.active, text: liveState.zoho.active ? 'Zoho CRM: Connected' : 'Zoho CRM: Inactive' },
|
|
234
|
+
{ serviceKey: 'whatsapp', label: 'WhatsApp', active: liveState.whatsapp.active, text: liveState.whatsapp.active ? `WA: ${liveState.whatsapp.number}` : 'WA: Inactive' }
|
|
235
|
+
];
|
|
236
|
+
|
|
237
|
+
items.forEach(item => {
|
|
238
|
+
const chip = document.createElement('span');
|
|
239
|
+
chip.className = `chip ${item.active ? 'chip-active' : 'chip-inactive'}`;
|
|
240
|
+
chip.innerHTML = `<span class="status-dot ${item.active ? 'dot-active' : 'dot-inactive'}"></span> <span>${item.text}</span> ${item.active ? `<button type="button" class="chip-remove-btn" data-service="${item.serviceKey}" title="Remove ${item.label}"><svg class="icon-xs"><use href="#icon-x"/></svg></button>` : ''}`;
|
|
241
|
+
|
|
242
|
+
const removeBtn = chip.querySelector('.chip-remove-btn');
|
|
243
|
+
if (removeBtn) {
|
|
244
|
+
removeBtn.addEventListener('click', (e) => {
|
|
245
|
+
e.stopPropagation();
|
|
246
|
+
removeSingleService(item.serviceKey);
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
liveStateChips.appendChild(chip);
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async function removeSingleService(serviceKey) {
|
|
256
|
+
const serviceNames = {
|
|
257
|
+
gtm: 'Google Tag Manager (GTM)',
|
|
258
|
+
meta: 'Meta Pixel & Client CAPI',
|
|
259
|
+
capi: 'Buzl CAPI Server-Side Endpoint',
|
|
260
|
+
sheets: 'Google Sheets Direct CRM',
|
|
261
|
+
zoho: 'Zoho CRM Web-to-Lead',
|
|
262
|
+
whatsapp: 'WhatsApp Handoff'
|
|
263
|
+
};
|
|
264
|
+
const name = serviceNames[serviceKey] || serviceKey;
|
|
265
|
+
if (!confirm(`Are you sure you want to remove ${name} from this site?\n\nAll other active tracking and configurations will be safely preserved. An automatic safety snapshot will be created before removal.`)) {
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
try {
|
|
270
|
+
const res = await fetch('/api/remove-service', {
|
|
271
|
+
method: 'POST',
|
|
272
|
+
headers: { 'Content-Type': 'application/json' },
|
|
273
|
+
body: JSON.stringify({ service: serviceKey })
|
|
274
|
+
});
|
|
275
|
+
const data = await res.json();
|
|
276
|
+
if (data.success) {
|
|
277
|
+
alert(data.message);
|
|
278
|
+
loadScan();
|
|
279
|
+
} else {
|
|
280
|
+
alert(`Failed to remove ${name}: ${data.message}`);
|
|
281
|
+
}
|
|
282
|
+
} catch (err) {
|
|
283
|
+
alert(`Network error removing ${name}: ${err.message}`);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function applyLiveConfigToForm(data) {
|
|
288
|
+
const live = data.liveState;
|
|
289
|
+
const cfg = data.existingConfig || {};
|
|
290
|
+
|
|
291
|
+
// GTM
|
|
292
|
+
const gtmSwitch = document.getElementById('enableGTM');
|
|
293
|
+
const gtmInput = document.getElementById('gtmId');
|
|
294
|
+
if (gtmInput) {
|
|
295
|
+
gtmInput.value = (live && live.gtm && live.gtm.id) || cfg.gtmId || '';
|
|
296
|
+
}
|
|
297
|
+
if (gtmSwitch) {
|
|
298
|
+
gtmSwitch.checked = !!(live && live.gtm && live.gtm.active);
|
|
299
|
+
gtmSwitch.dispatchEvent(new Event('change'));
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Meta Pixel
|
|
303
|
+
const metaSwitch = document.getElementById('enableMeta');
|
|
304
|
+
const metaInput = document.getElementById('metaPixelId');
|
|
305
|
+
if (metaInput) {
|
|
306
|
+
metaInput.value = (live && live.meta && live.meta.id) || cfg.metaPixelId || '';
|
|
307
|
+
}
|
|
308
|
+
if (metaSwitch) {
|
|
309
|
+
metaSwitch.checked = !!(live && live.meta && live.meta.active);
|
|
310
|
+
metaSwitch.dispatchEvent(new Event('change'));
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Buzl CAPI
|
|
314
|
+
const capiSwitch = document.getElementById('enableBuzlCapi');
|
|
315
|
+
const capiEndpoint = document.getElementById('buzlCapiEndpoint');
|
|
316
|
+
const capiUser = document.getElementById('buzlCapiUser');
|
|
317
|
+
const capiPass = document.getElementById('buzlCapiPass');
|
|
318
|
+
|
|
319
|
+
const capiData = (live && live.buzlCapi) || cfg.buzlCapi || {};
|
|
320
|
+
if (capiEndpoint && capiData.endpoint) capiEndpoint.value = capiData.endpoint;
|
|
321
|
+
if (capiUser) capiUser.value = capiData.authUser || '2BuzlmqpHJeVBow0dzR9gP3$uQLxIA';
|
|
322
|
+
if (capiPass) capiPass.value = (cfg.buzlCapi && cfg.buzlCapi.authPass) || 'dgAY%nH1MNPgOvGaYRg6ynomM3mbJgGjr%Z3FcPCJNzvm#KjV!I%Y9tf$bDacBgPIABuzl';
|
|
323
|
+
|
|
324
|
+
if (capiSwitch) {
|
|
325
|
+
capiSwitch.checked = !!(live && live.buzlCapi && live.buzlCapi.active);
|
|
326
|
+
capiSwitch.dispatchEvent(new Event('change'));
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Google Sheets
|
|
330
|
+
const sheetsSwitch = document.getElementById('enableSheets');
|
|
331
|
+
const sheetsInput = document.getElementById('googleSheetUrl');
|
|
332
|
+
if (sheetsInput && (live && live.googleSheets && live.googleSheets.url)) {
|
|
333
|
+
sheetsInput.value = live.googleSheets.url;
|
|
334
|
+
} else if (sheetsInput && cfg.googleSheetUrl) {
|
|
335
|
+
sheetsInput.value = cfg.googleSheetUrl;
|
|
336
|
+
}
|
|
337
|
+
if (sheetsSwitch) {
|
|
338
|
+
sheetsSwitch.checked = !!(live && live.googleSheets && live.googleSheets.active);
|
|
339
|
+
sheetsSwitch.dispatchEvent(new Event('change'));
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Zoho CRM
|
|
343
|
+
const zohoSwitch = document.getElementById('enableZoho');
|
|
344
|
+
const zohoXn = document.getElementById('zohoXnqsjsdp');
|
|
345
|
+
const zohoXm = document.getElementById('zohoXmiwtld');
|
|
346
|
+
const zohoEp = document.getElementById('zohoEndpoint');
|
|
347
|
+
if (cfg.zoho) {
|
|
348
|
+
if (zohoXn && cfg.zoho.xnQsjsdp) zohoXn.value = cfg.zoho.xnQsjsdp;
|
|
349
|
+
if (zohoXm && cfg.zoho.xmIwtLD) zohoXm.value = cfg.zoho.xmIwtLD;
|
|
350
|
+
if (zohoEp && cfg.zoho.endpoint) zohoEp.value = cfg.zoho.endpoint;
|
|
351
|
+
}
|
|
352
|
+
if (zohoSwitch) {
|
|
353
|
+
zohoSwitch.checked = !!(live && live.zoho && live.zoho.active);
|
|
354
|
+
zohoSwitch.dispatchEvent(new Event('change'));
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// WhatsApp
|
|
358
|
+
const waSwitch = document.getElementById('enableWhatsapp');
|
|
359
|
+
const waNumber = document.getElementById('whatsappNumber');
|
|
360
|
+
const waBadge = document.getElementById('waDetectedBadge');
|
|
361
|
+
const waHelper = document.getElementById('waHelperText');
|
|
362
|
+
|
|
363
|
+
const detectedWa = data.detectedWhatsapp || (live && live.whatsapp && live.whatsapp.detectedNumber);
|
|
364
|
+
if (detectedWa && waBadge) {
|
|
365
|
+
waBadge.innerHTML = `<svg class="icon-xs"><use href="#icon-check"/></svg> <span>Auto-detected: ${detectedWa}</span>`;
|
|
366
|
+
waBadge.classList.remove('hidden');
|
|
367
|
+
waBadge.onclick = () => {
|
|
368
|
+
if (waNumber) waNumber.value = detectedWa;
|
|
369
|
+
if (waSwitch && !waSwitch.checked) {
|
|
370
|
+
waSwitch.checked = true;
|
|
371
|
+
waSwitch.dispatchEvent(new Event('change'));
|
|
372
|
+
}
|
|
373
|
+
};
|
|
374
|
+
if (waHelper) {
|
|
375
|
+
waHelper.textContent = `Auto-fetches ${detectedWa} from form buttons, or specify fallback number.`;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (waNumber) {
|
|
380
|
+
if (live && live.whatsapp && live.whatsapp.number) {
|
|
381
|
+
waNumber.value = live.whatsapp.number;
|
|
382
|
+
} else if (cfg.whatsapp && cfg.whatsapp.number) {
|
|
383
|
+
waNumber.value = cfg.whatsapp.number;
|
|
384
|
+
} else if (detectedWa && !waNumber.value) {
|
|
385
|
+
waNumber.value = detectedWa;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (waSwitch) {
|
|
390
|
+
const isWaActive = !!(live && live.whatsapp && live.whatsapp.active);
|
|
391
|
+
waSwitch.checked = isWaActive || (!!detectedWa && (live && live.whatsapp && live.whatsapp.active !== false));
|
|
392
|
+
waSwitch.dispatchEvent(new Event('change'));
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// 2. Render Individual Discovered Forms with Per-Form Testing
|
|
397
|
+
function renderIndividualForms(forms, archetypes) {
|
|
398
|
+
if (!individualFormsList) return;
|
|
399
|
+
individualFormsList.innerHTML = '';
|
|
400
|
+
|
|
401
|
+
const listToRender = (archetypes && archetypes.length > 0) ? archetypes : forms;
|
|
402
|
+
|
|
403
|
+
if (formsCountBadge) {
|
|
404
|
+
if (archetypes && archetypes.length > 0 && forms.length !== archetypes.length) {
|
|
405
|
+
formsCountBadge.textContent = `${forms.length} Instances (${archetypes.length} Unique)`;
|
|
406
|
+
} else {
|
|
407
|
+
formsCountBadge.textContent = `${forms.length} Form(s)`;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
if (!listToRender || listToRender.length === 0) {
|
|
412
|
+
individualFormsList.innerHTML = '<p class="empty-state">No forms found on HTML pages.</p>';
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
listToRender.forEach((f, idx) => {
|
|
417
|
+
const card = document.createElement('div');
|
|
418
|
+
card.className = 'form-item-card';
|
|
419
|
+
|
|
420
|
+
const tagPills = f.inputs.map(i => `<span class="col-pill col-pill-dynamic" style="font-size: 10px;">${i.name}</span>`).join('');
|
|
421
|
+
|
|
422
|
+
// Build sample test input controls
|
|
423
|
+
const inputElements = f.inputs.map(i => {
|
|
424
|
+
let sampleVal = 'Test Value';
|
|
425
|
+
const lower = i.name.toLowerCase();
|
|
426
|
+
if (lower.includes('phone') || lower.includes('mobile')) {
|
|
427
|
+
sampleVal = f.detectedWhatsapp || (currentScanData && currentScanData.detectedWhatsapp) || '';
|
|
428
|
+
} else if (lower.includes('name')) {
|
|
429
|
+
sampleVal = `Test Lead ${idx + 1}`;
|
|
430
|
+
} else if (lower.includes('mail')) {
|
|
431
|
+
sampleVal = 'test-lead@example.com';
|
|
432
|
+
} else if (lower.includes('service') || lower.includes('inquiry') || lower.includes('treatment') || lower.includes('subject')) {
|
|
433
|
+
sampleVal = f.formTitle || f.id || (currentScanData && currentScanData.detectedService) || 'General Inquiry';
|
|
434
|
+
} else if (lower.includes('city') || lower.includes('loc')) {
|
|
435
|
+
sampleVal = (currentScanData && currentScanData.detectedLocation) || '';
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
return `
|
|
439
|
+
<div class="form-field-input-group">
|
|
440
|
+
<label>${i.name}</label>
|
|
441
|
+
<input type="text" data-field-name="${i.name}" value="${sampleVal}">
|
|
442
|
+
</div>
|
|
443
|
+
`;
|
|
444
|
+
}).join('');
|
|
445
|
+
|
|
446
|
+
const isShared = f.isShared || (f.pages && f.pages.length > 1);
|
|
447
|
+
const pageInfo = isShared
|
|
448
|
+
? `<span class="badge badge-info" style="font-size: 10.5px;"><svg class="icon-xs"><use href="#icon-refresh"/></svg> <span>Appears on ${f.pages.length} pages</span></span>`
|
|
449
|
+
: `<small style="color: var(--text-dim); font-size: 11px;">in <code>${f.file || (f.pages && f.pages[0]) || 'page'}</code></small>`;
|
|
450
|
+
|
|
451
|
+
const pagesList = (isShared && f.pages)
|
|
452
|
+
? `<div style="display: flex; gap: 4px; flex-wrap: wrap; margin-bottom: 8px;">
|
|
453
|
+
${f.pages.map(p => `<span class="badge badge-neutral" style="font-family: monospace; font-size: 10px;">${p}</span>`).join('')}
|
|
454
|
+
</div>`
|
|
455
|
+
: '';
|
|
456
|
+
|
|
457
|
+
const waBadge = f.detectedWhatsapp
|
|
458
|
+
? `<span class="badge badge-success" style="font-size: 10px; font-family: monospace;" title="Target WhatsApp Number for this form"><svg class="icon-xs"><use href="#icon-brand-whatsapp"/></svg> <span>WA: ${f.detectedWhatsapp}</span></span>`
|
|
459
|
+
: '';
|
|
460
|
+
|
|
461
|
+
card.innerHTML = `
|
|
462
|
+
<div class="form-item-header">
|
|
463
|
+
<div style="display: flex; align-items: center; gap: 8px; flex-wrap: wrap;">
|
|
464
|
+
<span class="form-selector-tag">${f.selector}</span>
|
|
465
|
+
${pageInfo}
|
|
466
|
+
${waBadge}
|
|
467
|
+
</div>
|
|
468
|
+
<span class="badge badge-neutral">${f.inputCount || f.inputs.length} input(s)</span>
|
|
469
|
+
</div>
|
|
470
|
+
${pagesList}
|
|
471
|
+
<div style="display: flex; gap: 4px; flex-wrap: wrap; margin-bottom: 8px;">${tagPills}</div>
|
|
472
|
+
<div class="form-fields-grid">${inputElements}</div>
|
|
473
|
+
<div class="form-actions-row">
|
|
474
|
+
<button type="button" class="btn btn-primary btn-sm btn-test-this-form" data-form-id="${f.formId}">
|
|
475
|
+
<svg class="icon-xs"><use href="#icon-send"/></svg>
|
|
476
|
+
<span>Test This Form (${f.selector})</span>
|
|
477
|
+
</button>
|
|
478
|
+
<span class="form-test-result hidden"></span>
|
|
479
|
+
</div>
|
|
480
|
+
`;
|
|
481
|
+
|
|
482
|
+
// Wire test click
|
|
483
|
+
const testBtn = card.querySelector('.btn-test-this-form');
|
|
484
|
+
const resultSpan = card.querySelector('.form-test-result');
|
|
485
|
+
|
|
486
|
+
testBtn.addEventListener('click', async () => {
|
|
487
|
+
testBtn.disabled = true;
|
|
488
|
+
testBtn.innerHTML = '<svg class="icon-xs icon-spin"><use href="#icon-refresh"/></svg> <span>Testing...</span>';
|
|
489
|
+
resultSpan.classList.remove('hidden');
|
|
490
|
+
resultSpan.className = 'form-test-result badge badge-info';
|
|
491
|
+
resultSpan.innerHTML = '<svg class="icon-xs icon-spin"><use href="#icon-refresh"/></svg> <span>Dispatching to Sheet & CAPI...</span>';
|
|
492
|
+
|
|
493
|
+
// Collect fields
|
|
494
|
+
const fieldValues = {};
|
|
495
|
+
card.querySelectorAll('input[data-field-name]').forEach(inp => {
|
|
496
|
+
fieldValues[inp.dataset.fieldName] = inp.value.trim();
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
const targetPage = f.file || (f.pages && f.pages[0]) || 'index.html';
|
|
500
|
+
|
|
501
|
+
try {
|
|
502
|
+
const res = await fetch('/api/test-form', {
|
|
503
|
+
method: 'POST',
|
|
504
|
+
headers: { 'Content-Type': 'application/json' },
|
|
505
|
+
body: JSON.stringify({
|
|
506
|
+
formId: f.formId,
|
|
507
|
+
formFields: fieldValues,
|
|
508
|
+
pagePath: targetPage,
|
|
509
|
+
config: getCurrentConfig()
|
|
510
|
+
})
|
|
511
|
+
});
|
|
512
|
+
const data = await res.json();
|
|
513
|
+
testBtn.disabled = false;
|
|
514
|
+
testBtn.innerHTML = `<svg class="icon-xs"><use href="#icon-send"/></svg> <span>Test This Form (${f.selector})</span>`;
|
|
515
|
+
|
|
516
|
+
if (data.success && data.results) {
|
|
517
|
+
const ch = data.results.channels;
|
|
518
|
+
const sheetOk = ch.googleSheets && ch.googleSheets.ok;
|
|
519
|
+
const capiOk = ch.buzlCapi && ch.buzlCapi.ok;
|
|
520
|
+
|
|
521
|
+
if (sheetOk || capiOk) {
|
|
522
|
+
resultSpan.className = 'form-test-result badge badge-success';
|
|
523
|
+
const rowLabel = (ch.googleSheets.message.match(/Row \d+/) || [])[0] || 'OK';
|
|
524
|
+
resultSpan.innerHTML = `<svg class="icon-xs" style="vertical-align:text-bottom; margin-right:3px;"><use href="#icon-check"/></svg> <span>Delivered! (Sheets: ${sheetOk ? rowLabel : 'Off'} | CAPI: ${capiOk ? 'Accepted' : 'Off'})</span>`;
|
|
525
|
+
} else {
|
|
526
|
+
resultSpan.className = 'form-test-result badge badge-danger';
|
|
527
|
+
resultSpan.innerHTML = `<svg class="icon-xs" style="vertical-align:text-bottom; margin-right:3px;"><use href="#icon-x"/></svg> <span>Failed: ${ch.googleSheets.message || ch.buzlCapi.message || 'Error'}</span>`;
|
|
528
|
+
}
|
|
529
|
+
} else {
|
|
530
|
+
resultSpan.className = 'form-test-result badge badge-danger';
|
|
531
|
+
resultSpan.innerHTML = `<svg class="icon-xs" style="vertical-align:text-bottom; margin-right:3px;"><use href="#icon-x"/></svg> <span>Error: ${data.message || 'Failed'}</span>`;
|
|
532
|
+
}
|
|
533
|
+
} catch (e) {
|
|
534
|
+
testBtn.disabled = false;
|
|
535
|
+
testBtn.innerHTML = `<svg class="icon-xs"><use href="#icon-send"/></svg> <span>Test This Form (${f.selector})</span>`;
|
|
536
|
+
resultSpan.className = 'form-test-result badge badge-danger';
|
|
537
|
+
resultSpan.innerHTML = `<svg class="icon-xs" style="vertical-align:text-bottom; margin-right:3px;"><use href="#icon-x"/></svg> <span>Network Error: ${e.message}</span>`;
|
|
538
|
+
}
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
individualFormsList.appendChild(card);
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// 3. Named Backups Management
|
|
546
|
+
async function loadBackupsList() {
|
|
547
|
+
if (!backupsTableBody) return;
|
|
548
|
+
backupsTableBody.innerHTML = '<tr><td colspan="5" style="text-align: center;">Loading backups...</td></tr>';
|
|
549
|
+
|
|
550
|
+
try {
|
|
551
|
+
const res = await fetch('/api/backups');
|
|
552
|
+
const data = await res.json();
|
|
553
|
+
const backups = data.backups || [];
|
|
554
|
+
|
|
555
|
+
if (backupsCountBadge) backupsCountBadge.textContent = backups.length;
|
|
556
|
+
|
|
557
|
+
if (backups.length === 0) {
|
|
558
|
+
backupsTableBody.innerHTML = '<tr><td colspan="5" style="text-align: center; color: var(--text-muted);">No backups found on disk.</td></tr>';
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
backupsTableBody.innerHTML = '';
|
|
563
|
+
backups.forEach(b => {
|
|
564
|
+
const tr = document.createElement('tr');
|
|
565
|
+
const formattedDate = b.timestamp ? new Date(b.timestamp).toLocaleString() : 'N/A';
|
|
566
|
+
|
|
567
|
+
tr.innerHTML = `
|
|
568
|
+
<td><strong>${b.name || 'Snapshot'}</strong></td>
|
|
569
|
+
<td><code style="font-size: 11px; color: var(--text-muted);">${b.dirName}</code></td>
|
|
570
|
+
<td>${formattedDate}</td>
|
|
571
|
+
<td><span class="badge badge-neutral">${b.filesCount} file(s)</span></td>
|
|
572
|
+
<td style="display: flex; gap: 6px;">
|
|
573
|
+
<button type="button" class="btn btn-secondary btn-sm btn-restore-backup" data-dir="${b.dirName}" data-name="${b.name}">
|
|
574
|
+
<svg class="icon-xs"><use href="#icon-refresh"/></svg>
|
|
575
|
+
<span>Restore</span>
|
|
576
|
+
</button>
|
|
577
|
+
<button type="button" class="btn btn-danger-outline btn-sm btn-delete-backup" data-dir="${b.dirName}">
|
|
578
|
+
<svg class="icon-xs"><use href="#icon-trash"/></svg>
|
|
579
|
+
<span>Delete</span>
|
|
580
|
+
</button>
|
|
581
|
+
</td>
|
|
582
|
+
`;
|
|
583
|
+
|
|
584
|
+
// Restore action
|
|
585
|
+
tr.querySelector('.btn-restore-backup').addEventListener('click', async (e) => {
|
|
586
|
+
const dir = e.currentTarget.dataset.dir;
|
|
587
|
+
const name = e.currentTarget.dataset.name;
|
|
588
|
+
if (!confirm(`Are you sure you want to restore "${name}" (${dir})? All current HTML files will be replaced with this snapshot.`)) return;
|
|
589
|
+
|
|
590
|
+
try {
|
|
591
|
+
const rRes = await fetch('/api/restore', {
|
|
592
|
+
method: 'POST',
|
|
593
|
+
headers: { 'Content-Type': 'application/json' },
|
|
594
|
+
body: JSON.stringify({ backupDirName: dir })
|
|
595
|
+
});
|
|
596
|
+
const rData = await rRes.json();
|
|
597
|
+
alert(rData.message);
|
|
598
|
+
backupsModal.classList.add('hidden');
|
|
599
|
+
loadScan();
|
|
600
|
+
} catch (err) {
|
|
601
|
+
alert('Restore failed: ' + err.message);
|
|
602
|
+
}
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
// Delete action
|
|
606
|
+
tr.querySelector('.btn-delete-backup').addEventListener('click', async (e) => {
|
|
607
|
+
const dir = e.currentTarget.dataset.dir;
|
|
608
|
+
if (!confirm(`Are you sure you want to delete backup "${dir}"? This cannot be undone.`)) return;
|
|
609
|
+
|
|
610
|
+
try {
|
|
611
|
+
const dRes = await fetch('/api/delete-backup', {
|
|
612
|
+
method: 'POST',
|
|
613
|
+
headers: { 'Content-Type': 'application/json' },
|
|
614
|
+
body: JSON.stringify({ backupDirName: dir })
|
|
615
|
+
});
|
|
616
|
+
const dData = await dRes.json();
|
|
617
|
+
loadBackupsList();
|
|
618
|
+
} catch (err) {
|
|
619
|
+
alert('Delete failed: ' + err.message);
|
|
620
|
+
}
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
backupsTableBody.appendChild(tr);
|
|
624
|
+
});
|
|
625
|
+
} catch (e) {
|
|
626
|
+
backupsTableBody.innerHTML = `<tr><td colspan="5" style="text-align: center; color: red;">Failed to load backups: ${e.message}</td></tr>`;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
if (btnOpenBackups) {
|
|
631
|
+
btnOpenBackups.addEventListener('click', () => {
|
|
632
|
+
backupsModal.classList.remove('hidden');
|
|
633
|
+
loadBackupsList();
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
if (closeBackupsModal) {
|
|
638
|
+
closeBackupsModal.addEventListener('click', () => {
|
|
639
|
+
backupsModal.classList.add('hidden');
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
if (btnCreateNamedBackup) {
|
|
644
|
+
btnCreateNamedBackup.addEventListener('click', async () => {
|
|
645
|
+
const name = customBackupName ? customBackupName.value.trim() : '';
|
|
646
|
+
btnCreateNamedBackup.disabled = true;
|
|
647
|
+
btnCreateNamedBackup.innerHTML = '<svg class="icon-sm icon-spin"><use href="#icon-refresh"/></svg> <span>Saving...</span>';
|
|
648
|
+
|
|
649
|
+
try {
|
|
650
|
+
const res = await fetch('/api/backup', {
|
|
651
|
+
method: 'POST',
|
|
652
|
+
headers: { 'Content-Type': 'application/json' },
|
|
653
|
+
body: JSON.stringify({ name })
|
|
654
|
+
});
|
|
655
|
+
const data = await res.json();
|
|
656
|
+
btnCreateNamedBackup.disabled = false;
|
|
657
|
+
btnCreateNamedBackup.innerHTML = '<svg class="icon-sm"><use href="#icon-database"/></svg> <span>Save Snapshot</span>';
|
|
658
|
+
if (customBackupName) customBackupName.value = '';
|
|
659
|
+
loadBackupsList();
|
|
660
|
+
} catch (err) {
|
|
661
|
+
btnCreateNamedBackup.disabled = false;
|
|
662
|
+
btnCreateNamedBackup.innerHTML = '<svg class="icon-sm"><use href="#icon-database"/></svg> <span>Save Snapshot</span>';
|
|
663
|
+
alert('Backup failed: ' + err.message);
|
|
664
|
+
}
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// 4. Clean Tracking Removal / Uninstaller
|
|
669
|
+
if (btnUninstallTracking) {
|
|
670
|
+
btnUninstallTracking.addEventListener('click', () => {
|
|
671
|
+
uninstallModal.classList.remove('hidden');
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
if (closeUninstallModal) closeUninstallModal.addEventListener('click', () => uninstallModal.classList.add('hidden'));
|
|
676
|
+
if (btnCancelUninstall) btnCancelUninstall.addEventListener('click', () => uninstallModal.classList.add('hidden'));
|
|
677
|
+
|
|
678
|
+
if (btnConfirmUninstall) {
|
|
679
|
+
btnConfirmUninstall.addEventListener('click', async () => {
|
|
680
|
+
btnConfirmUninstall.disabled = true;
|
|
681
|
+
btnConfirmUninstall.textContent = '⏳ Removing all tracking...';
|
|
682
|
+
|
|
683
|
+
try {
|
|
684
|
+
const res = await fetch('/api/remove-tracking', { method: 'POST' });
|
|
685
|
+
const data = await res.json();
|
|
686
|
+
btnConfirmUninstall.disabled = false;
|
|
687
|
+
btnConfirmUninstall.textContent = 'Confirm & Remove All Tracking';
|
|
688
|
+
uninstallModal.classList.add('hidden');
|
|
689
|
+
|
|
690
|
+
alert(data.message);
|
|
691
|
+
loadScan();
|
|
692
|
+
} catch (e) {
|
|
693
|
+
btnConfirmUninstall.disabled = false;
|
|
694
|
+
btnConfirmUninstall.textContent = 'Confirm & Remove All Tracking';
|
|
695
|
+
alert('Removal failed: ' + e.message);
|
|
696
|
+
}
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// 4.5. Selective Single Service Removal Buttons
|
|
701
|
+
document.querySelectorAll('.btn-remove-service').forEach(btn => {
|
|
702
|
+
btn.addEventListener('click', (e) => {
|
|
703
|
+
e.preventDefault();
|
|
704
|
+
const service = btn.dataset.service;
|
|
705
|
+
if (service) {
|
|
706
|
+
removeSingleService(service);
|
|
707
|
+
}
|
|
708
|
+
});
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
// 5. Fetch Apps Script Template Code
|
|
712
|
+
async function loadScriptTemplate() {
|
|
713
|
+
try {
|
|
714
|
+
const res = await fetch('/api/apps-script');
|
|
715
|
+
const data = await res.json();
|
|
716
|
+
scriptTemplateText = data.code;
|
|
717
|
+
scriptCodeDisplay.textContent = scriptTemplateText;
|
|
718
|
+
} catch (e) {}
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
if (openScriptModal) {
|
|
722
|
+
openScriptModal.addEventListener('click', (e) => {
|
|
723
|
+
e.preventDefault();
|
|
724
|
+
scriptModal.classList.remove('hidden');
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
if (closeScriptModal) {
|
|
729
|
+
closeScriptModal.addEventListener('click', () => {
|
|
730
|
+
scriptModal.classList.add('hidden');
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
if (copyScriptCode) {
|
|
735
|
+
copyScriptCode.addEventListener('click', () => {
|
|
736
|
+
navigator.clipboard.writeText(scriptTemplateText).then(() => {
|
|
737
|
+
copyScriptCode.innerHTML = '<svg class="icon-xs" style="color:var(--success)"><use href="#icon-check"/></svg> <span>Copied!</span>';
|
|
738
|
+
setTimeout(() => {
|
|
739
|
+
copyScriptCode.innerHTML = '<svg class="icon-xs"><use href="#icon-clipboard"/></svg> <span>Copy Code</span>';
|
|
740
|
+
}, 2000);
|
|
741
|
+
});
|
|
742
|
+
});
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
// Toggle helpers
|
|
746
|
+
function setupToggle(switchId, sectionId) {
|
|
747
|
+
const sw = document.getElementById(switchId);
|
|
748
|
+
const sec = document.getElementById(sectionId);
|
|
749
|
+
if (!sw || !sec) return;
|
|
750
|
+
sw.addEventListener('change', () => {
|
|
751
|
+
sec.style.opacity = sw.checked ? '1' : '0.4';
|
|
752
|
+
sec.style.pointerEvents = sw.checked ? 'all' : 'none';
|
|
753
|
+
});
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
setupToggle('enableGTM', 'gtmSection');
|
|
757
|
+
setupToggle('enableMeta', 'metaSection');
|
|
758
|
+
setupToggle('enableBuzlCapi', 'buzlCapiSection');
|
|
759
|
+
setupToggle('enableSheets', 'sheetsSection');
|
|
760
|
+
setupToggle('enableZoho', 'zohoSection');
|
|
761
|
+
setupToggle('enableWhatsapp', 'whatsappSection');
|
|
762
|
+
|
|
763
|
+
// Collect Current Configuration
|
|
764
|
+
function getCurrentConfig() {
|
|
765
|
+
const isCapiEnabled = document.getElementById('enableBuzlCapi').checked;
|
|
766
|
+
const isZohoEnabled = document.getElementById('enableZoho').checked;
|
|
767
|
+
const zohoFormId = isZohoEnabled ? document.getElementById('zohoXnqsjsdp').value.trim() : '';
|
|
768
|
+
|
|
769
|
+
return {
|
|
770
|
+
gtmId: document.getElementById('enableGTM').checked ? document.getElementById('gtmId').value.trim() : '',
|
|
771
|
+
enableDeferred: document.getElementById('enableDeferred').checked,
|
|
772
|
+
metaPixelId: document.getElementById('enableMeta').checked ? document.getElementById('metaPixelId').value.trim() : '',
|
|
773
|
+
trackMetaLeadEvent: document.getElementById('trackMetaLead').checked,
|
|
774
|
+
googleSheetUrl: document.getElementById('enableSheets').checked ? document.getElementById('googleSheetUrl').value.trim() : '',
|
|
775
|
+
siteLocation: siteLocationInput ? siteLocationInput.value.trim() : '',
|
|
776
|
+
dynamicFields: activeDynamicFields,
|
|
777
|
+
buzlCapi: {
|
|
778
|
+
endpoint: isCapiEnabled ? document.getElementById('buzlCapiEndpoint').value.trim() : '',
|
|
779
|
+
authUser: isCapiEnabled ? document.getElementById('buzlCapiUser').value.trim() : '',
|
|
780
|
+
authPass: isCapiEnabled ? document.getElementById('buzlCapiPass').value.trim() : ''
|
|
781
|
+
},
|
|
782
|
+
zoho: {
|
|
783
|
+
endpoint: (isZohoEnabled && zohoFormId) ? document.getElementById('zohoEndpoint').value : '',
|
|
784
|
+
xnQsjsdp: zohoFormId,
|
|
785
|
+
xmIwtLD: isZohoEnabled ? document.getElementById('zohoXmiwtld').value.trim() : ''
|
|
786
|
+
},
|
|
787
|
+
whatsappNumber: document.getElementById('enableWhatsapp').checked ? document.getElementById('whatsappNumber').value.trim() : ''
|
|
788
|
+
};
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
// 6. Inject & Run Tests Form Submit
|
|
792
|
+
form.addEventListener('submit', async (e) => {
|
|
793
|
+
e.preventDefault();
|
|
794
|
+
|
|
795
|
+
btnSubmit.disabled = true;
|
|
796
|
+
btnSubmit.innerHTML = '<svg class="icon-sm icon-spin"><use href="#icon-refresh"/></svg> <span>Injecting & Running Tests...</span>';
|
|
797
|
+
testSummaryBadge.textContent = 'Testing...';
|
|
798
|
+
testSummaryBadge.className = 'badge badge-info';
|
|
799
|
+
testConsole.innerHTML = '<p class="empty-state">Executing injection and running automated verification tests...</p>';
|
|
800
|
+
|
|
801
|
+
const payload = getCurrentConfig();
|
|
802
|
+
|
|
803
|
+
try {
|
|
804
|
+
const res = await fetch('/api/inject', {
|
|
805
|
+
method: 'POST',
|
|
806
|
+
headers: { 'Content-Type': 'application/json' },
|
|
807
|
+
body: JSON.stringify(payload)
|
|
808
|
+
});
|
|
809
|
+
const result = await res.json();
|
|
810
|
+
|
|
811
|
+
btnSubmit.disabled = false;
|
|
812
|
+
btnSubmit.innerHTML = '<svg class="icon-sm"><use href="#icon-brand-buzl"/></svg> <span>Apply Configuration & Run Automated Tests</span>';
|
|
813
|
+
|
|
814
|
+
if (result.success && result.testReport) {
|
|
815
|
+
const report = result.testReport;
|
|
816
|
+
testSummaryBadge.textContent = `${report.passedCount}/${report.total} Passed`;
|
|
817
|
+
testSummaryBadge.className = report.allPassed ? 'badge badge-success' : 'badge badge-danger';
|
|
818
|
+
|
|
819
|
+
testConsole.innerHTML = '';
|
|
820
|
+
report.tests.forEach(t => {
|
|
821
|
+
const item = document.createElement('div');
|
|
822
|
+
item.className = 'test-item';
|
|
823
|
+
const icon = t.passed
|
|
824
|
+
? '<svg class="test-status-svg pass"><use href="#icon-check"/></svg>'
|
|
825
|
+
: '<svg class="test-status-svg fail"><use href="#icon-x"/></svg>';
|
|
826
|
+
item.innerHTML = `
|
|
827
|
+
${icon}
|
|
828
|
+
<div class="test-text">
|
|
829
|
+
<div>${t.name}</div>
|
|
830
|
+
${t.detail ? `<div class="test-detail">${t.detail}</div>` : ''}
|
|
831
|
+
</div>
|
|
832
|
+
`;
|
|
833
|
+
testConsole.appendChild(item);
|
|
834
|
+
});
|
|
835
|
+
|
|
836
|
+
loadScan();
|
|
837
|
+
} else {
|
|
838
|
+
testSummaryBadge.textContent = 'Failed';
|
|
839
|
+
testSummaryBadge.className = 'badge badge-danger';
|
|
840
|
+
testConsole.innerHTML = `<p class="test-icon fail">Error: ${result.message || 'Injection failed'}</p>`;
|
|
841
|
+
}
|
|
842
|
+
} catch (err) {
|
|
843
|
+
btnSubmit.disabled = false;
|
|
844
|
+
btnSubmit.innerHTML = '<svg class="icon-sm"><use href="#icon-brand-buzl"/></svg> <span>Apply Configuration & Run Automated Tests</span>';
|
|
845
|
+
testSummaryBadge.textContent = 'Network Error';
|
|
846
|
+
testSummaryBadge.className = 'badge badge-danger';
|
|
847
|
+
testConsole.innerHTML = `<p class="test-icon fail">Error communicating with local server: ${err.message}</p>`;
|
|
848
|
+
}
|
|
849
|
+
});
|
|
850
|
+
|
|
851
|
+
// 7. Quick Multi-Channel Test Lead Button
|
|
852
|
+
if (btnTestSubmit) {
|
|
853
|
+
btnTestSubmit.addEventListener('click', async () => {
|
|
854
|
+
const config = getCurrentConfig();
|
|
855
|
+
const phone = testLeadPhone.value.trim() || (currentScanData && currentScanData.detectedWhatsapp) || '';
|
|
856
|
+
const name = testLeadName.value.trim() || 'Test Lead';
|
|
857
|
+
const service = testLeadService.value.trim() || (currentScanData && currentScanData.detectedService) || 'General Inquiry';
|
|
858
|
+
const loc = (siteLocationInput ? siteLocationInput.value.trim() : '') || (currentScanData && currentScanData.detectedLocation) || '';
|
|
859
|
+
|
|
860
|
+
btnTestSubmit.disabled = true;
|
|
861
|
+
btnTestSubmit.innerHTML = '<svg class="icon-sm icon-spin"><use href="#icon-refresh"/></svg> <span>Sending Live Test Lead...</span>';
|
|
862
|
+
liveTestBadge.textContent = 'Testing...';
|
|
863
|
+
liveTestBadge.className = 'badge badge-info';
|
|
864
|
+
testDispatchResults.classList.remove('hidden');
|
|
865
|
+
testDispatchResults.innerHTML = '<p class="dispatch-detail">Dispatching synthetic test lead across active channels...</p>';
|
|
866
|
+
|
|
867
|
+
try {
|
|
868
|
+
const res = await fetch('/api/test-submit', {
|
|
869
|
+
method: 'POST',
|
|
870
|
+
headers: { 'Content-Type': 'application/json' },
|
|
871
|
+
body: JSON.stringify({
|
|
872
|
+
config,
|
|
873
|
+
lead: {
|
|
874
|
+
phone,
|
|
875
|
+
name,
|
|
876
|
+
service,
|
|
877
|
+
location: loc
|
|
878
|
+
}
|
|
879
|
+
})
|
|
880
|
+
});
|
|
881
|
+
|
|
882
|
+
const data = await res.json();
|
|
883
|
+
btnTestSubmit.disabled = false;
|
|
884
|
+
btnTestSubmit.innerHTML = '<svg class="icon-sm"><use href="#icon-send"/></svg> <span>Dispatch Quick Live Test Lead</span>';
|
|
885
|
+
|
|
886
|
+
if (data.success && data.results) {
|
|
887
|
+
const channels = data.results.channels;
|
|
888
|
+
testDispatchResults.innerHTML = '';
|
|
889
|
+
|
|
890
|
+
let anyFailed = false;
|
|
891
|
+
|
|
892
|
+
for (const [key, ch] of Object.entries(channels)) {
|
|
893
|
+
const row = document.createElement('div');
|
|
894
|
+
row.className = 'dispatch-row';
|
|
895
|
+
|
|
896
|
+
const labelMap = {
|
|
897
|
+
googleSheets: 'Google Sheets',
|
|
898
|
+
buzlCapi: 'Buzl CAPI',
|
|
899
|
+
zoho: 'Zoho CRM'
|
|
900
|
+
};
|
|
901
|
+
const serviceName = labelMap[key] || key;
|
|
902
|
+
|
|
903
|
+
let badgeHtml = '';
|
|
904
|
+
if (!ch.tested) {
|
|
905
|
+
badgeHtml = `<span class="badge badge-neutral">Disabled</span>`;
|
|
906
|
+
} else if (ch.ok) {
|
|
907
|
+
badgeHtml = `<span class="badge badge-success"><svg class="icon-xs"><use href="#icon-check"/></svg> <span>Success</span></span>`;
|
|
908
|
+
} else {
|
|
909
|
+
anyFailed = true;
|
|
910
|
+
badgeHtml = `<span class="badge badge-danger"><svg class="icon-xs"><use href="#icon-x"/></svg> <span>Failed</span></span>`;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
row.innerHTML = `
|
|
914
|
+
<div>
|
|
915
|
+
<div class="dispatch-service">${serviceName}</div>
|
|
916
|
+
<div class="dispatch-detail">${ch.message}</div>
|
|
917
|
+
</div>
|
|
918
|
+
<div>${badgeHtml}</div>
|
|
919
|
+
`;
|
|
920
|
+
testDispatchResults.appendChild(row);
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
liveTestBadge.textContent = anyFailed ? 'Issues Found' : 'All Channels OK';
|
|
924
|
+
liveTestBadge.className = anyFailed ? 'badge badge-danger' : 'badge badge-success';
|
|
925
|
+
|
|
926
|
+
} else {
|
|
927
|
+
testDispatchResults.innerHTML = `<p class="dispatch-detail text-danger">${data.message || 'Failed to dispatch test lead'}</p>`;
|
|
928
|
+
}
|
|
929
|
+
} catch (err) {
|
|
930
|
+
btnTestSubmit.disabled = false;
|
|
931
|
+
btnTestSubmit.innerHTML = '<svg class="icon-sm"><use href="#icon-send"/></svg> <span>Dispatch Quick Live Test Lead</span>';
|
|
932
|
+
testDispatchResults.innerHTML = `<p class="dispatch-detail text-danger">Error: ${err.message}</p>`;
|
|
933
|
+
}
|
|
934
|
+
});
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
if (btnScan) btnScan.addEventListener('click', loadScan);
|
|
938
|
+
|
|
939
|
+
// Initialize
|
|
940
|
+
loadScan();
|
|
941
|
+
loadScriptTemplate();
|
|
942
|
+
});
|