// edit-site.js โ extracted from edit_site.html
// Data injected via window.__SITE_DATA__
// Tab switching
function showPanel(name) {
document.querySelectorAll('.panel').forEach(p => p.classList.remove('active'));
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
document.getElementById('panel-' + name).classList.add('active');
event.target.classList.add('active');
}
// Toggle advanced panel
function toggleAdvanced(btn) {
var row = btn.closest('.field-row');
var adv = row.querySelector('.field-advanced');
var isOpen = adv.classList.contains('open');
adv.classList.toggle('open');
btn.classList.toggle('active');
btn.textContent = isOpen ? 'โ Advanced' : 'โ Close';
}
// Field type change handler โ update advanced panel sections
function onFieldTypeChange(select) {
var row = select.closest('.field-row');
var adv = row.querySelector('.field-advanced');
var ftype = select.value;
updateAdvancedSections(adv, ftype);
}
function updateAdvancedSections(adv, ftype) {
// Show/hide sections based on type
var sections = adv.querySelectorAll('.field-adv-section[data-section]');
sections.forEach(function(sec) {
var section = sec.getAttribute('data-section');
if (section === 'basic') {
sec.style.display = ftype === 'computed' ? 'none' : 'block';
} else if (section === 'validation') {
var show = (ftype === 'text' || ftype === 'email' || ftype === 'textarea' ||
ftype === 'phone' || ftype === 'number' || ftype === 'select' || ftype === 'radio');
sec.style.display = show ? 'block' : 'none';
} else if (section === 'condition') {
sec.style.display = ftype !== 'computed' ? 'block' : 'none';
} else if (section === 'calculation') {
sec.style.display = ftype === 'computed' ? 'block' : 'none';
} else if (section === 'step') {
sec.style.display = 'block';
}
});
// Show/hide min/max vs minLength/maxLength based on type
var numFields = adv.querySelectorAll('[data-num-only]');
var lenFields = adv.querySelectorAll('[data-len-only]');
var patternFields = adv.querySelectorAll('[data-pattern-only]');
numFields.forEach(function(el) { el.style.display = ftype === 'number' ? 'block' : 'none'; });
lenFields.forEach(function(el) { el.style.display = (ftype === 'text' || ftype === 'email' || ftype === 'textarea' || ftype === 'phone') ? 'block' : 'none'; });
patternFields.forEach(function(el) { el.style.display = (ftype === 'text' || ftype === 'email' || ftype === 'textarea' || ftype === 'phone') ? 'block' : 'none'; });
// Trigger inline validation
validateFieldRow(row);
}
// ========================
// INLINE FIELD VALIDATION
// ========================
function validateFieldRow(row) {
var errors = [];
var key = (row.querySelector('.field-key') || {}).value || '';
var label = (row.querySelector('.field-label') || {}).value || '';
var ftype = (row.querySelector('.field-type') || {}).value || 'text';
// Key checks
if (!key) errors.push('Field key required');
else if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(key)) errors.push('Key must start with letter/_, then alphanumeric/_');
else {
var allKeys = [];
document.querySelectorAll('#field-list .field-row').forEach(function(r) {
if (r !== row) {
var k = (r.querySelector('.field-key') || {}).value || '';
if (k) allKeys.push(k);
}
});
if (allKeys.indexOf(key) !== -1) errors.push('Duplicate key: "' + key + '"');
}
if (!label) errors.push('Label required');
// Validation rule checks
var adv = row.querySelector('.field-advanced');
if (adv) {
var gv = function(sel) { var el = adv.querySelector(sel); return el ? el.value : ''; };
var gn = function(sel) { var el = adv.querySelector(sel); return el ? el.value : ''; };
// Regex pattern
var pattern = gv('[data-field-pattern]');
if (pattern) {
try { new RegExp(pattern); } catch(e) { errors.push('Invalid regex pattern'); }
}
// Min/Max (number type)
if (ftype === 'number') {
var min = gn('[data-field-min]');
var max = gn('[data-field-max]');
if (min && max && Number(min) >= Number(max)) errors.push('Min must be less than Max');
}
// MinLength/MaxLength
var minLength = gn('[data-field-min-length]');
var maxLength = gn('[data-field-max-length]');
if (minLength && maxLength && Number(minLength) > Number(maxLength)) errors.push('Min length must be โค Max length');
if (minLength && Number(minLength) < 0) errors.push('Min length cannot be negative');
if (maxLength && Number(maxLength) < 0) errors.push('Max length cannot be negative');
}
// Render errors
renderFieldErrors(row, errors);
}
function renderFieldErrors(row, errors) {
// Remove existing error elements
var existingBar = row.querySelector('.field-error-bar');
if (existingBar) existingBar.remove();
var existingList = row.querySelector('.field-error-list');
if (existingList) existingList.remove();
if (errors.length === 0) {
row.classList.remove('has-error');
return;
}
row.classList.add('has-error');
// Error bar on right
var bar = document.createElement('div');
bar.className = 'field-error-bar';
row.appendChild(bar);
// Error list below field-item
var item = row.querySelector('.field-item');
if (item) {
var ul = document.createElement('ul');
ul.className = 'field-error-list';
errors.forEach(function(err) {
var li = document.createElement('li');
li.textContent = err;
ul.appendChild(li);
});
item.parentNode.insertBefore(ul, item.nextSibling);
}
}
// Collect field data from a row (basic + advanced)
function getFieldFromRow(row) {
var item = row.querySelector('.field-item');
var adv = row.querySelector('.field-advanced');
var field = {
key: item.querySelector('.field-key').value.trim(),
type: item.querySelector('.field-type').value,
label: item.querySelector('.field-label').value.trim(),
required: item.querySelector('.field-required').value === '1'
};
// Advanced fields
if (adv) {
var val = function(cls) {
var el = adv.querySelector(cls);
return el ? el.value.trim() : '';
};
var numVal = function(cls) {
var el = adv.querySelector(cls);
return el ? el.value : '';
};
// Basic
field.placeholder = val('[data-field-placeholder]');
field.default = val('[data-field-default]');
// Options (select/radio)
var optionsStr = val('[data-field-options]');
field.options = optionsStr ? optionsStr.split(',').map(function(s){ return s.trim(); }).filter(function(s){ return s; }) : [];
// Validation
var pattern = val('[data-field-pattern]');
var errorMsg = val('[data-field-error-message]');
if (pattern) field.pattern = pattern;
if (errorMsg) field.errorMessage = errorMsg;
var min = numVal('[data-field-min]');
var max = numVal('[data-field-max]');
if (min) field.min = Number(min);
if (max) field.max = Number(max);
var minLength = numVal('[data-field-min-length]');
var maxLength = numVal('[data-field-max-length]');
if (minLength) field.minLength = Number(minLength);
if (maxLength) field.maxLength = Number(maxLength);
// Condition
var condField = val('[data-cond-field]');
var condOp = val('[data-cond-operator]');
var condVal = val('[data-cond-value]');
if (condField && condOp) {
field.condition = { field: condField, operator: condOp };
if (condVal) field.condition.value = condVal;
}
// Calculation
var formula = val('[data-calc-formula]');
var calcRound = numVal('[data-calc-round]');
if (formula) {
field.calculation = { formula: formula };
if (calcRound) field.calculation.round = Number(calcRound);
}
// Step
var step = numVal('[data-field-step]');
if (step) field.step = parseInt(step);
}
return field;
}
// Get all fields
function getFields() {
var rows = document.querySelectorAll('#field-list .field-row');
var fields = [];
rows.forEach(function(row) {
var f = getFieldFromRow(row);
if (f.key) fields.push(f);
});
return fields;
}
function addField() {
var list = document.getElementById('field-list');
list.style.display = 'flex';
document.getElementById('empty-state').style.display = 'none';
var row = document.createElement('div');
row.className = 'field-row';
row.innerHTML = '<div class="field-item">' +
'<input type="text" class="field-key" placeholder="Field key (e.g. name)">' +
'<select class="field-type" data-onchange="onFieldTypeChange(this)">' +
'<option value="text">Text</option>' +
'<option value="email">Email</option>' +
'<option value="phone">Phone</option>' +
'<option value="number">Number</option>' +
'<option value="textarea">Textarea</option>' +
'<option value="select">Select</option>' +
'<option value="radio">Radio</option>' +
'<option value="checkbox">Checkbox</option>' +
'<option value="date">Date</option>' +
'<option value="computed">Computed</option>' +
'</select>' +
'<input type="text" class="field-label" placeholder="Display label">' +
'<select class="field-required">' +
'<option value="0">Optional</option>' +
'<option value="1">Required</option>' +
'</select>' +
'<div class="field-actions">' +
'<button type="button" class="field-expand-btn" data-onclick="toggleAdvanced(this)">โ Advanced</button>' +
'<button type="button" class="field-remove" data-onclick="removeField(this)">×</button>' +
'</div>' +
'</div>' +
'<div class="field-advanced">' +
'<div class="field-adv-section" data-section="basic">' +
'<div class="field-adv-section-title">Basic</div>' +
'<div class="field-advanced-grid">' +
'<div class="field-adv-group">' +
'<label>Placeholder</label>' +
'<input type="text" data-field-placeholder placeholder="Hint text...">' +
'</div>' +
'<div class="field-adv-group">' +
'<label>Default Value</label>' +
'<input type="text" data-field-default placeholder="Default value...">' +
'</div>' +
'<div class="field-adv-group full-width" data-options-only>' +
'<label>Options (comma-separated)</label>' +
'<input type="text" data-field-options placeholder="Option 1, Option 2, Option 3">' +
'<div class="field-adv-hint">For select/radio field types.</div>' +
'</div>' +
'</div>' +
'</div>' +
'<div class="field-adv-section" data-section="validation">' +
'<div class="field-adv-section-title">Validation</div>' +
'<div class="field-advanced-grid">' +
'<div class="field-adv-group" data-num-only>' +
'<label>Min Value</label>' +
'<input type="number" data-field-min step="any">' +
'</div>' +
'<div class="field-adv-group" data-num-only>' +
'<label>Max Value</label>' +
'<input type="number" data-field-max step="any">' +
'</div>' +
'<div class="field-adv-group" data-len-only>' +
'<label>Min Length</label>' +
'<input type="number" data-field-min-length min="0" step="1">' +
'</div>' +
'<div class="field-adv-group" data-len-only>' +
'<label>Max Length</label>' +
'<input type="number" data-field-max-length min="0" step="1">' +
'</div>' +
'<div class="field-adv-group full-width" data-pattern-only>' +
'<label>Pattern (Regex)</label>' +
'<input type="text" data-field-pattern placeholder="^[A-Z]{2}\\d{4}$">' +
'<div class="field-adv-hint">JavaScript regex pattern for custom validation.</div>' +
'</div>' +
'<div class="field-adv-group full-width">' +
'<label>Error Message</label>' +
'<input type="text" data-field-error-message placeholder="Custom error message...">' +
'</div>' +
'</div>' +
'</div>' +
'<div class="field-adv-section" data-section="condition">' +
'<div class="field-adv-section-title">Conditional Visibility</div>' +
'<div class="field-advanced-grid">' +
'<div class="field-adv-group">' +
'<label>Depends On (Field Key)</label>' +
'<input type="text" data-field-condition-field placeholder="e.g. question_type">' +
'</div>' +
'<div class="field-adv-group">' +
'<label>Condition</label>' +
'<select data-field-condition-op>' +
'<option value="">Always visible</option>' +
'<option value="equals">Is</option>' +
'<option value="not_equals">Is Not</option>' +
'<option value="contains">Contains</option>' +
'<option value="not_contains">Does Not Contain</option>' +
'</select>' +
'</div>' +
'<div class="field-adv-group full-width">' +
'<label>Dependent Value</label>' +
'<input type="text" data-field-condition-value placeholder="Value that triggers visibility">' +
'</div>' +
'</div>' +
'</div>' +
'<div class="field-adv-section" data-section="calculation" style="display:none;">' +
'<div class="field-adv-section-title">Calculation</div>' +
'<div class="field-advanced-grid">' +
'<div class="field-adv-group full-width">' +
'<label>Formula</label>' +
'<input type="text" data-field-formula placeholder="e.g. total * 0.08">' +
'<div class="field-adv-hint">Use field keys as variables. e.g. <code>qty * price</code></div>' +
'</div>' +
'</div>' +
'</div>' +
'<div class="field-adv-section" data-section="step">' +
'<div class="field-adv-section-title">Multi-Step</div>' +
'<div class="field-advanced-grid">' +
'<div class="field-adv-group">' +
'<label>Step Number</label>' +
'<input type="number" data-field-step min="1" step="1" placeholder="1">' +
'<div class="field-adv-hint">Leave blank for default (Step 1). Group fields by step number for multi-step forms.</div>' +
'</div>' +
'</div>' +
'</div>' +
'</div>';
row.querySelector('.field-key').focus();
// Apply current filter if active
var searchInput = document.getElementById('field-search-input');
if (searchInput && searchInput.value.trim()) {
filterFields(searchInput.value);
}
list.appendChild(row);
// Show options field only for select/radio
var optionsEl = row.querySelector('[data-options-only]');
if (optionsEl) optionsEl.style.display = 'none';
// Init advanced sections
var adv = row.querySelector('.field-advanced');
updateAdvancedSections(adv, 'text');
// Wire up inline validation on input changes
row.addEventListener('input', function(e) {
validateFieldRow(row);
});
}
// ========================
// FIELD SEARCH / FILTER
// ========================
function filterFields(query) {
var q = query.trim().toLowerCase();
var rows = document.querySelectorAll('#field-list .field-row');
var count = 0;
var total = rows.length;
rows.forEach(function(row) {
var item = row.querySelector('.field-item');
if (!item) return;
var key = (item.querySelector('.field-key') || {}).value || '';
var label = (item.querySelector('.field-label') || {}).value || '';
var type = (item.querySelector('.field-type') || {}).value || '';
if (!q || key.toLowerCase().includes(q) || label.toLowerCase().includes(q) || type.includes(q)) {
row.classList.remove('hidden');
count++;
} else {
row.classList.add('hidden');
}
});
// Update UI
var clearBtn = document.getElementById('field-search-clear');
var countEl = document.getElementById('field-search-count');
if (clearBtn) clearBtn.style.display = q ? 'block' : 'none';
if (countEl) {
if (q) {
countEl.textContent = count + ' of ' + total + ' fields shown';
} else {
countEl.textContent = '';
}
}
}
function clearFieldFilter() {
var input = document.getElementById('field-search-input');
if (input) {
input.value = '';
filterFields('');
input.focus();
}
}
function removeField(btn) {
var row = btn.closest('.field-row');
row.remove();
var list = document.getElementById('field-list');
if (list.querySelectorAll('.field-row').length === 0) {
list.style.display = 'none';
document.getElementById('empty-state').style.display = 'block';
}
}
// Initialize advanced panels for existing fields on page load
document.addEventListener('DOMContentLoaded', function() {
var rows = document.querySelectorAll('#field-list .field-row');
rows.forEach(function(row) {
var item = row.querySelector('.field-item');
var typeSelect = item.querySelector('.field-type');
var ftype = typeSelect.value;
var adv = row.querySelector('.field-advanced');
if (adv) {
updateAdvancedSections(adv, ftype);
// Show options for select/radio
var optionsEl = adv.querySelector('[data-options-only]');
if (optionsEl) {
optionsEl.style.display = (ftype === 'select' || ftype === 'radio') ? 'block' : 'none';
}
}
// Wire up inline validation
row.addEventListener('input', function(e) {
validateFieldRow(row);
});
validateFieldRow(row);
});
});
// Also handle options visibility on type change
var origOnType = onFieldTypeChange;
onFieldTypeChange = function(select) {
var row = select.closest('.field-row');
var adv = row.querySelector('.field-advanced');
var ftype = select.value;
updateAdvancedSections(adv, ftype);
var optionsEl = adv.querySelector('[data-options-only]');
if (optionsEl) {
optionsEl.style.display = (ftype === 'select' || ftype === 'radio') ? 'block' : 'none';
}
};
// Serialize fields before form submit
document.getElementById('edit-form').addEventListener('submit', function(e) {
document.getElementById('field_config_json').value = JSON.stringify(getFields());
document.getElementById('hidden_webhook_url').value = document.getElementById('webhook_url').value;
document.getElementById('hidden_webhook_enabled').checked = document.getElementById('webhook_enabled').checked;
document.getElementById('hidden_webhook_events').value = document.getElementById('webhook_events').value;
});
document.getElementById('webhook-form').addEventListener('submit', function(e) {
document.getElementById('hidden_field_config').value = JSON.stringify(getFields());
});
document.getElementById('protection-form').addEventListener('submit', function(e) {
document.getElementById('hidden_protection_field_config').value = JSON.stringify(getFields());
});
// Toggle rate limit config visibility
document.getElementById('rate_limit_enabled').addEventListener('change', function(e) {
document.getElementById('rate_limit_config').style.display = e.target.checked ? 'block' : 'none';
});
// Embed code copy
function copyEmbedCode() {
var snippet = document.getElementById('embed-snippet').textContent;
navigator.clipboard.writeText(snippet).then(function() {
var fb = document.getElementById('copy-feedback');
fb.style.display = 'inline';
setTimeout(function() { fb.style.display = 'none'; }, 2000);
});
}
// Share panel โ copy hosted form URL
function copyShareUrl(token) {
var baseUrl = window.location.origin;
var url = baseUrl + '/f/' + token;
document.getElementById('share-url-display').textContent = url;
navigator.clipboard.writeText(url).then(function() {
var fb = document.getElementById('share-url-feedback');
fb.style.display = 'block';
setTimeout(function() { fb.style.display = 'none'; }, 2000);
});
}
// Share panel โ load & copy email HTML
var emailHtmlCache = '';
function loadEmailHtml(token) {
if (emailHtmlCache) return;
fetch('/api/v2/forms/' + token + '/email-html')
.then(function(r) { return r.text(); })
.then(function(html) {
emailHtmlCache = html;
var preview = document.getElementById('email-html-preview');
if (preview) {
preview.textContent = html.substring(0, 500) + (html.length > 500 ? '\n...(truncated, click Copy to get full HTML)' : '');
}
})
.catch(function() {
var preview = document.getElementById('email-html-preview');
if (preview) preview.textContent = 'Failed to load email HTML.';
});
}
function copyEmailHtml(token) {
if (!emailHtmlCache) {
loadEmailHtml(token);
var fb = document.getElementById('email-html-feedback');
fb.textContent = 'Loading...';
fb.style.display = 'block';
fb.style.color = 'var(--warning)';
return;
}
navigator.clipboard.writeText(emailHtmlCache).then(function() {
var fb = document.getElementById('email-html-feedback');
fb.textContent = '\u2713 Copied to clipboard!';
fb.style.color = 'var(--success)';
fb.style.display = 'block';
setTimeout(function() { fb.style.display = 'none'; }, 2000);
});
}
// Embed theme toggle
document.getElementById('embed-theme-check').addEventListener('change', function(e) {
var snippet = document.getElementById('embed-snippet');
var token = window.__SITE_DATA__.siteToken;
var theme = e.target.checked ? 'dark' : 'light';
snippet.innerHTML = '<script src="https://agentforms.io/embed.js"><\\/script>\\n<div id="agentform"><\\/div>\\n<script>\\n AF.render(\\'agentform\\', {\\n token: \\' + token + \\',\\n theme: \\' + theme + \\',\\n successMessage: "Thank you! Your submission was received.",\\n onSubmit: function(data) { /* analytics hook */ }\\n });\\n<\\/script>';
});
// Branding panel - load/save branding settings
var brandingLoaded = false;
function loadBranding() {
if (brandingLoaded) return;
brandingLoaded = true;
fetch(''/api/v2/sites/' + window.__SITE_DATA__.siteId + '/branding'')
.then(function(r) { return r.json(); })
.then(function(data) {
// Populate fields
if (data.branding_color) {
document.getElementById('branding-color-picker').value = data.branding_color;
document.getElementById('branding-color-input').value = data.branding_color;
document.getElementById('branding-color-preview').style.background = data.branding_color;
}
if (data.logo_url) document.getElementById('branding-logo-url').value = data.logo_url;
if (data.favicon_url) document.getElementById('branding-favicon-url').value = data.favicon_url;
if (data.og_title) document.getElementById('branding-og-title').value = data.og_title;
if (data.og_description) document.getElementById('branding-og-description').value = data.og_description;
if (data.og_image) document.getElementById('branding-og-image').value = data.og_image;
if (data.custom_domain) document.getElementById('branding-custom-domain').value = data.custom_domain;
// Show/hide custom domain section based on Pro tier
if (data.is_pro) {
document.getElementById('branding-custom-domain-section').style.display = 'block';
}
// Color picker sync
document.getElementById('branding-color-picker').addEventListener('input', function(e) {
document.getElementById('branding-color-input').value = e.target.value;
document.getElementById('branding-color-preview').style.background = e.target.value;
});
document.getElementById('branding-color-input').addEventListener('input', function(e) {
if (/^#[0-9a-fA-F]{6}$/.test(e.target.value)) {
document.getElementById('branding-color-picker').value = e.target.value;
document.getElementById('branding-color-preview').style.background = e.target.value;
}
});
})
.catch(function(err) { console.error('Failed to load branding:', err); });
}
function saveBranding() {
var data = {
branding_color: document.getElementById('branding-color-input').value || null,
logo_url: document.getElementById('branding-logo-url').value || null,
favicon_url: document.getElementById('branding-favicon-url').value || null,
og_title: document.getElementById('branding-og-title').value || null,
og_description: document.getElementById('branding-og-description').value || null,
og_image: document.getElementById('branding-og-image').value || null,
custom_domain: document.getElementById('branding-custom-domain').value || null,
};
fetch(''/api/v2/sites/' + window.__SITE_DATA__.siteId + '/branding'', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
})
.then(function(r) { return r.json(); })
.then(function(result) {
if (result.success) {
var feedback = document.getElementById('branding-save-feedback');
feedback.style.display = 'inline';
setTimeout(function() { feedback.style.display = 'none'; }, 3000);
} else {
alert('Error: ' + result.error);
}
})
.catch(function(err) { console.error('Failed to save branding:', err); });
}
// Analytics - fetch on tab click (triggered by showPanel override below)
var analyticsLoaded = false;
function fetchAnalytics(token) {
fetch('/api/analytics/json/' + token)
.then(function(r) { return r.json(); })
.then(function(data) {
document.getElementById('analytics-loading').style.display = 'none';
// Total submissions
document.getElementById('analytics-total').textContent = data.total_submissions || 0;
// 7-day total
var days7 = data.daily_submissions.slice(0, 7);
var total7d = days7.reduce(function(s, d) { return s + d.count; }, 0);
document.getElementById('analytics-7d').textContent = total7d;
// Avg/day
var avg = days7.length > 0 ? (total7d / Math.max(days7.length, 1)).toFixed(1) : 0;
document.getElementById('analytics-avg').textContent = avg;
// Bar chart - last 7 days
renderBarChart(days7);
})
.catch(function(err) {
document.getElementById('analytics-loading').textContent = 'Failed to load analytics.';
});
}
function renderBarChart(days) {
var container = document.getElementById('bar-chart');
var labels = document.getElementById('bar-labels');
container.innerHTML = '';
labels.innerHTML = '';
var maxCount = Math.max(1, ...days.map(function(d) { return d.count; }));
days.forEach(function(day) {
var pct = (day.count / maxCount) * 100;
var bar = document.createElement('div');
bar.className = 'bar-segment';
bar.style.flex = '1';
bar.style.height = Math.max(2, pct) + '%';
bar.title = day.date + ': ' + day.count + ' submissions';
container.appendChild(bar);
var label = document.createElement('div');
label.className = 'bar-label';
label.textContent = day.date.slice(5); // MM-DD
labels.appendChild(label);
});
document.getElementById('analytics-chart').style.display = 'block';
}
// ========================
// INTEGRATIONS
// ========================
var SITE_ID = window.__SITE_DATA__.siteId;
var CSRF_TOKEN = 'window.__SITE_DATA__.csrfToken';
var integrationsLoaded = false;
var availableTypes = [];
var selectedIntType = null;
var editingIntId = null;
// Integration type defaults (used as fallback when API hasn't loaded)
var INTEGRATION_TYPES = {
webhook: { icon: '๐', name: 'Webhook', color: '#6366f1' },
google_sheets: { icon: '๐', name: 'Google Sheets', color: '#34a853' },
slack: { icon: '๐ฌ', name: 'Slack', color: '#4a154b' },
discord: { icon: '๐ฎ', name: 'Discord', color: '#5865f2' },
telegram: { icon: 'โ๏ธ', name: 'Telegram', color: '#0088cc' },
airtable: { icon: '๐', name: 'Airtable', color: '#188fff' },
notion: { icon: '๐', name: 'Notion', color: '#000000' },
};
// Override showPanel to load panel-specific data
var _origShowPanel = showPanel;
showPanel = function(name) {
_origShowPanel(name);
if (name === 'analytics' && !analyticsLoaded) {
analyticsLoaded = true;
fetchAnalytics(window.__SITE_DATA__.siteToken);
}
if (name === 'integrations' && !integrationsLoaded) {
integrationsLoaded = true;
loadIntegrations();
loadAvailableTypes();
}
if (name === 'share') {
loadEmailHtml(window.__SITE_DATA__.siteToken);
}
if (name === 'versions' && !versionsLoaded) {
versionsLoaded = true;
loadVersions();
}
if (name === 'abtest') loadABTest();
if (name === 'documents') loadDocPanel();
};
function apiFetch(url, options) {
var opts = options || {};
opts.headers = opts.headers || {};
opts.headers['X-CSRF-Token'] = CSRF_TOKEN;
if (opts.body && typeof opts.body === 'object' && !(opts.body instanceof FormData)) {
opts.headers['Content-Type'] = 'application/json';
opts.body = JSON.stringify(opts.body);
}
return fetch(url, opts).then(function(r) {
if (r.status === 204) return null;
return r.json();
});
}
function getIconInfo(type) {
return INTEGRATION_TYPES[type] || { icon: '๐', name: type, color: '#6366f1' };
}
function formatDate(d) {
if (!d) return 'Never';
var dt = new Date(d);
return dt.toLocaleDateString() + ' ' + dt.toLocaleTimeString([], {hour:'2-digit', minute:'2-digit'});
}
function loadAvailableTypes() {
apiFetch('/sites/' + SITE_ID + '/integrations/available')
.then(function(types) {
availableTypes = types || [];
if (availableTypes.length) {
// Merge into defaults
availableTypes.forEach(function(t) {
INTEGRATION_TYPES[t.type] = {
icon: t.icon || '๐',
name: t.name || t.type,
color: '#6366f1'
};
});
}
})
.catch(function() { /* use defaults */ });
}
function loadIntegrations() {
var loading = document.getElementById('int-loading');
var empty = document.getElementById('int-empty');
var grid = document.getElementById('int-grid');
loading.style.display = 'block';
empty.style.display = 'none';
grid.style.display = 'none';
apiFetch('/sites/' + SITE_ID + '/integrations')
.then(function(destinations) {
loading.style.display = 'none';
if (!destinations || destinations.length === 0) {
empty.style.display = 'block';
grid.style.display = 'none';
} else {
empty.style.display = 'none';
grid.style.display = 'grid';
renderIntegrations(destinations);
}
})
.catch(function(err) {
loading.innerHTML = '<p style="color:var(--danger);">Failed to load integrations.</p>';
});
}
function renderIntegrations(destinations) {
var grid = document.getElementById('int-grid');
grid.innerHTML = '';
destinations.forEach(function(dest) {
var info = getIconInfo(dest.type);
var card = document.createElement('div');
card.className = 'int-card';
card.id = 'int-card-' + dest.id;
var statusDot = dest.last_status === 'success' ? 'dot-success' :
dest.last_status === 'failure' ? 'dot-failure' : 'dot-neutral';
card.innerHTML =
'<div class="int-card-header">' +
'<div class="int-card-icon" style="background:' + info.color + '20;">' + info.icon + '</div>' +
'<div>' +
'<div class="int-card-title">' + escHtml(dest.name || info.name) + '</div>' +
'<div class="int-card-type">' + (dest.type || '').replace('_', ' ') + '</div>' +
'</div>' +
'<label class="int-toggle">' +
'<input type="checkbox" ' + (dest.enabled ? 'checked' : '') +
' data-onchange="toggleIntegration(' + dest.id + ', this.checked)">' +
'<span class="slider"></span>' +
'</label>' +
'</div>' +
'<div class="int-card-body">' +
(dest.url ? '<div class="int-url">' + escHtml(dest.url) + '</div>' : '') +
'</div>' +
'<div class="int-status-row">' +
'<span class="int-status-item"><span class="dot dot-success"></span> ' + (dest.success_count || 0) + ' success</span>' +
'<span class="int-status-item"><span class="dot dot-failure"></span> ' + (dest.failure_count || 0) + ' failure</span>' +
'<span class="int-status-item"><span class="dot ' + statusDot + '"></span> Last: ' + formatDate(dest.last_delivered_at) + '</span>' +
'</div>' +
'<div class="int-footer">' +
'<button class="int-btn-sm" data-onclick="testIntegration(' + dest.id + ', this)">๐งช Test</button>' +
'<button class="int-btn-sm danger" data-onclick="deleteIntegration(' + dest.id + ')">๐ Delete</button>' +
'</div>';
grid.appendChild(card);
});
}
// ---- MODAL ----
function openIntModal(dest) {
editingIntId = dest ? dest.id : null;
var modal = document.getElementById('int-modal');
var title = document.getElementById('int-modal-title');
var saveBtn = document.getElementById('int-save-btn');
title.textContent = dest ? 'Edit Integration' : 'Add Integration';
saveBtn.textContent = dest ? 'Save Changes' : 'Add Integration';
// Populate type selector
renderTypeList(dest);
// Pre-fill if editing
if (dest) {
document.getElementById('int-name').value = dest.name || '';
document.getElementById('int-url').value = dest.url || '';
document.getElementById('int-enabled').checked = dest.enabled !== false;
// Pre-fill config fields
if (dest.config) {
var configInputs = document.getElementById('int-config-fields').querySelectorAll('.int-config-input');
configInputs.forEach(function(inp) {
var key = inp.getAttribute('data-config-key');
if (dest.config[key] !== undefined) {
inp.value = dest.config[key];
}
});
}
} else {
document.getElementById('int-name').value = '';
document.getElementById('int-url').value = '';
document.getElementById('int-enabled').checked = true;
}
modal.classList.add('open');
}
function closeIntModal() {
document.getElementById('int-modal').classList.remove('open');
editingIntId = null;
selectedIntType = null;
}
function renderTypeList(dest) {
var list = document.getElementById('int-type-list');
list.innerHTML = '';
var typesToUse = [];
if (availableTypes.length) {
typesToUse = availableTypes;
} else {
// Fallback from defaults
Object.keys(INTEGRATION_TYPES).forEach(function(k) {
typesToUse.push({ type: k, name: INTEGRATION_TYPES[k].name, icon: INTEGRATION_TYPES[k].icon, description: '', config_fields: [], requires_url: true });
});
}
typesToUse.forEach(function(t) {
var info = getIconInfo(t.type);
var opt = document.createElement('div');
opt.className = 'int-type-option' + (dest && dest.type === t.type ? ' selected' : '');
if (!dest && !selectedIntType && !dest) {
// Auto-select first on fresh open
}
opt.setAttribute('data-type', t.type);
opt.innerHTML =
'<span class="type-icon">' + (t.icon || info.icon) + '</span>' +
'<div>' +
'<div class="type-name">' + (t.name || info.name) + '</div>' +
(t.description ? '<div class="type-desc">' + escHtml(t.description) + '</div>' : '') +
'</div>';
opt.addEventListener('click', function() { selectIntType(t.type, t); });
list.appendChild(opt);
// Auto-select if editing or first
if ((dest && dest.type === t.type) || (!dest && !selectedIntType && list.children.length === 1 && !dest)) {
if (dest && dest.type === t.type) {
opt.classList.add('selected');
selectIntType(t.type, t);
}
}
});
// If no dest and not yet selected, auto-pick first
if (!dest && !selectedIntType && list.children.length > 0) {
list.children[0].classList.add('selected');
var firstType = list.children[0].getAttribute('data-type');
var firstData = typesToUse[0];
selectIntType(firstType, firstData);
}
}
function selectIntType(type, typeData) {
selectedIntType = type;
// Update UI selection
var options = document.getElementById('int-type-list').querySelectorAll('.int-type-option');
options.forEach(function(o) {
o.classList.toggle('selected', o.getAttribute('data-type') === type);
});
// Show/hide URL field
var urlGroup = document.getElementById('int-url-group');
urlGroup.style.display = typeData.requires_url !== false ? 'block' : 'none';
// Render config fields
renderConfigFields(typeData);
// Show setup guide
renderSetupGuide(typeData);
}
function renderConfigFields(typeData) {
var container = document.getElementById('int-config-fields');
container.innerHTML = '';
if (!typeData.config_fields || typeData.config_fields.length === 0) return;
typeData.config_fields.forEach(function(cf) {
var group = document.createElement('div');
group.className = 'form-group';
var inputType = cf.type === 'textarea' ? 'textarea' : cf.type || 'text';
var tag = inputType === 'textarea' ? 'textarea' : 'input';
var attrs = 'type="' + cf.type + '" class="int-config-input" data-config-key="' + cf.key + '"';
if (cf.placeholder) attrs += ' placeholder="' + cf.placeholder + '"';
if (cf.required) attrs += ' required';
var closeTag = tag === 'textarea' ? '</textarea>' : '';
group.innerHTML =
'<label>' + escHtml(cf.label) + (cf.required ? ' <span style="color:var(--danger);">*</span>' : '') + '</label>' +
'<' + tag + ' ' + attrs + '></' + tag + '>';
container.appendChild(group);
});
}
function renderSetupGuide(typeData) {
var guideDiv = document.getElementById('int-setup-guide');
if (!typeData || !typeData.setup_guide || !typeData.setup_guide.length) {
guideDiv.style.display = 'none';
guideDiv.innerHTML = '';
return;
}
guideDiv.style.display = 'block';
var info = getIconInfo(selectedIntType || typeData.type);
guideDiv.innerHTML =
'<details class="setup-guide">' +
'<summary>Setup Guide: ' + (typeData.name || info.name) + '</summary>' +
'<div class="guide-content">' +
typeData.setup_guide.map(function(s) {
if (typeof s === 'string') return '<p>' + escHtml(s) + '</p>';
if (s.title) return '<p><strong>' + escHtml(s.title) + '</strong>: ' + escHtml(s.step || '') + '</p>';
return '';
}).join('') +
'</div>' +
'</details>';
}
// ---- CRUD ----
function saveIntegration() {
var name = document.getElementById('int-name').value.trim();
var url = document.getElementById('int-url').value.trim();
var enabled = document.getElementById('int-enabled').checked;
if (!name) { alert('Please enter a name.'); return; }
if (!selectedIntType) { alert('Please select an integration type.'); return; }
// Collect config fields
var config = {};
document.querySelectorAll('.int-config-input').forEach(function(inp) {
var key = inp.getAttribute('data-config-key');
var val = inp.value.trim();
if (val) config[key] = val;
});
var payload = {
type: selectedIntType,
name: name,
url: url,
config: config,
enabled: enabled
};
var saveBtn = document.getElementById('int-save-btn');
saveBtn.disabled = true;
saveBtn.textContent = 'Saving...';
var url_path = '/sites/' + SITE_ID + '/integrations';
var method = 'POST';
if (editingIntId) {
url_path += '/' + editingIntId;
method = 'PUT';
}
apiFetch(url_path, { method: method, body: payload })
.then(function() {
closeIntModal();
loadIntegrations();
})
.catch(function(err) {
alert('Failed to save integration. ' + (err.message || ''));
})
.finally(function() {
saveBtn.disabled = false;
saveBtn.textContent = editingIntId ? 'Save Changes' : 'Add Integration';
});
}
function toggleIntegration(id, enabled) {
apiFetch('/sites/' + SITE_ID + '/integrations/' + id, {
method: 'PUT',
body: { enabled: enabled }
})
.catch(function(err) {
alert('Failed to update integration.');
loadIntegrations(); // refresh to show correct state
});
}
function testIntegration(id, btn) {
btn.disabled = true;
btn.textContent = 'โณ Testing...';
apiFetch('/sites/' + SITE_ID + '/integrations/' + id + '/test', { method: 'POST' })
.then(function(result) {
if (result && result.success) {
btn.textContent = 'โ
Success!';
setTimeout(function() {
btn.textContent = '๐งช Test';
btn.disabled = false;
}, 2000);
loadIntegrations(); // refresh counts
} else {
var msg = (result && result.error) || 'Test failed';
btn.textContent = 'โ Failed';
alert(msg);
setTimeout(function() {
btn.textContent = '๐งช Test';
btn.disabled = false;
}, 3000);
}
})
.catch(function(err) {
btn.textContent = 'โ Error';
alert('Test request failed.');
setTimeout(function() {
btn.textContent = '๐งช Test';
btn.disabled = false;
}, 3000);
});
}
function deleteIntegration(id) {
if (!confirm('Delete this integration? This cannot be undone.')) return;
apiFetch('/sites/' + SITE_ID + '/integrations/' + id, { method: 'DELETE' })
.then(function() {
var card = document.getElementById('int-card-' + id);
if (card) card.remove();
// Check if grid is empty
var grid = document.getElementById('int-grid');
if (!grid.querySelectorAll('.int-card').length) {
document.getElementById('int-empty').style.display = 'block';
grid.style.display = 'none';
}
})
.catch(function(err) {
alert('Failed to delete integration.');
});
}
// Close modal on overlay click
document.getElementById('int-modal').addEventListener('click', function(e) {
if (e.target === this) closeIntModal();
});
// Keyboard shortcuts
document.addEventListener('keydown', function(e) {
// Ctrl+S: Save form fields
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
e.preventDefault();
var editForm = document.getElementById('edit-form');
if (editForm && document.activeElement.closest('#panel-fields')) {
editForm.requestSubmit();
}
}
// Ctrl+N: Add new field (only in fields panel)
if ((e.ctrlKey || e.metaKey) && e.key === 'n') {
e.preventDefault();
if (document.getElementById('panel-fields').classList.contains('active')) {
addField();
}
}
// Escape: Close modal
if (e.key === 'Escape') {
closeIntModal();
}
});
function escHtml(s) {
if (!s) return '';
var div = document.createElement('div');
div.textContent = s;
return div.innerHTML;
}
// ========================
// VERSION HISTORY
// ========================
var versionsLoaded = false;
function loadVersions() {
fetch('/versions/' + SITE_ID + '/history')
.then(function(r) { return r.json(); })
.then(function(data) {
document.getElementById('versions-loading').style.display = 'none';
var versions = data.versions || [];
if (versions.length === 0) {
document.getElementById('versions-empty').style.display = 'block';
return;
}
document.getElementById('versions-empty').style.display = 'none';
var timeline = document.getElementById('versions-timeline');
timeline.style.display = 'flex';
timeline.innerHTML = '';
versions.forEach(function(v) {
timeline.appendChild(createVersionItem(v));
});
})
.catch(function(err) {
document.getElementById('versions-loading').textContent = 'Failed to load version history.';
console.error('Error loading versions:', err);
});
}
function createVersionItem(v) {
var item = document.createElement('div');
item.className = 'version-item';
var changedKeys = v.changes ? Object.keys(v.changes) : [];
var added = 0, removed = 0, modified = 0;
changedKeys.forEach(function(key) {
var ch = v.changes[key];
if (ch.type === 'added') added++;
else if (ch.type === 'removed') removed++;
else modified++;
});
var diffHtml = '';
if (added > 0 || removed > 0 || modified > 0) {
diffHtml = '<div class="version-diff">';
if (added > 0) diffHtml += '<span class="field-added">+' + added + ' added</span> ';
if (removed > 0) diffHtml += '<span class="field-removed">-' + removed + ' removed</span> ';
if (modified > 0) diffHtml += '<span class="field-modified">~' + modified + ' modified</span>';
diffHtml += '</div>';
}
var now = new Date();
var created = new Date(v.created_at);
var timeDiff = Math.floor((now - created) / 1000);
var timeStr;
if (timeDiff < 60) timeStr = 'just now';
else if (timeDiff < 3600) timeStr = Math.floor(timeDiff / 60) + 'm ago';
else if (timeDiff < 86400) timeStr = Math.floor(timeDiff / 3600) + 'h ago';
else timeStr = Math.floor(timeDiff / 86400) + 'd ago';
var isCurrent = v.current ? ' (current)' : '';
item.innerHTML = '<div class="version-dot-col">' +
'<div class="version-dot"></div>' +
'<div class="version-line"></div>' +
'</div>' +
'<div class="version-content">' +
'<div class="version-header">' +
'<div class="version-meta">' +
'<strong>v' + v.version + '</strong> · ' + timeStr + isCurrent +
'</div>' +
'</div>' +
(v.reason ? '<div class="version-reason">' + escHtml(v.reason) + '</div>' : '') +
diffHtml +
'<div class="version-actions">' +
(v.version > 1 ? '<button class="version-btn" data-onclick="compareVersions(' + v.version + ', ' + (v.version - 1) + ')">๐ Compare</button>' : '') +
(!v.current ? '<button class="version-btn danger" data-onclick="rollbackVersion(' + v.version + ')">โฉ Rollback</button>' : '') +
'<button class="version-btn" data-onclick="viewVersionDetail(' + v.version + ')">๐ Details</button>' +
'</div>' +
'</div>';
return item;
}
function createSnapshot() {
var reason = prompt('Snapshot reason (optional):', '');
var btn = document.getElementById('snapshot-btn');
btn.disabled = true;
btn.textContent = 'Saving...';
var body = {};
if (reason) body.reason = reason;
fetch('/versions/' + SITE_ID + '/snapshot', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': CSRF_TOKEN },
body: JSON.stringify(body)
})
.then(function(r) { return r.json(); })
.then(function(data) {
loadVersions();
})
.catch(function(err) {
alert('Failed to create snapshot: ' + (err.message || ''));
})
.finally(function() {
btn.disabled = false;
btn.textContent = '+ Save Snapshot';
});
}
function compareVersions(newer, older) {
fetch('/versions/' + SITE_ID + '/compare/' + newer + '/' + older)
.then(function(r) { return r.json(); })
.then(function(data) {
openDiffModal(data);
})
.catch(function(err) {
alert('Failed to compare versions: ' + (err.message || ''));
});
}
function viewVersionDetail(versionId) {
fetch('/versions/' + SITE_ID + '/versions/' + versionId)
.then(function(r) { return r.json(); })
.then(function(data) {
openDetailModal(data.version);
})
.catch(function(err) {
alert('Failed to load version details: ' + (err.message || ''));
});
}
function rollbackVersion(versionId) {
var confirmMsg = 'Rollback to v' + versionId + '? This will create a new snapshot with the old field configuration.';
if (!confirm(confirmMsg)) return;
fetch('/versions/' + SITE_ID + '/rollback/' + versionId, {
method: 'POST',
headers: { 'X-CSRF-Token': CSRF_TOKEN }
})
.then(function(r) {
if (!r.ok) return r.json().then(function(d) { throw new Error(d.error || 'Rollback failed'); });
return r.json();
})
.then(function(data) {
alert('Rolled back to v' + versionId + '.');
loadVersions();
})
.catch(function(err) {
alert('Rollback failed: ' + (err.message || ''));
});
}
function openDiffModal(data) {
var overlay = document.createElement('div');
overlay.className = 'modal-overlay open';
overlay.innerHTML = '<div class="modal-box" style="max-width:700px;">' +
'<div class="modal-header">' +
'<h3>Compare v' + data.newer + ' vs v' + data.older + '</h3>' +
'<button class="modal-close" data-onclick="this.closest(\'.modal-overlay\').remove()">\u00d7</button>' +
'</div>' +
'<div class="modal-body">' +
'<table class="diff-table">' +
'<thead><tr><th>Field</th><th>Change</th><th>Old Value</th><th>New Value</th></tr></thead>' +
'<tbody>' + renderDiffRows(data.changes) + '</tbody>' +
'</table>' +
'</div>' +
'<div class="modal-footer">' +
'<button class="btn btn-outline" data-onclick="this.closest(\'.modal-overlay\').remove()">Close</button>' +
'</div>' +
'</div>';
overlay.addEventListener('click', function(e) { if (e.target === overlay) overlay.remove(); });
document.body.appendChild(overlay);
}
function renderDiffRow(change, key) {
var badge, rowClass;
if (change.type === 'added') {
badge = '<span class="diff-badge added">Added</span>';
rowClass = 'diff-added';
} else if (change.type === 'removed') {
badge = '<span class="diff-badge removed">Removed</span>';
rowClass = 'diff-removed';
} else {
badge = '<span class="diff-badge modified">Modified</span>';
rowClass = 'diff-modified';
}
return '<tr class="' + rowClass + '">' +
'<td>' + escHtml(key) + '</td>' +
'<td>' + badge + '</td>' +
'<td>' + escHtml(change.old ? JSON.stringify(change.old) : 'โ') + '</td>' +
'<td>' + escHtml(change.new ? JSON.stringify(change.new) : 'โ') + '</td>' +
'</tr>';
}
function renderDiffRows(changes) {
var rows = '';
Object.keys(changes).forEach(function(key) {
rows += renderDiffRow(changes[key], key);
});
return rows || '<tr><td colspan="4" style="text-align:center;color:var(--text-dim);">No differences</td></tr>';
}
function openDetailModal(data) {
var overlay = document.createElement('div');
overlay.className = 'modal-overlay open';
overlay.innerHTML = '<div class="modal-box">' +
'<div class="modal-header">' +
'<h3>Version ' + data.version + ' Details</h3>' +
'<button class="modal-close" data-onclick="this.closest(\'.modal-overlay\').remove()">\u00d7</button>' +
'</div>' +
'<div class="modal-body">' +
'<p><strong>Created:</strong> ' + escHtml(data.created_at) + '</p>' +
'<p><strong>Reason:</strong> ' + escHtml(data.reason || 'โ') + '</p>' +
'<p><strong>Fields:</strong> ' + (data.fields ? data.fields.length : 0) + '</p>' +
(data.fields ? '<div style="margin-top:16px;"><table class="diff-table">' +
'<thead><tr><th>Key</th><th>Label</th><th>Type</th><th>Required</th></tr></thead><tbody>' +
data.fields.map(function(f) {
return '<tr><td>' + escHtml(f.key) + '</td><td>' + escHtml(f.label || '') + '</td><td>' + escHtml(f.type || '') + '</td><td>' + (f.required ? 'Yes' : 'No') + '</td></tr>';
}).join('') +
'</tbody></table></div>' : '') +
'</div>' +
'<div class="modal-footer">' +
'<button class="btn btn-outline" data-onclick="this.closest(\'.modal-overlay\').remove()">Close</button>' +
'</div>' +
'</div>';
overlay.addEventListener('click', function(e) { if (e.target === overlay) overlay.remove(); });
document.body.appendChild(overlay);
}
// โโโ A/B Testing (Phase 10.9) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
var abTestState = { loading: false, variants: null, csrf: 'window.__SITE_DATA__.csrfToken' };
function loadABTest() {
if (abTestState.loading) return;
abTestState.loading = true;
var panel = document.getElementById('panel-abtest');
if (!panel) return;
var xhr = new XMLHttpRequest();
xhr.open('GET', '/sites/' + siteId + '/variants', true);
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
abTestState.loading = false;
if (xhr.status === 200) {
try {
var data = JSON.parse(xhr.responseText);
abTestState.variants = data;
renderABTest(data);
} catch (e) {
console.error('Failed to parse A/B test data:', e);
}
}
}
};
xhr.send();
}
function renderABTest(data) {
var panel = document.getElementById('panel-abtest');
if (!panel) return;
var variants = data.variants || [];
var hasActiveTest = variants.length >= 2;
var stats = data.stats || { a: { impressions: 0, completions: 0, rate: 0 }, b: { impressions: 0, completions: 0, rate: 0 } };
var html = '<div class="card">' +
'<h3>A/B Testing</h3>' +
'<p style="font-size:13px;color:var(--text-muted);margin-bottom:16px;">' +
'Create two variants of your form to test which performs better. ' +
'Traffic is split 50/50 between variants automatically.' +
'</p>';
// Status indicator
if (hasActiveTest) {
html += '<div style="background:#e6f7ee;color:#155724;padding:12px 16px;border-radius:8px;margin-bottom:20px;font-size:14px;">' +
'<strong>โ Active Test</strong> โ Variant A vs Variant B receiving equal traffic' +
'</div>';
} else {
html += '<div style="background:#fff3cd;color:#856404;padding:12px 16px;border-radius:8px;margin-bottom:20px;font-size:14px;">' +
'<strong>No Active Test</strong> โ Create two variants to start A/B testing' +
'</div>';
}
// Stats comparison (if active)
if (hasActiveTest) {
var aStats = stats.a || { impressions: 0, completions: 0, rate: 0 };
var bStats = stats.b || { impressions: 0, completions: 0, rate: 0 };
var winner = aStats.rate > bStats.rate ? 'A' : (bStats.rate > aStats.rate ? 'B' : null);
html += '<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-bottom:24px;">';
['A', 'B'].forEach(function(key) {
var s = key === 'A' ? aStats : bStats;
var isWinner = winner === key;
html += '<div style="padding:16px;background:' + (isWinner ? '#e6f7ee' : 'var(--surface)') + ';border:2px solid ' + (isWinner ? '#22c55e' : 'var(--border)') + ';border-radius:8px;">' +
'<div style="font-size:12px;color:var(--text-muted);margin-bottom:4px;">VARIANT ' + key + (isWinner ? ' <span style="color:#22c55e;font-weight:bold;">โ
LEADING</span>' : '') + '</div>' +
'<div style="font-size:28px;font-weight:bold;color:var(--primary);">' + s.rate.toFixed(1) + '%</div>' +
'<div style="font-size:12px;color:var(--text-muted);">completion rate (' + s.completions + '/' + s.impressions + ')</div>' +
'</div>';
});
html += '</div>';
}
// Variant forms
html += '<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px;">';
['A', 'B'].forEach(function(key) {
var variant = variants.find(function(v) { return v.variant_key === key; });
html += '<div style="border:1px solid var(--border);border-radius:8px;padding:16px;">' +
'<div style="font-weight:bold;margin-bottom:8px;color:' + (variant ? 'var(--primary)' : 'var(--text-muted)') + ';">' +
(variant ? 'Variant ' + key + ': ' + escapeAttr(variant.name || key) : 'Variant ' + key + ' โ Not Set') +
'</div>';
if (variant) {
html += '<div style="font-size:12px;color:var(--text-muted);margin-bottom:12px;">' +
(variant.field_config ? variant.field_config.length : 0) + ' fields' +
'</div>';
html += '<button class="btn btn-sm btn-outline" data-onclick="editVariant(\'' + key + '\')" style="margin-right:8px;">Edit</button>' +
'<button class="btn btn-sm btn-danger" data-onclick="deleteVariant(' + variant.id + ', \'' + key + '\')">Deactivate</button>';
} else {
html += '<button class="btn btn-sm" data-onclick="createVariant(\'' + key + '\')">Create Variant</button>';
}
html += '</div>';
});
html += '</div></div>';
// Variant modal (hidden)
html += '<div id="ab-variant-modal" style="display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);z-index:1000;align-items:center;justify-content:center;">' +
'<div style="background:var(--bg);border-radius:12px;padding:24px;max-width:600px;width:90%;max-height:80vh;overflow-y:auto;">' +
'<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;">' +
'<h3 id="ab-modal-title" style="margin:0;">Create Variant</h3>' +
'<button data-onclick="closeVariantModal()" style="background:none;border:none;font-size:20px;cursor:pointer;color:var(--text-muted);">โ</button>' +
'</div>' +
'<div id="ab-modal-body"></div>' +
'</div>' +
'</div>';
panel.innerHTML = html;
}
function createVariant(key) {
// Get current field config
var fieldConfigEl = document.getElementById('field_config_json');
var fieldConfig = fieldConfigEl ? JSON.parse(fieldConfigEl.value) : getFields();
var name = prompt('Name for Variant ' + key + ' (e.g. "Short Form", "With Phone"):');
if (!name) return;
var modal = document.getElementById('ab-variant-modal');
var body = document.getElementById('ab-modal-body');
document.getElementById('ab-modal-title').textContent = 'Create Variant ' + key;
body.innerHTML = '<p style="font-size:13px;color:var(--text-muted);margin-bottom:16px;">' +
'This variant will show different fields to 50% of visitors. ' +
'Copy your current form and modify it below.' +
'</p>' +
'<div class="field-list" id="variant-field-list">' +
renderVariantFields(fieldConfig, key) +
'</div>' +
'<div style="margin-top:16px;">' +
'<label style="font-size:13px;color:var(--text-muted);">Success Message (optional):</label><br>' +
'<input type="text" id="ab-success-msg" placeholder="Leave empty to use default" style="width:100%;padding:8px 12px;border:1px solid var(--border);border-radius:6px;margin-top:4px;">' +
'</div>' +
'<div style="margin-top:16px;display:flex;gap:8px;">' +
'<button class="btn" data-onclick="saveVariant(\'' + key + '\')">Save & Start Test</button>' +
'<button class="btn btn-outline" data-onclick="closeVariantModal()">Cancel</button>' +
'</div>';
modal.style.display = 'flex';
}
function editVariant(key) {
var variant = abTestState.variants.variants.find(function(v) { return v.variant_key === key; });
if (!variant) return;
createVariant(key);
// Update modal title
document.getElementById('ab-modal-title').textContent = 'Edit Variant ' + key;
// Update success message
var successEl = document.getElementById('ab-success-msg');
if (successEl && variant.success_message) {
successEl.value = variant.success_message;
}
}
function renderVariantFields(fields, key) {
var html = '';
(fields || []).forEach(function(field, idx) {
html += '<div class="field-item" style="margin-bottom:8px;">' +
'<input type="text" class="vf-key" value="' + escapeAttr(field.key || '') + '" placeholder="Key" style="width:80px;">' +
'<input type="text" class="vf-label" value="' + escapeAttr(field.label || '') + '" placeholder="Label" style="flex:1;">' +
'<select class="vf-type" style="width:100px;">' +
['text', 'email', 'phone', 'number', 'textarea', 'select', 'radio', 'checkbox'].map(function(t) {
return '<option value="' + t + '"' + (field.type === t ? ' selected' : '') + '>' + t.charAt(0).toUpperCase() + t.slice(1) + '</option>';
}).join('') +
'</select>' +
'<button data-onclick="this.parentElement.remove()" style="background:none;border:none;cursor:pointer;color:var(--text-muted);">โ</button>' +
'</div>';
});
html += '<button class="btn btn-sm btn-outline" data-onclick="addVariantField()" style="margin-top:8px;">+ Add Field</button>';
return html;
}
function addVariantField() {
var list = document.getElementById('variant-field-list');
var div = document.createElement('div');
div.className = 'field-item';
div.style.marginBottom = '8px';
div.innerHTML = '<input type="text" class="vf-key" placeholder="Key" style="width:80px;">' +
'<input type="text" class="vf-label" placeholder="Label" style="flex:1;">' +
'<select class="vf-type" style="width:100px;">' +
['text', 'email', 'phone', 'number', 'textarea', 'select', 'radio', 'checkbox'].map(function(t) {
return '<option value="' + t + '">' + t.charAt(0).toUpperCase() + t.slice(1) + '</option>';
}).join('') +
'</select>' +
'<button data-onclick="this.parentElement.remove()" style="background:none;border:none;cursor:pointer;color:var(--text-muted);">โ</button>';
list.appendChild(div);
}
function saveVariant(key) {
var items = document.querySelectorAll('#variant-field-list .field-item');
var fields = [];
items.forEach(function(item) {
var k = item.querySelector('.vf-key').value.trim();
var label = item.querySelector('.vf-label').value.trim();
var type = item.querySelector('.vf-type').value;
if (k) {
fields.push({ key: k, label: label || k, type: type, required: false });
}
});
if (fields.length === 0) {
alert('Add at least one field.');
return;
}
var successMsg = document.getElementById('ab-success-msg').value.trim();
var xhr = new XMLHttpRequest();
xhr.open('POST', '/sites/' + siteId + '/variants', true);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.setRequestHeader('X-CSRFToken', abTestState.csrf);
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
if (xhr.status === 201 || xhr.status === 200) {
closeVariantModal();
loadABTest();
} else {
alert('Failed to save variant. Try again.');
}
}
};
xhr.send(JSON.stringify({
site_id: siteId,
variant_key: key,
name: document.getElementById('ab-success-msg').previousElementSibling ? document.getElementById('ab-success-msg').previousElementSibling.previousElementSibling.textContent : 'Variant ' + key,
field_config: fields,
success_message: successMsg || null
}));
}
function deleteVariant(id, key) {
if (!confirm('Deactivate Variant ' + key + '? This will end the A/B test if both variants are active.')) return;
var xhr = new XMLHttpRequest();
xhr.open('DELETE', '/sites/' + siteId + '/variants/' + id, true);
xhr.setRequestHeader('X-CSRFToken', abTestState.csrf);
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
loadABTest();
} else {
alert('Failed to deactivate variant.');
}
}
};
xhr.send();
}
function closeVariantModal() {
var modal = document.getElementById('ab-variant-modal');
if (modal) modal.style.display = 'none';
}
// โโ DOCUMENTS PANEL โโ
var docTemplates = [];
var docDocuments = [];
var docSubmissions = [];
var editingDocTemplateId = null;
var SITE_ID = window.__SITE_DATA__.siteId;
function loadDocPanel() {
loadDocTemplates();
loadDocDocuments();
loadDocSubmissions();
}
function showDocSection(section) {
document.querySelectorAll('.doc-section').forEach(function(el) { el.style.display = 'none'; });
document.querySelectorAll('.doc-subtab').forEach(function(el) { el.classList.remove('active'); });
var target = document.getElementById('doc-' + section);
if (target) target.style.display = 'block';
var tabs = document.querySelectorAll('.doc-subtab');
var sectionMap = { templates: 0, generate: 1, history: 2, schedules: 3, gallery: 4 };
var idx = sectionMap[section];
if (idx !== undefined && tabs[idx]) tabs[idx].classList.add('active');
if (section === 'schedules' && !window._schedulesLoaded) {
window._schedulesLoaded = true;
loadDocSchedules();
}
if (section === 'gallery' && !window._galleryLoaded) {
window._galleryLoaded = true;
loadDocGallery();
}
}
function loadDocTemplates() {
fetch('/sites/' + SITE_ID + '/templates')
.then(function(r) { return r.json(); })
.then(function(data) {
docTemplates = data.templates || [];
renderDocTemplates();
updateDocTemplateSelects();
})
.catch(function(err) { console.error('Failed to load templates:', err); });
}
function renderDocTemplates() {
var grid = document.getElementById('doc-templates-grid');
var empty = document.getElementById('doc-templates-empty');
var loading = document.getElementById('doc-templates-loading');
if (!grid) return;
loading.style.display = 'none';
if (docTemplates.length === 0) {
empty.style.display = 'block';
grid.style.display = 'none';
return;
}
empty.style.display = 'none';
grid.style.display = 'grid';
grid.innerHTML = docTemplates.map(function(t) {
var autoBadge = t.auto_generate ? '<span class="doc-status-badge ready" style="margin-left:8px;">โก auto-gen</span>' : '';
return '<div class="int-card">' +
'<div class="int-card-header">' +
'<div class="int-card-icon" style="background:#f0f0ff;">๐</div>' +
'<div>' +
'<div class="int-card-title">' + escapeHtml(t.name) + autoBadge + '</div>' +
'<div class="int-card-type">' + t.layout + ' layout</div>' +
'</div>' +
'<div class="int-card-actions">' +
'<button class="int-btn-sm" data-onclick="editDocTemplate(' + t.id + ')">โ๏ธ Edit</button>' +
'<button class="int-btn-sm danger" data-onclick="deleteDocTemplate(' + t.id + ')">๐๏ธ</button>' +
'</div>' +
'</div>' +
'<div class="int-card-body">' +
'<div style="font-size:12px;color:var(--text-muted);">From: ' + escapeHtml(t.style.from_name || t.style.from_name || 'โ') + '</div>' +
'<div style="font-size:12px;color:var(--text-dim);">Currency: ' + (t.style.currency || '$') + ' โข Tax: ' + (t.style.tax_rate || 0) + '%</div>' +
'</div>' +
'</div>';
}).join('');
}
function updateDocTemplateSelects() {
var sel = document.getElementById('doc-gen-template');
if (!sel) return;
sel.innerHTML = '<option value="">Select a template...</option>' +
docTemplates.map(function(t) {
return '<option value="' + t.id + '">' + escapeHtml(t.name) + '</option>';
}).join('');
}
function loadDocSubmissions() {
fetch('/sites/' + SITE_ID + '/api/submissions?limit=50')
.then(function(r) { return r.json(); })
.then(function(data) {
docSubmissions = data.submissions || [];
var sel = document.getElementById('doc-gen-submission');
if (!sel) return;
sel.innerHTML = '<option value="">Manual entry...</option>' +
docSubmissions.map(function(s) {
return '<option value="' + s.id + '">' + escapeHtml(s.customer_name || s.email || 'Submission #' + s.id) + ' โ ' + s.submitted_at + '</option>';
}).join('');
})
.catch(function(err) { console.error('Failed to load submissions:', err); });
}
function loadDocDocuments() {
fetch('/sites/' + SITE_ID + '/documents')
.then(function(r) { return r.json(); })
.then(function(data) {
docDocuments = data.documents || [];
renderDocHistory();
})
.catch(function(err) { console.error('Failed to load documents:', err); });
}
function renderDocHistory() {
var list = document.getElementById('doc-history-list');
var empty = document.getElementById('doc-history-empty');
var loading = document.getElementById('doc-history-loading');
var actions = document.getElementById('doc-history-actions');
if (!list) return;
loading.style.display = 'none';
if (docDocuments.length === 0) {
empty.style.display = 'block';
list.style.display = 'none';
if (actions) actions.style.display = 'none';
return;
}
empty.style.display = 'none';
list.style.display = 'block';
if (actions) actions.style.display = 'flex';
var now = new Date();
var statusMap = { draft: 'draft', sent: 'sent', viewed: 'viewed', paid: 'paid', void: 'void' };
var cycleLabels = { draft: 'โ Sent', sent: 'โ Viewed', viewed: 'โ Paid', paid: 'โบ Draft' };
var rows = docDocuments.map(function(d) {
var status = d.status || 'draft';
var statusClass = statusMap[status] || 'draft';
var isOverdue = false;
var overdueBadge = '';
var rowClass = '';
if (d.due_date && status !== 'paid') {
var due = new Date(d.due_date + 'T23:59:59');
if (due < now) {
isOverdue = true;
overdueBadge = '<span class="doc-status-badge overdue" style="margin-left:4px;">OVERDUE</span>';
rowClass = 'doc-row-overdue';
}
}
var dueDateDisplay = d.due_date ? ' | Due: ' + escapeHtml(d.due_date) : '';
var docNum = d.document_number ? escapeHtml(d.document_number) : '# ' + d.id;
return '<tr class="' + rowClass + '">' +
'<td><input type="checkbox" class="doc-checkbox" data-id="' + d.id + '" data-onclick="toggleDocCheckbox(this)"></td>' +
'<td><strong>' + escapeHtml(docNum) + '</strong>' + overdueBadge +
'<div style="font-size:11px;color:var(--text-muted);">' + escapeHtml(d.to_name || d.customer_name || 'โ') + '</div></td>' +
'<td><span class="doc-status-badge ' + statusClass + '">' + status + '</span></td>' +
'<td style="font-size:12px;">' + (d.created_at || 'โ') + dueDateDisplay + '</td>' +
'<td style="white-space:nowrap;">' +
'<button class="int-btn-sm" data-onclick="cycleDocStatus(' + d.id + ')" title="Cycle status">' + (cycleLabels[status] || 'โบ') + '</button> ' +
'<a href="/documents/' + d.document_id + '/preview" class="int-btn-sm" target="_blank" title="Preview">๐</a> ' +
'<a href="/documents/' + d.document_id + '/download" class="int-btn-sm primary" download title="Download">โฌ</a>' +
'</td>' +
'</tr>';
}).join('');
list.innerHTML = '<div class="card">' +
'<table class="history-table">' +
'<thead><tr>' +
'<th><input type="checkbox" id="doc-select-all" data-onchange="toggleAllDocCheckboxes(this)" title="Select all"></th>' +
'<th>Document</th>' +
'<th>Status</th>' +
'<th>Date</th>' +
'<th>Actions</th>' +
'</tr></thead>' +
'<tbody>' + rows + '</tbody>' +
'</table></div>';
}
// โโ 13.3: Status Cycle โโ
function cycleDocStatus(docId) {
fetch('/documents/' + docId + '/status/cycle', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRFToken': getCsrfToken() }
})
.then(function(r) { return r.json(); })
.then(function(data) {
loadDocDocuments();
})
.catch(function(err) { alert('Failed to cycle status: ' + err); });
}
// โโ 13.4: Bulk Generate โโ
function toggleDocCheckbox(el) {
var allCb = document.getElementById('doc-select-all');
if (allCb) {
var allChecked = document.querySelectorAll('.doc-checkbox');
allCb.checked = allChecked.length > 0 && Array.from(allChecked).every(function(cb) { return cb.checked; });
}
}
function toggleAllDocCheckboxes(el) {
document.querySelectorAll('.doc-checkbox').forEach(function(cb) { cb.checked = el.checked; });
}
function bulkGenerateSelected() {
var selected = Array.from(document.querySelectorAll('.doc-checkbox:checked')).map(function(cb) { return parseInt(cb.dataset.id); });
if (selected.length === 0) { alert('Please select at least one document.'); return; }
var progress = document.getElementById('doc-history-progress');
progress.textContent = 'Generating ' + selected.length + ' documents...';
var docIds = selected;
var done = 0;
var success = 0;
var failed = 0;
selected.forEach(function(docId, i) {
// Use the selected documents to generate new versions
fetch('/documents/' + docId + '/status/cycle', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRFToken': getCsrfToken() }
})
.then(function(r) { return r.json(); })
.then(function() {
done++;
success++;
progress.textContent = 'Generated ' + done + '/' + selected.length + ' (' + success + ' ok, ' + failed + ' failed)';
if (done === selected.length) {
setTimeout(function() { progress.textContent = ''; }, 3000);
loadDocDocuments();
}
})
.catch(function() {
done++;
failed++;
progress.textContent = 'Generated ' + done + '/' + selected.length + ' (' + success + ' ok, ' + failed + ' failed)';
if (done === selected.length) {
setTimeout(function() { progress.textContent = ''; }, 3000);
loadDocDocuments();
}
});
});
}
// โโ TEMPLATE MODAL โโ
function openDocTemplateModal(tplId) {
editingDocTemplateId = tplId || null;
document.getElementById('doc-modal-title').textContent = tplId ? 'Edit Template' : 'New Template';
if (tplId) {
var tpl = docTemplates.find(function(t) { return t.id === tplId; });
if (tpl) {
document.getElementById('doc-tpl-name').value = tpl.name || '';
document.getElementById('doc-tpl-layout').value = tpl.layout || 'classic';
document.getElementById('doc-tpl-from-name').value = (tpl.style && tpl.style.from_name) || '';
document.getElementById('doc-tpl-from-email').value = (tpl.style && tpl.style.from_email) || '';
document.getElementById('doc-tpl-from-address').value = (tpl.style && tpl.style.from_address) || '';
document.getElementById('doc-tpl-currency').value = (tpl.style && tpl.style.currency) || '$';
document.getElementById('doc-tpl-tax-rate').value = (tpl.style && tpl.style.tax_rate) || 0;
document.getElementById('doc-tpl-notes').value = (tpl.style && tpl.style.notes) || '';
document.getElementById('doc-tpl-primary').value = (tpl.style && tpl.style.primary_color) || '#2563eb';
document.getElementById('doc-tpl-logo').value = (tpl.style && tpl.style.logo_url) || '';
document.getElementById('doc-tpl-payment-link').value = (tpl.style && tpl.style.payment_link) || '';
document.getElementById('doc-tpl-payment-link-text').value = (tpl.style && tpl.style.payment_link_text) || 'Pay Now';
document.getElementById('doc-tpl-invoice-format').value = tpl.invoice_number_format || 'INV-YYYY-####';
document.getElementById('doc-tpl-next-invoice').value = tpl.next_invoice_number || '';
document.getElementById('doc-tpl-auto-gen').checked = !!tpl.auto_generate;
}
} else {
clearDocTemplateForm();
}
document.getElementById('doc-template-modal').classList.add('open');
}
function closeDocTemplateModal() {
document.getElementById('doc-template-modal').classList.remove('open');
editingDocTemplateId = null;
}
function clearDocTemplateForm() {
document.getElementById('doc-tpl-name').value = '';
document.getElementById('doc-tpl-layout').value = 'classic';
document.getElementById('doc-tpl-from-name').value = '';
document.getElementById('doc-tpl-from-email').value = '';
document.getElementById('doc-tpl-from-address').value = '';
document.getElementById('doc-tpl-currency').value = '$';
document.getElementById('doc-tpl-tax-rate').value = 0;
document.getElementById('doc-tpl-notes').value = '';
document.getElementById('doc-tpl-primary').value = '#2563eb';
document.getElementById('doc-tpl-logo').value = '';
document.getElementById('doc-tpl-payment-link').value = '';
document.getElementById('doc-tpl-payment-link-text').value = 'Pay Now';
document.getElementById('doc-tpl-invoice-format').value = 'INV-YYYY-####';
document.getElementById('doc-tpl-next-invoice').value = '';
}
function saveDocTemplate() {
var name = document.getElementById('doc-tpl-name').value.trim();
if (!name) { alert('Template name is required'); return; }
var style = {
from_name: document.getElementById('doc-tpl-from-name').value.trim(),
from_email: document.getElementById('doc-tpl-from-email').value.trim(),
from_address: document.getElementById('doc-tpl-from-address').value.trim(),
currency: document.getElementById('doc-tpl-currency').value,
tax_rate: parseFloat(document.getElementById('doc-tpl-tax-rate').value) || 0,
notes: document.getElementById('doc-tpl-notes').value.trim(),
primary_color: document.getElementById('doc-tpl-primary').value,
logo_url: document.getElementById('doc-tpl-logo').value.trim(),
payment_link: document.getElementById('doc-tpl-payment-link').value.trim(),
payment_link_text: document.getElementById('doc-tpl-payment-link-text').value.trim(),
invoice_number_format: document.getElementById('doc-tpl-invoice-format').value.trim(),
next_invoice_number: document.getElementById('doc-tpl-next-invoice').value.trim() || null
};
var url = editingDocTemplateId
? '/sites/' + SITE_ID + '/templates/' + editingDocTemplateId
: '/sites/' + SITE_ID + '/templates';
var method = editingDocTemplateId ? 'PUT' : 'POST';
fetch(url, {
method: method,
headers: { 'Content-Type': 'application/json', 'X-CSRFToken': getCsrfToken() },
body: JSON.stringify({
name: name,
layout: document.getElementById('doc-tpl-layout').value,
style: style,
auto_generate: document.getElementById('doc-tpl-auto-gen').checked,
invoice_number_format: style.invoice_number_format,
next_invoice_number: style.next_invoice_number
})
})
.then(function(r) { return r.json(); })
.then(function(data) {
if (data.status === 'ok') {
closeDocTemplateModal();
loadDocTemplates();
} else {
alert('Error: ' + (data.error || 'Unknown error'));
}
})
.catch(function(err) { alert('Request failed: ' + err); });
}
function editDocTemplate(id) {
openDocTemplateModal(id);
}
function deleteDocTemplate(id) {
if (!confirm('Delete this template?')) return;
fetch('/sites/' + SITE_ID + '/templates/' + id, {
method: 'DELETE',
headers: { 'X-CSRFToken': getCsrfToken() }
})
.then(function(r) { return r.json(); })
.then(function(data) {
if (data.status === 'ok') loadDocTemplates();
else alert('Error: ' + (data.error || 'Unknown error'));
})
.catch(function(err) { alert('Request failed: ' + err); });
}
// โโ DOCUMENT GENERATION โโ
function onDocTemplateChange() {
var tplId = document.getElementById('doc-gen-template').value;
var fieldsDiv = document.getElementById('doc-gen-fields');
fieldsDiv.innerHTML = '';
if (!tplId) return;
var tpl = docTemplates.find(function(t) { return t.id === parseInt(tplId); });
if (tpl && tpl.field_mappings) {
fieldsDiv.innerHTML = buildDocFields(tpl.field_mappings);
} else {
fieldsDiv.innerHTML = buildDefaultDocFields();
}
}
function buildDefaultDocFields() {
return '<div class="form-group"><label>To (Customer Name)</label><input type="text" id="doc-to-name" placeholder="Customer Name"></div>' +
'<div class="form-group"><label>To (Email)</label><input type="email" id="doc-to-email" placeholder="customer@example.com"></div>' +
'<div class="form-group"><label>Document Number</label><input type="text" id="doc-number" placeholder="INV-001"></div>' +
'<div class="form-group"><label>Issue Date</label><input type="date" id="doc-issue-date"></div>' +
'<div class="form-group"><label>Due Date</label><input type="date" id="doc-due-date"></div>' +
'<div class="form-group"><label>Line Items (JSON)</label><textarea id="doc-line-items" rows="4" placeholder=\'[{"description":"Service","quantity":1,"unit_price":100}]\'></textarea></div>';
}
function buildDocFields(mappings) {
return buildDefaultDocFields();
}
function onDocSubmissionChange() {
var subId = document.getElementById('doc-gen-submission').value;
if (!subId) return;
var sub = docSubmissions.find(function(s) { return s.id === parseInt(subId); });
if (!sub) return;
if (sub.customer_name) document.getElementById('doc-to-name').value = sub.customer_name;
if (sub.customer_email) document.getElementById('doc-to-email').value = sub.customer_email;
}
function getDocPayload() {
var lineItemsRaw = document.getElementById('doc-line-items').value || '[]';
var lineItems;
try { lineItems = JSON.parse(lineItemsRaw); } catch(e) { lineItems = []; }
return {
to: {
name: document.getElementById('doc-to-name').value.trim(),
email: document.getElementById('doc-to-email').value.trim()
},
document_number: document.getElementById('doc-number').value.trim(),
issue_date: document.getElementById('doc-issue-date').value || null,
due_date: document.getElementById('doc-due-date').value || null,
line_items: lineItems
};
}
function generateDoc() {
var tplId = document.getElementById('doc-gen-template').value;
if (!tplId) { alert('Please select a template'); return; }
var payload = getDocPayload();
if (!payload.to.name) { alert('Customer name is required'); return; }
fetch('/documents/generate', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRFToken': getCsrfToken() },
body: JSON.stringify({
site_id: SITE_ID,
template_id: parseInt(tplId),
document_type: document.getElementById('doc-gen-type').value,
data: payload
})
})
.then(function(r) { return r.json(); })
.then(function(data) {
if (data.status === 'ok') {
window.open('/documents/' + data.document_id + '/download', '_blank');
loadDocDocuments();
} else {
alert('Error: ' + (data.error || 'Generation failed'));
}
})
.catch(function(err) { alert('Request failed: ' + err); });
}
function previewDoc() {
var tplId = document.getElementById('doc-gen-template').value;
if (!tplId) { alert('Please select a template'); return; }
var payload = getDocPayload();
fetch('/sites/' + SITE_ID + '/documents/preview', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRFToken': getCsrfToken() },
body: JSON.stringify({
template_id: parseInt(tplId),
document_type: document.getElementById('doc-gen-type').value,
data: payload
})
})
.then(function(r) { return r.text(); })
.then(function(html) {
var win = window.open('', '_blank');
win.document.write(html);
})
.catch(function(err) { alert('Preview failed: ' + err); });
}
// โโ 13.5: Invoice Schedules โโ
var docSchedules = [];
function loadDocSchedules() {
var loading = document.getElementById('doc-schedules-loading');
if (loading) loading.style.display = 'block';
fetch('/sites/' + SITE_ID + '/invoice-schedules')
.then(function(r) { return r.json(); })
.then(function(data) {
docSchedules = data.schedules || [];
renderDocSchedules();
updateScheduleTemplateSelect();
})
.catch(function(err) {
console.error('Failed to load schedules:', err);
if (loading) loading.style.display = 'none';
});
}
function updateScheduleTemplateSelect() {
var sel = document.getElementById('schedule-template');
if (!sel) return;
sel.innerHTML = '<option value="">Select a template...</option>' +
docTemplates.map(function(t) {
return '<option value="' + t.id + '">' + escapeHtml(t.name) + '</option>';
}).join('');
}
function renderDocSchedules() {
var list = document.getElementById('doc-schedules-list');
var empty = document.getElementById('doc-schedules-empty');
var loading = document.getElementById('doc-schedules-loading');
if (!list) return;
loading.style.display = 'none';
if (docSchedules.length === 0) {
empty.style.display = 'block';
list.style.display = 'none';
return;
}
empty.style.display = 'none';
list.style.display = 'block';
var rows = docSchedules.map(function(s) {
var enabled = !!s.enabled;
return '<tr>' +
'<td><strong>' + escapeHtml(s.customer_name || 'โ') + '</strong>' +
'<div style="font-size:11px;color:var(--text-muted);">' + escapeHtml(s.customer_email || '') + '</div></td>' +
'<td>' + (s.template_name || 'โ') + '</td>' +
'<td>' + escapeHtml(s.interval) + (s.interval_count && s.interval_count > 1 ? ' x' + s.interval_count : '') + '</td>' +
'<td style="font-size:12px;">' + (s.next_run || 'โ') + '</td>' +
'<td style="font-size:12px;">' + (s.last_run || 'Never') + '</td>' +
'<td><label class="toggle-switch"><input type="checkbox" ' + (enabled ? 'checked' : '') + ' data-onchange="toggleScheduleEnabled(' + s.id + ', this.checked)"><span class="toggle-slider"></span></label></td>' +
'<td style="white-space:nowrap;">' +
'<button class="int-btn-sm" data-onclick="editSchedule(' + s.id + ')">โ๏ธ</button> ' +
'<button class="int-btn-sm danger" data-onclick="deleteSchedule(' + s.id + ')">๐๏ธ</button>' +
'</td>' +
'</tr>';
}).join('');
list.innerHTML = '<div class="card">' +
'<table class="schedule-table">' +
'<thead><tr>' +
'<th>Customer</th>' +
'<th>Template</th>' +
'<th>Interval</th>' +
'<th>Next Run</th>' +
'<th>Last Run</th>' +
'<th>Enabled</th>' +
'<th>Actions</th>' +
'</tr></thead>' +
'<tbody>' + rows + '</tbody>' +
'</table></div>';
}
function openScheduleModal(editId) {
document.getElementById('schedule-modal-title').textContent = editId ? 'Edit Schedule' : 'New Schedule';
document.getElementById('schedule-edit-id').value = editId || '';
document.getElementById('schedule-customer-name').value = '';
document.getElementById('schedule-customer-email').value = '';
document.getElementById('schedule-interval').value = 'monthly';
document.getElementById('schedule-interval-count').value = '1';
document.getElementById('schedule-start-date').value = '';
document.getElementById('schedule-doc-prefix').value = 'INV';
document.getElementById('schedule-line-items').value = '';
document.getElementById('schedule-enabled').checked = true;
updateScheduleTemplateSelect();
if (editId) {
var s = docSchedules.find(function(sc) { return sc.id === editId; });
if (s) {
document.getElementById('schedule-template').value = s.template_id || '';
document.getElementById('schedule-customer-name').value = s.customer_name || '';
document.getElementById('schedule-customer-email').value = s.customer_email || '';
document.getElementById('schedule-interval').value = s.interval || 'monthly';
document.getElementById('schedule-interval-count').value = s.interval_count || 1;
document.getElementById('schedule-start-date').value = s.start_date || '';
document.getElementById('schedule-doc-prefix').value = s.document_number_prefix || 'INV';
if (s.line_items && Array.isArray(s.line_items)) {
document.getElementById('schedule-line-items').value = JSON.stringify(s.line_items, null, 2);
}
document.getElementById('schedule-enabled').checked = !!s.enabled;
}
}
document.getElementById('doc-schedule-modal').classList.add('open');
}
function closeScheduleModal() {
document.getElementById('doc-schedule-modal').classList.remove('open');
}
function saveSchedule() {
var editId = document.getElementById('schedule-edit-id').value;
var templateId = document.getElementById('schedule-template').value;
var customerName = document.getElementById('schedule-customer-name').value.trim();
var customerEmail = document.getElementById('schedule-customer-email').value.trim();
var startDate = document.getElementById('schedule-start-date').value;
if (!templateId || !customerName || !customerEmail || !startDate) {
alert('Template, customer name, email, and start date are required.');
return;
}
var lineItemsRaw = document.getElementById('schedule-line-items').value || '[]';
var lineItems;
try { lineItems = JSON.parse(lineItemsRaw); } catch(e) { lineItems = []; }
var payload = {
template_id: parseInt(templateId),
customer_name: customerName,
customer_email: customerEmail,
line_items: lineItems,
document_number_prefix: document.getElementById('schedule-doc-prefix').value.trim() || 'INV',
interval: document.getElementById('schedule-interval').value,
interval_count: parseInt(document.getElementById('schedule-interval-count').value) || 1,
start_date: startDate,
enabled: document.getElementById('schedule-enabled').checked
};
var url = editId
? '/sites/' + SITE_ID + '/invoice-schedules/' + editId
: '/sites/' + SITE_ID + '/invoice-schedules';
var method = editId ? 'PUT' : 'POST';
fetch(url, {
method: method,
headers: { 'Content-Type': 'application/json', 'X-CSRFToken': getCsrfToken() },
body: JSON.stringify(payload)
})
.then(function(r) { return r.json(); })
.then(function(data) {
closeScheduleModal();
loadDocSchedules();
})
.catch(function(err) { alert('Failed to save schedule: ' + err); });
}
function editSchedule(id) {
openScheduleModal(id);
}
function deleteSchedule(id) {
if (!confirm('Delete this schedule?')) return;
fetch('/sites/' + SITE_ID + '/invoice-schedules/' + id, {
method: 'DELETE',
headers: { 'X-CSRFToken': getCsrfToken() }
})
.then(function(r) { return r.json(); })
.then(function() {
loadDocSchedules();
})
.catch(function(err) { alert('Failed to delete schedule: ' + err); });
}
function toggleScheduleEnabled(id, enabled) {
fetch('/sites/' + SITE_ID + '/invoice-schedules/' + id, {
method: 'PUT',
headers: { 'Content-Type': 'application/json', 'X-CSRFToken': getCsrfToken() },
body: JSON.stringify({ enabled: enabled })
})
.then(function() { loadDocSchedules(); })
.catch(function(err) { alert('Failed to update schedule: ' + err); });
}
function runDueSchedules() {
fetch('/sites/' + SITE_ID + '/invoice-schedules/run', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRFToken': getCsrfToken() }
})
.then(function(r) { return r.json(); })
.then(function(data) {
var results = data.results || [];
var ok = results.filter(function(r) { return r.status === 'ok'; }).length;
var err = results.filter(function(r) { return r.error; }).length;
alert('Processed ' + results.length + ' schedules: ' + ok + ' ok, ' + err + ' errors');
loadDocSchedules();
loadDocDocuments();
})
.catch(function(err) { alert('Failed to run schedules: ' + err); });
}
// โโ 13.7: Template Gallery โโ
function loadDocGallery() {
var loading = document.getElementById('doc-gallery-loading');
if (loading) loading.style.display = 'block';
fetch('/documents/gallery')
.then(function(r) { return r.json(); })
.then(function(data) {
renderDocGallery(data.gallery || []);
})
.catch(function(err) {
console.error('Failed to load gallery:', err);
if (loading) loading.style.display = 'none';
var empty = document.getElementById('doc-gallery-empty');
if (empty) empty.style.display = 'block';
});
}
function renderDocGallery(items) {
var grid = document.getElementById('doc-gallery-grid');
var empty = document.getElementById('doc-gallery-empty');
var loading = document.getElementById('doc-gallery-loading');
if (!grid) return;
loading.style.display = 'none';
if (items.length === 0) {
empty.style.display = 'block';
grid.style.display = 'none';
return;
}
empty.style.display = 'none';
grid.style.display = 'grid';
grid.innerHTML = items.map(function(item) {
var layoutIcons = { classic: '๐', modern: 'โจ', minimal: 'โป๏ธ', bold: '๐ช' };
var icon = layoutIcons[item.layout] || '๐';
return '<div class="gallery-card">' +
'<span class="gallery-badge">' + escapeHtml(item.layout) + '</span>' +
'<h4>' + icon + ' ' + escapeHtml(item.name) + '</h4>' +
'<p>' + escapeHtml(item.description || item.preview || '') + '</p>' +
'<button class="btn" data-onclick="useGalleryTemplate(\'' + escapeHtml(item.layout) + '\', \'' + escapeHtml(item.name) + '\')">Use Template</button>' +
'</div>';
}).join('');
}
function useGalleryTemplate(layout, name) {
// Pre-fill the template modal with gallery data
openDocTemplateModal();
document.getElementById('doc-tpl-name').value = name + ' Template';
document.getElementById('doc-tpl-layout').value = layout;
document.getElementById('doc-modal-title').textContent = 'New Template from Gallery';
}
function getCsrfToken() {
var input = document.querySelector('input[name="csrf_token"]');
return input ? input.value : '';
}
function escapeHtml(str) {
if (!str) return '';
return String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
}