Covers & Base Pricing 18
Component Options & Add-Ons 50+
Organization & White-Label
Storefront Routing & Embeds
Dealer Invites & Accounts
Quotes & Customer Leads 0
Labor & Tax Rates
Login & OAuth Setup
Legal & OEM Disclaimers
Changes saved successfully.
Dismiss
Brand
Model Name & Identifier
Style / Category
Wholesale Cost ($)
Markup %
Margin %
MSRP Retail ($)
Gross Profit ($)
Status
Actions
A 6-digit security code has been dispatched to:
Enter the code below to prove ownership and lock in this email change before committing to production.
6-Digit Security Code
Code expires in 10 minutes
Resend Code
Marketing Tagline
Description
Standard Features (1 feature per line)
Apply a standard markup percentage across multiple covers. This will recompute all non-overridden retail prices automatically.
Target Brand
All Brands & Covers
Venturous Only
Ranch Only
Unicover Only
Swiss Only
New Markup %
`;
} else {
embedBox.value = `\nConfigure & Price Your Bed Cap Online `;
}
}
}
function onDeploymentModeChange(val) {
const slug = document.getElementById('settingCompanySlug')?.value || 'truck_guy_upfitters';
updateLiveSlugPreview(slug);
markDirty();
}
function copyEmbedCode() {
const embedBox = document.getElementById('settingEmbedCode');
if (embedBox) {
embedBox.select();
navigator.clipboard.writeText(embedBox.value);
showBanner('Embed code copied to clipboard!', 'success');
}
}
// Tab Controller
function updateLogoPreview(url) {
const container = document.getElementById('logoPreviewContainer');
const img = document.getElementById('logoPreviewImg');
if (url && url.trim()) {
img.src = url;
container.style.display = 'flex';
} else {
container.style.display = 'none';
}
}
async function uploadLogoFile(input) {
if (!input.files || !input.files[0]) return;
const file = input.files[0];
const formData = new FormData();
formData.append('file', file);
showBanner('Uploading logo...', 'info');
try {
const res = await fetch('/api/configurator/admin/upload-logo', {
method: 'POST',
body: formData
});
const data = await res.json();
if (res.ok && data.success) {
document.getElementById('settingLogoUrl').value = data.url;
updateLogoPreview(data.url);
markDirty();
showBanner('Logo uploaded successfully!', 'success');
} else {
showBanner(data.detail || 'Upload failed', 'error');
}
} catch (err) {
showBanner('Upload error: ' + err.message, 'error');
}
}
async function loadAdminQuotes() {
const tbody = document.getElementById('adminQuotesTableBody');
const statusFilter = document.getElementById('quoteStatusFilter')?.value || 'ALL';
try {
let url = '/api/configurator/quotes?limit=100';
if (statusFilter !== 'ALL') url += `&status=${statusFilter}`;
const res = await fetch(url);
if (!res.ok) return;
const quotes = await res.json();
document.getElementById('tabBadgeQuotes').innerText = quotes.length;
if (!quotes.length) {
tbody.innerHTML = 'No quotes found for this filter. ';
return;
}
tbody.innerHTML = '';
quotes.forEach(q => {
const cust = q.customer || {};
const veh = q.vehicle || {};
const build = q.build || {};
const row = document.createElement('tr');
const statusColors = {
draft: '#94a3b8',
quoted: '#f59e0b',
approved: '#10b981',
ordered: '#00E5FF'
};
const color = statusColors[q.status] || '#94a3b8';
row.innerHTML = `
${escapeHtml(q.quote_ref || q.id || 'N/A')}
${escapeHtml(cust.full_name || 'Anonymous Guest')}
${escapeHtml(q.created_at ? new Date(q.created_at).toLocaleDateString() : '')}
${escapeHtml(cust.email || 'No email')}
${escapeHtml(cust.phone || '')}
${escapeHtml(veh.year || '')} ${escapeHtml(veh.make || '')} ${escapeHtml(veh.model || '')}
${escapeHtml(build.model_name || build.model_id || 'Custom Build')}
$${parseFloat(q.pricing?.grand_total || 0).toFixed(2)}
● ${escapeHtml(q.status || 'draft')}
View Build
`;
tbody.appendChild(row);
});
} catch (err) {
console.error('Error loading quotes:', err);
}
}
function switchAdminTab(tabKey) {
document.querySelectorAll('.admin-tab-btn').forEach(btn => btn.classList.remove('active'));
document.querySelectorAll('.admin-tab-content').forEach(content => content.classList.remove('active'));
const targetTab = document.getElementById('tab' + tabKey.charAt(0).toUpperCase() + tabKey.slice(1));
if (targetTab) targetTab.classList.add('active');
const activeBtn = Array.from(document.querySelectorAll('.admin-tab-btn')).find(b => b.getAttribute('onclick') && b.getAttribute('onclick').includes(tabKey));
if (activeBtn) activeBtn.classList.add('active');
if (tabKey === 'options') {
renderOptionsEditor();
}
if (tabKey === 'leads') {
loadAdminQuotes();
}
}
function syncColorInput(inputId, val) {
document.getElementById(inputId).value = val;
markDirty();
}
function syncColorPicker(pickerId, val) {
if (/^#[0-9A-Fa-f]{6}$/.test(val)) {
document.getElementById(pickerId).value = val;
}
markDirty();
}
function onAuthProviderChange(val) {
const gBlock = document.getElementById('googleOauthSettingsBlock');
const pinBlock = document.getElementById('dealerPinSettingsBlock');
if (gBlock) gBlock.style.display = val === 'google' ? 'grid' : 'none';
if (pinBlock) pinBlock.style.display = val === 'dealer_pin' ? 'block' : 'none';
markDirty();
}
// Render Options Catalog Editor
function renderOptionsEditor() {
const container = document.getElementById('optionsCategoriesContainer');
if (!container) return;
container.innerHTML = '';
const optCat = currentCatalog.options_catalog || {};
const catKeys = Object.keys(optCat);
document.getElementById('tabBadgeOptions').innerText = catKeys.reduce((acc, k) => acc + (optCat[k] || []).length, 0);
catKeys.forEach(catKey => {
const items = optCat[catKey] || [];
const card = document.createElement('div');
card.className = 'option-cat-card';
let formattedCatName = catKey.replace(/_/g, ' ').toUpperCase();
card.innerHTML = `
`;
container.appendChild(card);
const list = card.querySelector(`#optList_${catKey}`);
items.forEach((item, idx) => {
const row = document.createElement('div');
row.className = 'option-item-row';
row.innerHTML = `
${item.id}
Active
`;
list.appendChild(row);
});
});
}
function updateOptionItem(catKey, itemIdx, field, val) {
if (currentCatalog.options_catalog && currentCatalog.options_catalog[catKey]) {
currentCatalog.options_catalog[catKey][itemIdx][field] = val;
markDirty();
}
}
// Global Catalog State
let currentCatalog = {
metadata: {},
pricing_rules: {},
models: [],
options_catalog: {}
};
let catalogHistory = [];
let isDirty = false;
let perspectiveMode = 'retail'; // 'retail' or 'mfr'
let verifiedPrimaryEmail = "";
let verifiedRecoveryEmail = "";
let pendingVerification = null; // { email, emailType }
// Math Helpers
function markupToMargin(markup) {
if (markup <= -100) return 0;
return (markup / (100 + markup)) * 100;
}
function marginToMarkup(margin) {
if (margin >= 100) return 999;
return (margin / (100 - margin)) * 100;
}
function calcRetail(cost, markup) {
return cost * (1 + (markup / 100));
}
function calcCost(retail, markup) {
if (markup <= -100) return retail;
return retail / (1 + (markup / 100));
}
// Environment & Portal Detection
const isDealerPortal = window.location.hostname.startsWith('dealer.');
const isAdminPortal = window.location.hostname.startsWith('admin.');
function applyPortalCustomizations() {
if (isDealerPortal) {
const titleH1 = document.getElementById('headerCompanyTitle');
if (titleH1) titleH1.innerText = 'Authorized Dealer Management Console';
// Hide dealer account invitation & corporate oversight tab
const dealersTabBtn = Array.from(document.querySelectorAll('.admin-tab-btn')).find(b => b.getAttribute('onclick') && b.getAttribute('onclick').includes('dealers'));
if (dealersTabBtn) dealersTabBtn.style.display = 'none';
// Update header & badges
const logoBadge = document.querySelector('.brand-logo-badge');
if (logoBadge) logoBadge.innerText = 'AUTHORIZED DEALER PORTAL';
const subtitle = document.querySelector('.header-title-group p');
if (subtitle) subtitle.innerText = 'Dealer Storefront Settings, Markups, Labor Rates & Local Quote Management';
// Disable/Hide wholesale manufacturer perspective switch
const mfrBtn = document.getElementById('btnModeMfr');
if (mfrBtn) mfrBtn.style.display = 'none';
// Hide manufacturer cost inputs in modal & table
const costTh = Array.from(document.querySelectorAll('.data-table thead th')).find(th => th.innerText.toLowerCase().includes('base cost'));
if (costTh) costTh.style.display = 'none';
const addCoverBtn = document.querySelector('.header-actions .btn-blue');
if (addCoverBtn) addCoverBtn.style.display = 'none';
} else {
const logoBadge = document.querySelector('.brand-logo-badge');
if (logoBadge) logoBadge.innerText = 'VENTUROUS EXECUTIVE DESK';
}
}
// Initialize on Load
window.addEventListener('DOMContentLoaded', async () => {
applyPortalCustomizations();
await fetchCatalog();
await fetchHistory();
});
async function fetchCatalog() {
try {
const res = await fetch('/api/configurator/admin/catalog');
if (res.ok) {
currentCatalog = await res.json();
populateSettings();
renderModelsTable();
}
} catch (err) {
showBanner('Error loading catalog: ' + err.message, 'error');
}
}
async function fetchHistory() {
try {
const res = await fetch('/api/configurator/admin/history');
if (res.ok) {
catalogHistory = await res.json();
document.getElementById('historyCountBadge').innerText = catalogHistory.length;
}
} catch (err) {
console.error('History fetch error:', err);
}
}
function populateSettings() {
const meta = currentCatalog.metadata || {};
const pricing = currentCatalog.pricing_rules || {};
const auth = meta.auth_config || {};
if (isAdminPortal || window.location.hostname.includes('admin.')) {
document.getElementById('headerCompanyTitle').innerText = 'Venturous — Bed Cap Master Admin';
} else if (isDealerPortal) {
document.getElementById('headerCompanyTitle').innerText = (meta.company_name || 'Authorized Dealer') + ' — Dealer Management Console';
} else {
document.getElementById('headerCompanyTitle').innerText = (meta.company_name || 'Venturous') + ' — Bed Cap Master Admin';
}
document.getElementById('settingCompanyName').value = meta.company_name || 'Venturous';
document.getElementById('settingCompanySlug').value = meta.company_slug || slugify(meta.company_name || 'truck_guy_upfitters');
document.getElementById('settingDeploymentMode').value = meta.deployment_mode || 'hosted_handle';
document.getElementById('settingParentHost').value = meta.parent_host || 'buildyour.venturous.top';
document.getElementById('settingRetailerDomain').value = meta.custom_retailer_domain || '';
updateLiveSlugPreview(document.getElementById('settingCompanySlug').value);
document.getElementById('settingTagline').value = meta.tagline || 'Authorized Vehicle Upfitting & Overland Platform';
document.getElementById('settingSupportPhone').value = meta.support_phone || '864.79.TRUCK';
document.getElementById('settingSupportPhoneTel').value = meta.support_phone_tel || '18647987825';
document.getElementById('settingSupportEmail').value = meta.support_email || 'support@example.com';
document.getElementById('settingLogoUrl').value = meta.logo_url || '';
updateLogoPreview(meta.logo_url || '');
const pColor = meta.primary_color || '#00E5FF';
const sColor = meta.secondary_color || '#F59E0B';
document.getElementById('settingPrimaryColor').value = pColor;
document.getElementById('settingPrimaryColorPicker').value = pColor;
document.getElementById('settingSecondaryColor').value = sColor;
document.getElementById('settingSecondaryColorPicker').value = sColor;
document.getElementById('settingDefaultBrand').value = meta.default_brand || 'Venturous';
document.getElementById('settingDefaultMarkup').value = meta.default_markup_pct || 40.0;
document.getElementById('settingDefaultMargin').value = (meta.default_margin_pct || markupToMargin(meta.default_markup_pct || 40.0)).toFixed(2);
document.getElementById('settingTaxRate').value = ((meta.tax_rate || 0.06) * 100).toFixed(1);
document.getElementById('settingCurrency').value = meta.currency || 'USD';
document.getElementById('settingLaborBase').value = pricing.labor_rates?.base_installation || 200.0;
document.getElementById('settingLaborKeyless').value = pricing.labor_rates?.keyless_wiring || 120.0;
document.getElementById('settingLaborRack').value = pricing.labor_rates?.rack_installation || 95.0;
// Dual Admin Emails
verifiedPrimaryEmail = (meta.admin_primary_email || auth.admin_primary_email || 'admin@example.com').toLowerCase().trim();
verifiedRecoveryEmail = (meta.admin_recovery_email || auth.admin_recovery_email || 'recovery@example.com').toLowerCase().trim();
document.getElementById('settingAdminPrimaryEmail').value = verifiedPrimaryEmail;
document.getElementById('settingAdminRecoveryEmail').value = verifiedRecoveryEmail;
// Auth Config
const authProvider = auth.provider || (auth.enabled ? 'google' : 'disabled');
document.getElementById('settingAuthProvider').value = authProvider;
document.getElementById('settingAuthAllowGuest').value = auth.allow_guest_submission !== false ? 'true' : 'false';
document.getElementById('settingGoogleClientId').value = auth.google_client_id || '';
document.getElementById('settingGoogleClientSecret').value = auth.google_client_secret || '';
document.getElementById('settingDealerPin').value = auth.dealer_pin || '1234';
onAuthProviderChange(authProvider);
// Legal
document.getElementById('settingLegalDisclaimer').value = meta.legal_disclaimer || 'Ford, Chevrolet, GMC, Ram, Toyota, Nissan, and Jeep are registered trademarks of their respective original equipment manufacturers. Venturous and {company_name} are independent aftermarket upfitters and are not affiliated with, sponsored by, or endorsed by any OEM.';
document.getElementById('footerVersion').innerText = meta.version || '1.0.0';
document.getElementById('tabBadgeCovers').innerText = (currentCatalog.models || []).length;
}
function markDirty() {
isDirty = true;
}
function onGlobalMarkupChange(val) {
const markup = parseFloat(val) || 0;
const margin = markupToMargin(markup);
document.getElementById('settingDefaultMargin').value = margin.toFixed(2);
markDirty();
}
function onGlobalMarginChange(val) {
const margin = parseFloat(val) || 0;
const markup = marginToMarkup(margin);
document.getElementById('settingDefaultMarkup').value = markup.toFixed(2);
markDirty();
}
function setPerspectiveMode(mode) {
perspectiveMode = mode;
document.getElementById('btnModeRetail').classList.toggle('active', mode === 'retail');
document.getElementById('btnModeMfr').classList.toggle('active', mode === 'mfr');
renderModelsTable();
}
function renderModelsTable() {
const tbody = document.getElementById('modelsTableBody');
tbody.innerHTML = '';
const searchQ = document.getElementById('filterSearchInput').value.toLowerCase().trim();
const brandFilter = document.getElementById('filterBrandSelect').value;
const styleFilter = document.getElementById('filterStyleSelect').value;
const statusFilter = document.getElementById('filterStatusSelect').value;
let models = currentCatalog.models || [];
let visibleCount = 0;
models.forEach((m, idx) => {
// Filter checks
if (brandFilter !== 'ALL' && m.brand !== brandFilter) return;
if (styleFilter !== 'ALL' && m.style !== styleFilter) return;
if (statusFilter === 'active' && m.is_active === false) return;
if (statusFilter === 'archived' && m.is_active !== false) return;
if (searchQ) {
const haystack = `${m.name} ${m.id} ${m.brand} ${m.style} ${m.category} ${m.tagline || ''}`.toLowerCase();
if (!haystack.includes(searchQ)) return;
}
visibleCount++;
const cost = parseFloat(m.cost || 0);
const markup = parseFloat(m.markup_pct || 40.0);
const margin = parseFloat(m.margin_pct || markupToMargin(markup));
const retail = parseFloat(m.base_retail_price || 0);
const profit = retail - cost;
const isArchived = m.is_active === false;
const isOverridden = !!m.manual_price_override;
const brandBadgeClass = `badge-brand-${(m.brand || 'custom').toLowerCase()}`;
const tr = document.createElement('tr');
if (isArchived) tr.classList.add('row-archived');
tr.innerHTML = `
${m.brand || 'Venturous'}
${m.name}
${m.id}
${(m.style || 'cab_high').replace(/_/g, ' ')}
${m.category || 'fiberglass'}
${isDealerPortal ? ` ` : `
$
`}
%
%
$
${isOverridden ? `
LOCKED ` : ''}
+$${profit.toFixed(2)}
${isArchived ? 'ARCHIVED' : 'ACTIVE'}
Edit
${isArchived ? 'Restore' : 'Archive'}
`;
tbody.appendChild(tr);
});
document.getElementById('tableRecordCount').innerText = `Showing ${visibleCount} of ${models.length} Covers`;
}
// Table Inline Math Updates
function onTableCostChange(idx, val) {
const cost = parseFloat(val) || 0;
const m = currentCatalog.models[idx];
m.cost = cost;
if (m.manual_price_override && m.base_retail_price > 0 && cost > 0) {
m.markup_pct = ((m.base_retail_price - cost) / cost) * 100;
m.margin_pct = ((m.base_retail_price - cost) / m.base_retail_price) * 100;
} else {
const markup = parseFloat(m.markup_pct || 40.0);
m.base_retail_price = calcRetail(cost, markup);
m.margin_pct = markupToMargin(markup);
}
markDirty();
renderModelsTable();
}
function onTableMarkupChange(idx, val) {
const markup = parseFloat(val) || 0;
const m = currentCatalog.models[idx];
m.markup_pct = markup;
m.margin_pct = markupToMargin(markup);
m.manual_price_override = false;
m.base_retail_price = calcRetail(parseFloat(m.cost || 0), markup);
markDirty();
renderModelsTable();
}
function onTableMarginChange(idx, val) {
const margin = parseFloat(val) || 0;
const markup = marginToMarkup(margin);
const m = currentCatalog.models[idx];
m.margin_pct = margin;
m.markup_pct = markup;
m.manual_price_override = false;
m.base_retail_price = calcRetail(parseFloat(m.cost || 0), markup);
markDirty();
renderModelsTable();
}
function onTableRetailChange(idx, val) {
const retail = parseFloat(val) || 0;
const m = currentCatalog.models[idx];
const cost = parseFloat(m.cost || 0);
m.base_retail_price = retail;
m.manual_price_override = true;
if (cost > 0) {
m.markup_pct = ((retail - cost) / cost) * 100;
m.margin_pct = ((retail - cost) / retail) * 100;
}
markDirty();
renderModelsTable();
}
function unlockModelRetail(idx) {
const m = currentCatalog.models[idx];
m.manual_price_override = false;
const markup = parseFloat(m.markup_pct || 40.0);
m.base_retail_price = calcRetail(parseFloat(m.cost || 0), markup);
markDirty();
renderModelsTable();
showBanner(`Unlocked ${m.name} to auto-calculate MSRP at ${markup.toFixed(1)}% markup.`, 'success');
}
function toggleArchiveModel(idx) {
const m = currentCatalog.models[idx];
m.is_active = (m.is_active === false) ? true : false;
markDirty();
renderModelsTable();
showBanner(`${m.is_active ? 'Restored' : 'Archived'} cover: ${m.name}`, 'success');
}
// Modal Operations: Add / Edit
let activeEditModelIdx = null;
function openAddModelModal() {
activeEditModelIdx = null;
document.getElementById('modalModelTitle').innerText = 'Add New Truck Cap Model';
document.getElementById('editModelName').value = '';
document.getElementById('editModelId').value = '';
document.getElementById('editModelId').disabled = false;
document.getElementById('editModelBrand').value = document.getElementById('settingDefaultBrand').value;
document.getElementById('editModelStyle').value = 'cab_high';
document.getElementById('editModelCategory').value = 'fiberglass';
document.getElementById('editModelWarranty').value = 'Lifetime Structural & Paint Warranty';
document.getElementById('editModelTagline').value = '';
document.getElementById('editModelDesc').value = '';
document.getElementById('editModelFeatures').value = '';
const defMarkup = parseFloat(document.getElementById('settingDefaultMarkup').value) || 40.0;
document.getElementById('editModelCost').value = '2000.00';
document.getElementById('editModelMarkup').value = defMarkup.toFixed(1);
document.getElementById('editModelMargin').value = markupToMargin(defMarkup).toFixed(1);
document.getElementById('editModelRetail').value = calcRetail(2000.0, defMarkup).toFixed(2);
document.getElementById('editModelOverride').checked = false;
updateModalProfitPreview();
document.getElementById('modelEditModal').classList.add('active');
}
function openEditModelModal(idx) {
activeEditModelIdx = idx;
const m = currentCatalog.models[idx];
document.getElementById('modalModelTitle').innerText = `Edit Model: ${m.name}`;
document.getElementById('editModelName').value = m.name || '';
document.getElementById('editModelId').value = m.id || '';
document.getElementById('editModelId').disabled = true; // prevent primary key mutation
document.getElementById('editModelBrand').value = m.brand || 'Venturous';
document.getElementById('editModelStyle').value = m.style || 'cab_high';
document.getElementById('editModelCategory').value = m.category || 'fiberglass';
document.getElementById('editModelWarranty').value = m.warranty || '';
document.getElementById('editModelTagline').value = m.tagline || '';
document.getElementById('editModelDesc').value = m.description || '';
document.getElementById('editModelFeatures').value = (m.standard_features || []).join('\n');
document.getElementById('editModelCost').value = parseFloat(m.cost || 0).toFixed(2);
document.getElementById('editModelMarkup').value = parseFloat(m.markup_pct || 40.0).toFixed(1);
document.getElementById('editModelMargin').value = parseFloat(m.margin_pct || markupToMargin(m.markup_pct || 40.0)).toFixed(1);
document.getElementById('editModelRetail').value = parseFloat(m.base_retail_price || 0).toFixed(2);
document.getElementById('editModelOverride').checked = !!m.manual_price_override;
updateModalProfitPreview();
document.getElementById('modelEditModal').classList.add('active');
}
function closeModelModal() {
document.getElementById('modelEditModal').classList.remove('active');
}
function onModelNameInput(val) {
if (activeEditModelIdx === null) {
const slug = val.toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_+|_+$/g, '');
document.getElementById('editModelId').value = slug;
}
}
function onModalCostInput(val) {
const cost = parseFloat(val) || 0;
const override = document.getElementById('editModelOverride').checked;
if (override) {
const retail = parseFloat(document.getElementById('editModelRetail').value) || 0;
if (cost > 0 && retail > 0) {
document.getElementById('editModelMarkup').value = (((retail - cost) / cost) * 100).toFixed(1);
document.getElementById('editModelMargin').value = (((retail - cost) / retail) * 100).toFixed(1);
}
} else {
const markup = parseFloat(document.getElementById('editModelMarkup').value) || 40.0;
document.getElementById('editModelRetail').value = calcRetail(cost, markup).toFixed(2);
}
updateModalProfitPreview();
}
function onModalMarkupInput(val) {
const markup = parseFloat(val) || 0;
const margin = markupToMargin(markup);
document.getElementById('editModelMargin').value = margin.toFixed(1);
document.getElementById('editModelOverride').checked = false;
const cost = parseFloat(document.getElementById('editModelCost').value) || 0;
document.getElementById('editModelRetail').value = calcRetail(cost, markup).toFixed(2);
updateModalProfitPreview();
}
function onModalMarginInput(val) {
const margin = parseFloat(val) || 0;
const markup = marginToMarkup(margin);
document.getElementById('editModelMarkup').value = markup.toFixed(1);
document.getElementById('editModelOverride').checked = false;
const cost = parseFloat(document.getElementById('editModelCost').value) || 0;
document.getElementById('editModelRetail').value = calcRetail(cost, markup).toFixed(2);
updateModalProfitPreview();
}
function onModalRetailInput(val) {
const retail = parseFloat(val) || 0;
const cost = parseFloat(document.getElementById('editModelCost').value) || 0;
document.getElementById('editModelOverride').checked = true;
if (cost > 0) {
document.getElementById('editModelMarkup').value = (((retail - cost) / cost) * 100).toFixed(1);
document.getElementById('editModelMargin').value = (((retail - cost) / retail) * 100).toFixed(1);
}
updateModalProfitPreview();
}
function updateModalProfitPreview() {
const cost = parseFloat(document.getElementById('editModelCost').value) || 0;
const retail = parseFloat(document.getElementById('editModelRetail').value) || 0;
const profit = retail - cost;
document.getElementById('modalProfitPreview').innerText = `Profit: +$${profit.toFixed(2)}`;
}
function saveModalModel() {
const name = document.getElementById('editModelName').value.trim();
const id = document.getElementById('editModelId').value.trim();
if (!name || !id) {
alert('Please enter a valid Model Name and ID.');
return;
}
const features = document.getElementById('editModelFeatures').value
.split('\n')
.map(s => s.trim())
.filter(Boolean);
const modelData = {
id: id,
name: name,
brand: document.getElementById('editModelBrand').value,
style: document.getElementById('editModelStyle').value,
category: document.getElementById('editModelCategory').value,
cost: parseFloat(document.getElementById('editModelCost').value) || 0,
markup_pct: parseFloat(document.getElementById('editModelMarkup').value) || 40.0,
margin_pct: parseFloat(document.getElementById('editModelMargin').value) || 28.57,
base_retail_price: parseFloat(document.getElementById('editModelRetail').value) || 0,
manual_price_override: document.getElementById('editModelOverride').checked,
tagline: document.getElementById('editModelTagline').value.trim(),
description: document.getElementById('editModelDesc').value.trim(),
warranty: document.getElementById('editModelWarranty').value.trim(),
standard_features: features,
compatible_options: activeEditModelIdx !== null ? (currentCatalog.models[activeEditModelIdx].compatible_options || []) : [],
is_active: activeEditModelIdx !== null ? (currentCatalog.models[activeEditModelIdx].is_active !== false) : true
};
if (activeEditModelIdx !== null) {
currentCatalog.models[activeEditModelIdx] = modelData;
} else {
currentCatalog.models.push(modelData);
}
markDirty();
closeModelModal();
renderModelsTable();
showBanner(`Saved cover model: ${name}`, 'success');
}
// Bulk Markup Modal
function openBulkMarkupModal() {
document.getElementById('bulkMarkupModal').classList.add('active');
}
function closeBulkMarkupModal() {
document.getElementById('bulkMarkupModal').classList.remove('active');
}
function applyBulkMarkup() {
const targetBrand = document.getElementById('bulkBrandTarget').value;
const newMarkup = parseFloat(document.getElementById('bulkMarkupValue').value) || 40.0;
let count = 0;
currentCatalog.models.forEach(m => {
if (targetBrand === 'ALL' || m.brand === targetBrand) {
m.markup_pct = newMarkup;
m.margin_pct = markupToMargin(newMarkup);
m.manual_price_override = false;
m.base_retail_price = calcRetail(parseFloat(m.cost || 0), newMarkup);
count++;
}
});
markDirty();
closeBulkMarkupModal();
renderModelsTable();
showBanner(`Applied ${newMarkup}% markup across ${count} covers.`, 'success');
}
// History Modal
function openHistoryModal() {
const container = document.getElementById('historyListContainer');
container.innerHTML = '';
if (!catalogHistory.length) {
container.innerHTML = 'No historical snapshots recorded yet.
';
} else {
catalogHistory.forEach(entry => {
const div = document.createElement('div');
div.className = 'history-item';
const dt = new Date(entry.timestamp);
const dateStr = dt.toLocaleString();
div.innerHTML = `
${entry.version_id}
${entry.summary || 'Catalog update'}
Author: ${entry.author || 'Admin'} • ${dateStr} • ${entry.active_models_count || entry.models_count} Active Covers
Restore Version
`;
container.appendChild(div);
});
}
document.getElementById('historyModal').classList.add('active');
}
function closeHistoryModal() {
document.getElementById('historyModal').classList.remove('active');
}
async function restoreHistoricalVersion(versionId) {
if (!confirm(`Are you sure you want to restore catalog version ${versionId}? Current unsaved changes will be replaced.`)) {
return;
}
try {
const res = await fetch('/api/configurator/admin/history/restore', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ version_id: versionId, author: 'Admin Web' })
});
const data = await res.json();
if (data.success) {
closeHistoryModal();
await fetchCatalog();
await fetchHistory();
showBanner(`Successfully restored catalog to version ${versionId}`, 'success');
} else {
showBanner('Restore failed: ' + data.detail, 'error');
}
} catch (err) {
showBanner('Restore error: ' + err.message, 'error');
}
}
// Save All Changes
function openEmailVerifyModal(email, emailType) {
pendingVerification = { email, emailType };
document.getElementById('emailVerifyModalTitle').innerText = `Verify ${emailType === 'primary' ? 'Primary Admin' : 'Recovery'} Email`;
document.getElementById('emailVerifyTargetDisplay').innerText = email;
document.getElementById('emailVerifyCodeInput').value = '';
document.getElementById('emailVerifyError').style.display = 'none';
document.getElementById('emailVerifyModal').classList.add('active');
document.getElementById('emailVerifyCodeInput').focus();
}
function closeEmailVerifyModal() {
document.getElementById('emailVerifyModal').classList.remove('active');
}
async function resendVerificationCode() {
if (!pendingVerification) return;
try {
const res = await fetch('/api/configurator/admin/email/send-code', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: pendingVerification.email, email_type: pendingVerification.emailType })
});
const data = await res.json();
if (data.success) {
showBanner(data.message, 'success');
} else {
document.getElementById('emailVerifyError').innerText = data.detail || 'Failed to resend code';
document.getElementById('emailVerifyError').style.display = 'block';
}
} catch (err) {
document.getElementById('emailVerifyError').innerText = 'Network error: ' + err.message;
document.getElementById('emailVerifyError').style.display = 'block';
}
}
async function confirmEmailVerificationCode() {
if (!pendingVerification) return;
const code = document.getElementById('emailVerifyCodeInput').value.trim();
if (!code || code.length !== 6) {
document.getElementById('emailVerifyError').innerText = 'Please enter a valid 6-digit verification code.';
document.getElementById('emailVerifyError').style.display = 'block';
return;
}
try {
const res = await fetch('/api/configurator/admin/email/verify-code', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: pendingVerification.email, code: code, email_type: pendingVerification.emailType })
});
const data = await res.json();
if (data.success && data.verified) {
if (pendingVerification.emailType === 'primary') {
verifiedPrimaryEmail = pendingVerification.email;
} else if (pendingVerification.emailType === 'recovery') {
verifiedRecoveryEmail = pendingVerification.email;
}
closeEmailVerifyModal();
showBanner(`Email verified successfully! Resuming save...`, 'success');
setTimeout(saveAllCatalogChanges, 300);
} else {
document.getElementById('emailVerifyError').innerText = data.detail || 'Invalid verification code.';
document.getElementById('emailVerifyError').style.display = 'block';
}
} catch (err) {
document.getElementById('emailVerifyError').innerText = 'Verification error: ' + err.message;
document.getElementById('emailVerifyError').style.display = 'block';
}
}
async function saveAllCatalogChanges() {
// Validate mandatory dual admin emails before proceeding
const pEmail = (document.getElementById('settingAdminPrimaryEmail')?.value || '').trim().toLowerCase();
const rEmail = (document.getElementById('settingAdminRecoveryEmail')?.value || '').trim().toLowerCase();
const emailErrEl = document.getElementById('adminEmailError');
if (!pEmail || !rEmail || pEmail === rEmail) {
if (emailErrEl) emailErrEl.style.display = 'block';
switchAdminTab('auth');
showBanner('Error: You must provide TWO DIFFERENT email addresses (Primary Admin & Secondary Recovery) to prevent instance lockout.', 'error');
return;
}
if (emailErrEl) emailErrEl.style.display = 'none';
// Check if Primary Email changed and needs OTP verification
if (verifiedPrimaryEmail && pEmail !== verifiedPrimaryEmail) {
switchAdminTab('auth');
showBanner(`Security Verification Required: A confirmation code is being sent to ${pEmail}`, 'info');
try {
const sRes = await fetch('/api/configurator/admin/email/send-code', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: pEmail, email_type: 'primary' })
});
const sData = await sRes.json();
if (sRes.ok) {
openEmailVerifyModal(pEmail, 'primary');
} else {
showBanner(sData.detail || 'Failed to dispatch verification code.', 'error');
}
} catch (err) {
showBanner('Error dispatching verification code: ' + err.message, 'error');
}
return;
}
// Check if Recovery Email changed and needs OTP verification
if (verifiedRecoveryEmail && rEmail !== verifiedRecoveryEmail) {
switchAdminTab('auth');
showBanner(`Security Verification Required: A confirmation code is being sent to ${rEmail}`, 'info');
try {
const sRes = await fetch('/api/configurator/admin/email/send-code', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: rEmail, email_type: 'recovery' })
});
const sData = await sRes.json();
if (sRes.ok) {
openEmailVerifyModal(rEmail, 'recovery');
} else {
showBanner(sData.detail || 'Failed to dispatch verification code.', 'error');
}
} catch (err) {
showBanner('Error dispatching verification code: ' + err.message, 'error');
}
return;
}
// Sync global organization & white-label settings to metadata
currentCatalog.metadata.company_name = document.getElementById('settingCompanyName').value;
currentCatalog.metadata.company_slug = slugify(document.getElementById('settingCompanySlug').value || document.getElementById('settingCompanyName').value);
currentCatalog.metadata.deployment_mode = document.getElementById('settingDeploymentMode').value;
currentCatalog.metadata.parent_host = document.getElementById('settingParentHost').value;
currentCatalog.metadata.custom_retailer_domain = document.getElementById('settingRetailerDomain').value;
currentCatalog.metadata.tagline = document.getElementById('settingTagline').value;
currentCatalog.metadata.support_phone = document.getElementById('settingSupportPhone').value;
currentCatalog.metadata.support_phone_tel = document.getElementById('settingSupportPhoneTel').value;
currentCatalog.metadata.support_email = document.getElementById('settingSupportEmail').value;
currentCatalog.metadata.logo_url = document.getElementById('settingLogoUrl').value;
currentCatalog.metadata.primary_color = document.getElementById('settingPrimaryColor').value;
currentCatalog.metadata.secondary_color = document.getElementById('settingSecondaryColor').value;
currentCatalog.metadata.default_brand = document.getElementById('settingDefaultBrand').value;
currentCatalog.metadata.default_markup_pct = parseFloat(document.getElementById('settingDefaultMarkup').value) || 40.0;
currentCatalog.metadata.default_margin_pct = parseFloat(document.getElementById('settingDefaultMargin').value) || 28.57;
currentCatalog.metadata.tax_rate = (parseFloat(document.getElementById('settingTaxRate').value) || 6.0) / 100.0;
currentCatalog.metadata.currency = document.getElementById('settingCurrency').value;
currentCatalog.metadata.legal_disclaimer = document.getElementById('settingLegalDisclaimer').value;
currentCatalog.metadata.admin_primary_email = pEmail;
currentCatalog.metadata.admin_recovery_email = rEmail;
// Auth Config
const authProvider = document.getElementById('settingAuthProvider').value;
currentCatalog.metadata.auth_config = {
enabled: authProvider !== 'disabled',
provider: authProvider,
admin_primary_email: pEmail,
admin_recovery_email: rEmail,
google_client_id: document.getElementById('settingGoogleClientId').value,
google_client_secret: document.getElementById('settingGoogleClientSecret').value,
dealer_pin: document.getElementById('settingDealerPin').value,
allow_guest_submission: document.getElementById('settingAuthAllowGuest').value === 'true'
};
if (!currentCatalog.pricing_rules) currentCatalog.pricing_rules = {};
if (!currentCatalog.pricing_rules.labor_rates) currentCatalog.pricing_rules.labor_rates = {};
currentCatalog.pricing_rules.labor_rates.base_installation = parseFloat(document.getElementById('settingLaborBase').value) || 200.0;
currentCatalog.pricing_rules.labor_rates.keyless_wiring = parseFloat(document.getElementById('settingLaborKeyless').value) || 120.0;
currentCatalog.pricing_rules.labor_rates.rack_installation = parseFloat(document.getElementById('settingLaborRack').value) || 95.0;
try {
const res = await fetch('/api/configurator/admin/catalog', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
metadata: currentCatalog.metadata,
pricing_rules: currentCatalog.pricing_rules,
models: currentCatalog.models,
options_catalog: currentCatalog.options_catalog,
author: 'Admin Portal',
change_summary: `Saved configuration across ${currentCatalog.models.length} covers`
})
});
const data = await res.json();
if (data.success) {
isDirty = false;
await fetchCatalog();
await fetchHistory();
showBanner(`All changes successfully saved. Version: ${data.version_id}`, 'success');
} else {
showBanner('Failed to save catalog: ' + data.detail, 'error');
}
} catch (err) {
showBanner('Network error saving catalog: ' + err.message, 'error');
}
}
function showBanner(text, type = 'success') {
const b = document.getElementById('statusBanner');
b.className = type;
document.getElementById('statusBannerText').innerText = text;
b.style.display = 'flex';
setTimeout(() => { b.style.display = 'none'; }, 6000);
}
function hideBanner() {
document.getElementById('statusBanner').style.display = 'none';
}