Sbui503's picture
https://www.uprinting.com With admin panel to change pricing quote
7774eb1 verified
Raw
History Blame
38.2 kB
// =============================================
// PrintPro - Admin Panel Script
// =============================================
// --- DEFAULT PRICING (same as frontend) ---
const DEFAULT_PRICING = {
products: {
'business-cards': {
name: 'Business Cards',
icon: 'credit-card',
image: 'http://static.photos/office/320x240/42',
basePrice: 19.00,
sizes: [
{ id: '3.5x2', name: '3.5" x 2"', price: 0 },
{ id: '3.5x2-rounded', name: '3.5" x 2" Rounded', price: 4 },
{ id: '3.5x2-square', name: '3.5" x 3.5" Square', price: 6 }
],
paperTypes: [
{ id: '14pt-gloss', name: '14pt Gloss', price: 0 },
{ id: '16pt-matte', name: '16pt Matte', price: 6 },
{ id: '14pt-uncoated', name: '14pt Uncoated', price: 3 },
{ id: '16pt-silk', name: '16pt Silk', price: 8 }
],
finishing: [
{ id: 'none', name: 'None', price: 0 },
{ id: 'matte', name: 'Matte Coating', price: 5 },
{ id: 'gloss', name: 'Gloss Coating', price: 5 },
{ id: 'uv', name: 'UV Spot Gloss', price: 12 },
{ id: 'foil', name: 'Foil Stamp', price: 18 }
],
quantityTiers: [
{ min: 50, max: 250, multiplier: 1.0, label: '50-250' },
{ min: 251, max: 500, multiplier: 0.75, label: '251-500' },
{ min: 501, max: 1000, multiplier: 0.55, label: '501-1K' },
{ min: 1001, max: 2500, multiplier: 0.40, label: '1K-2.5K' },
{ min: 2501, max: 5000, multiplier: 0.30, label: '2.5K-5K' },
{ min: 5001, max: 10000, multiplier: 0.22, label: '5K-10K' }
],
turnaround: [
{ id: 'standard', name: 'Standard (5-7 days)', price: 0 },
{ id: 'rush', name: 'Rush (3-4 days)', price: 15 },
{ id: 'express', name: 'Express (1-2 days)', price: 30 }
],
defaultQuantity: 500
},
'flyers': {
name: 'Flyers',
icon: 'file-text',
image: 'http://static.photos/office/320x240/55',
basePrice: 35.00,
sizes: [
{ id: '8.5x11', name: '8.5" x 11"', price: 0 },
{ id: '5.5x8.5', name: '5.5" x 8.5"', price: -3 },
{ id: '11x17', name: '11" x 17"', price: 8 }
],
paperTypes: [
{ id: '100lb-gloss', name: '100lb Gloss', price: 0 },
{ id: '80lb-matte', name: '80lb Matte', price: -2 },
{ id: '100lb-uncoated', name: '100lb Uncoated', price: 2 }
],
finishing: [
{ id: 'none', name: 'None', price: 0 },
{ id: 'matte', name: 'Matte Coating', price: 8 },
{ id: 'gloss', name: 'Gloss Coating', price: 8 }
],
quantityTiers: [
{ min: 50, max: 250, multiplier: 1.0, label: '50-250' },
{ min: 251, max: 500, multiplier: 0.70, label: '251-500' },
{ min: 501, max: 1000, multiplier: 0.50, label: '501-1K' },
{ min: 1001, max: 5000, multiplier: 0.35, label: '1K-5K' },
{ min: 5001, max: 10000, multiplier: 0.25, label: '5K-10K' }
],
turnaround: [
{ id: 'standard', name: 'Standard (5-7 days)', price: 0 },
{ id: 'rush', name: 'Rush (3-4 days)', price: 20 },
{ id: 'express', name: 'Express (1-2 days)', price: 40 }
],
defaultQuantity: 250
},
'brochures': {
name: 'Brochures',
icon: 'book-open',
image: 'http://static.photos/office/320x240/67',
basePrice: 55.00,
sizes: [
{ id: '8.5x11-bi', name: '8.5"x11" Bi-Fold', price: 0 },
{ id: '8.5x11-tri', name: '8.5"x11" Tri-Fold', price: 3 },
{ id: '11x17-bi', name: '11"x17" Bi-Fold', price: 10 }
],
paperTypes: [
{ id: '100lb-gloss', name: '100lb Gloss', price: 0 },
{ id: '80lb-matte', name: '80lb Matte', price: -3 },
{ id: '100lb-silk', name: '100lb Silk', price: 5 }
],
finishing: [
{ id: 'none', name: 'None', price: 0 },
{ id: 'matte', name: 'Matte Coating', price: 10 },
{ id: 'gloss', name: 'Gloss Coating', price: 10 }
],
quantityTiers: [
{ min: 25, max: 100, multiplier: 1.0, label: '25-100' },
{ min: 101, max: 250, multiplier: 0.70, label: '101-250' },
{ min: 251, max: 500, multiplier: 0.52, label: '251-500' },
{ min: 501, max: 1000, multiplier: 0.40, label: '501-1K' },
{ min: 1001, max: 5000, multiplier: 0.30, label: '1K-5K' }
],
turnaround: [
{ id: 'standard', name: 'Standard (5-7 days)', price: 0 },
{ id: 'rush', name: 'Rush (3-4 days)', price: 25 },
{ id: 'express', name: 'Express (1-2 days)', price: 50 }
],
defaultQuantity: 100
},
'banners': {
name: 'Banners',
icon: 'flag',
image: 'http://static.photos/office/320x240/78',
basePrice: 45.00,
sizes: [
{ id: '24x36', name: '24" x 36"', price: 0 },
{ id: '36x48', name: '36" x 48"', price: 15 },
{ id: '48x72', name: '48" x 72"', price: 35 }
],
paperTypes: [
{ id: 'vinyl', name: '13oz Vinyl', price: 0 },
{ id: 'mesh', name: 'Mesh Vinyl', price: 8 },
{ id: 'fabric', name: 'Fabric', price: 20 }
],
finishing: [
{ id: 'none', name: 'None', price: 0 },
{ id: 'grommets', name: 'Grommets', price: 5 },
{ id: 'pole-pockets', name: 'Pole Pockets', price: 10 }
],
quantityTiers: [
{ min: 1, max: 5, multiplier: 1.0, label: '1-5' },
{ min: 6, max: 10, multiplier: 0.80, label: '6-10' },
{ min: 11, max: 25, multiplier: 0.65, label: '11-25' },
{ min: 26, max: 50, multiplier: 0.50, label: '26-50' }
],
turnaround: [
{ id: 'standard', name: 'Standard (5-7 days)', price: 0 },
{ id: 'rush', name: 'Rush (3-4 days)', price: 20 },
{ id: 'express', name: 'Express (1-2 days)', price: 45 }
],
defaultQuantity: 1
},
'stickers': {
name: 'Stickers & Labels',
icon: 'sticky-note',
image: 'http://static.photos/office/320x240/91',
basePrice: 25.00,
sizes: [
{ id: '2x2', name: '2" x 2"', price: 0 },
{ id: '3x3', name: '3" x 3"', price: 5 },
{ id: '4x4', name: '4" x 4"', price: 8 },
{ id: 'custom-die', name: 'Custom Die-Cut', price: 15 }
],
paperTypes: [
{ id: 'white-vinyl', name: 'White Vinyl', price: 0 },
{ id: 'clear-vinyl', name: 'Clear Vinyl', price: 5 },
{ id: 'metallic', name: 'Metallic', price: 10 }
],
finishing: [
{ id: 'none', name: 'None', price: 0 },
{ id: 'matte-lam', name: 'Matte Laminate', price: 6 },
{ id: 'gloss-lam', name: 'Gloss Laminate', price: 6 }
],
quantityTiers: [
{ min: 50, max: 250, multiplier: 1.0, label: '50-250' },
{ min: 251, max: 500, multiplier: 0.70, label: '251-500' },
{ min: 501, max: 1000, multiplier: 0.48, label: '501-1K' },
{ min: 1001, max: 2500, multiplier: 0.35, label: '1K-2.5K' },
{ min: 2501, max: 5000, multiplier: 0.25, label: '2.5K-5K' }
],
turnaround: [
{ id: 'standard', name: 'Standard (5-7 days)', price: 0 },
{ id: 'rush', name: 'Rush (3-4 days)', price: 12 },
{ id: 'express', name: 'Express (1-2 days)', price: 28 }
],
defaultQuantity: 250
},
'postcards': {
name: 'Postcards',
icon: 'mail',
image: 'http://static.photos/office/320x240/33',
basePrice: 28.00,
sizes: [
{ id: '4x6', name: '4" x 6"', price: 0 },
{ id: '5x7', name: '5" x 7"', price: 4 },
{ id: '6x9', name: '6" x 9"', price: 7 },
{ id: '6x11', name: '6" x 11"', price: 10 }
],
paperTypes: [
{ id: '14pt-gloss', name: '14pt Gloss', price: 0 },
{ id: '16pt-matte', name: '16pt Matte', price: 5 },
{ id: '14pt-uncoated', name: '14pt Uncoated', price: 2 }
],
finishing: [
{ id: 'none', name: 'None', price: 0 },
{ id: 'matte', name: 'Matte Coating', price: 5 },
{ id: 'gloss', name: 'Gloss Coating', price: 5 },
{ id: 'uv-front', name: 'UV Front Only', price: 8 }
],
quantityTiers: [
{ min: 100, max: 500, multiplier: 1.0, label: '100-500' },
{ min: 501, max: 1000, multiplier: 0.72, label: '501-1K' },
{ min: 1001, max: 2500, multiplier: 0.52, label: '1K-2.5K' },
{ min: 2501, max: 5000, multiplier: 0.38, label: '2.5K-5K' },
{ min: 5001, max: 10000, multiplier: 0.28, label: '5K-10K' }
],
turnaround: [
{ id: 'standard', name: 'Standard (5-7 days)', price: 0 },
{ id: 'rush', name: 'Rush (3-4 days)', price: 12 },
{ id: 'express', name: 'Express (1-2 days)', price: 28 }
],
defaultQuantity: 500
},
'letterhead': {
name: 'Letterhead',
icon: 'file',
image: 'http://static.photos/office/320x240/44',
basePrice: 40.00,
sizes: [
{ id: '8.5x11', name: '8.5" x 11"', price: 0 },
{ id: '8.5x14', name: '8.5" x 14"', price: 5 }
],
paperTypes: [
{ id: '24lb-bond', name: '24lb Bond', price: 0 },
{ id: '28lb-bond', name: '28lb Bond', price: 5 },
{ id: '32lb-bond', name: '32lb Bond', price: 10 },
{ id: 'linen', name: 'Linen Finish', price: 15 }
],
finishing: [
{ id: 'none', name: 'None', price: 0 },
{ id: 'watermark', name: 'Watermark', price: 8 }
],
quantityTiers: [
{ min: 100, max: 500, multiplier: 1.0, label: '100-500' },
{ min: 501, max: 1000, multiplier: 0.70, label: '501-1K' },
{ min: 1001, max: 2500, multiplier: 0.48, label: '1K-2.5K' },
{ min: 2501, max: 5000, multiplier: 0.35, label: '2.5K-5K' }
],
turnaround: [
{ id: 'standard', name: 'Standard (5-7 days)', price: 0 },
{ id: 'rush', name: 'Rush (3-4 days)', price: 18 },
{ id: 'express', name: 'Express (1-2 days)', price: 38 }
],
defaultQuantity: 500
},
'envelopes': {
name: 'Envelopes',
icon: 'mail-open',
image: 'http://static.photos/office/320x240/56',
basePrice: 32.00,
sizes: [
{ id: '10-standard', name: '#10 Standard', price: 0 },
{ id: 'a7', name: 'A7 (5.25x7.25)', price: 3 },
{ id: 'a2', name: 'A2 (4.375x5.75)', price: 0 }
],
paperTypes: [
{ id: '24lb-white', name: '24lb White Wove', price: 0 },
{ id: '28lb-white', name: '28lb White Wove', price: 4 },
{ id: 'kraft', name: 'Kraft Brown', price: 3 }
],
finishing: [
{ id: 'none', name: 'None', price: 0 },
{ id: 'window', name: 'Window', price: 5 },
{ id: 'peel-seal', name: 'Peel & Seal', price: 4 }
],
quantityTiers: [
{ min: 100, max: 500, multiplier: 1.0, label: '100-500' },
{ min: 501, max: 1000, multiplier: 0.72, label: '501-1K' },
{ min: 1001, max: 2500, multiplier: 0.52, label: '1K-2.5K' },
{ min: 2501, max: 5000, multiplier: 0.36, label: '2.5K-5K' }
],
turnaround: [
{ id: 'standard', name: 'Standard (5-7 days)', price: 0 },
{ id: 'rush', name: 'Rush (3-4 days)', price: 15 },
{ id: 'express', name: 'Express (1-2 days)', price: 32 }
],
defaultQuantity: 250
}
}
};
// --- STATE ---
let pricing = null;
let editingProductKey = null;
let editingData = null;
// --- AUTH ---
const ADMIN_CREDENTIALS = { username: 'admin', password: 'admin123' };
let isLoggedIn = sessionStorage.getItem('printpro_admin_loggedin') === 'true';
// --- DOM ---
const loginScreen = document.getElementById('loginScreen');
const dashboard = document.getElementById('dashboard');
const loginForm = document.getElementById('loginForm');
const loginError = document.getElementById('loginError');
const editModal = document.getElementById('editModal');
const modalProductName = document.getElementById('modalProductName');
const modalContent = document.getElementById('modalContent');
const productCards = document.getElementById('productCards');
const toast = document.getElementById('toast');
const toastMsg = document.getElementById('toastMsg');
// --- INIT ---
function getPricing() {
const stored = localStorage.getItem('printpro_pricing');
if (stored) {
try { return JSON.parse(stored); } catch (e) { return JSON.parse(JSON.stringify(DEFAULT_PRICING)); }
}
const def = JSON.parse(JSON.stringify(DEFAULT_PRICING));
localStorage.setItem('printpro_pricing', JSON.stringify(def));
return def;
}
function savePricingToStorage(data) {
localStorage.setItem('printpro_pricing', JSON.stringify(data));
// Dispatch events to notify the frontend
window.dispatchEvent(new Event('pricingUpdated'));
// Also try to notify other tabs via storage event
localStorage.setItem('printpro_pricing_updated', Date.now().toString());
updateLastSaved();
}
function updateLastSaved() {
const now = new Date();
const timeStr = now.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
document.getElementById('statLastSaved').textContent = timeStr;
localStorage.setItem('printpro_last_saved', timeStr);
}
// --- LOGIN ---
loginForm.addEventListener('submit', (e) => {
e.preventDefault();
const username = document.getElementById('username').value.trim();
const password = document.getElementById('password').value.trim();
if (username === ADMIN_CREDENTIALS.username && password === ADMIN_CREDENTIALS.password) {
isLoggedIn = true;
sessionStorage.setItem('printpro_admin_loggedin', 'true');
loginScreen.classList.add('hidden');
dashboard.classList.remove('hidden');
initDashboard();
showToast('Welcome back, Admin! 👋');
} else {
loginError.textContent = 'Invalid username or password.';
loginError.classList.remove('hidden');
document.getElementById('password').classList.add('border-red-500');
setTimeout(() => {
document.getElementById('password').classList.remove('border-red-500');
loginError.classList.add('hidden');
}, 2500);
}
});
function logout() {
isLoggedIn = false;
sessionStorage.removeItem('printpro_admin_loggedin');
dashboard.classList.add('hidden');
loginScreen.classList.remove('hidden');
document.getElementById('password').value = '';
loginError.classList.add('hidden');
}
// --- TOAST ---
function showToast(message) {
toastMsg.textContent = message;
toast.classList.remove('hidden');
void toast.offsetWidth;
toast.classList.add('toast');
setTimeout(() => {
toast.classList.add('hidden');
toast.classList.remove('toast');
}, 3000);
}
// --- DASHBOARD ---
function initDashboard() {
pricing = getPricing();
updateStats();
renderProductCards();
lucide.createIcons();
}
function updateStats() {
const products = pricing.products;
const keys = Object.keys(products);
const totalBase = keys.reduce((sum, k) => sum + products[k].basePrice, 0);
const avgPrice = totalBase / keys.length;
const totalTiers = keys.reduce((sum, k) => sum + products[k].quantityTiers.length, 0);
document.getElementById('statProducts').textContent = keys.length;
document.getElementById('statAvgPrice').textContent = '$' + avgPrice.toFixed(2);
document.getElementById('statTiers').textContent = totalTiers;
document.getElementById('statLastSaved').textContent = localStorage.getItem('printpro_last_saved') || 'Never';
}
function renderProductCards() {
const products = pricing.products;
productCards.innerHTML = Object.entries(products).map(([key, product]) => `
<div class="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden hover:shadow-md transition-all">
<div class="flex flex-col sm:flex-row">
<div class="sm:w-48 h-40 sm:h-auto flex-shrink-0">
<img src="${product.image}" alt="${product.name}" class="w-full h-full object-cover">
</div>
<div class="flex-1 p-5">
<div class="flex items-start justify-between mb-3">
<div>
<h3 class="text-lg font-bold text-gray-900 flex items-center gap-2">
<i data-lucide="${product.icon}" class="w-5 h-5 text-brand-500"></i>
${product.name}
</h3>
<p class="text-sm text-gray-500 mt-0.5">Product Key: <code class="bg-gray-100 px-1.5 py-0.5 rounded text-xs">${key}</code></p>
</div>
<span class="text-2xl font-extrabold text-brand-600">$${product.basePrice.toFixed(2)}</span>
</div>
<div class="grid grid-cols-2 sm:grid-cols-4 gap-3 text-xs">
<div class="bg-gray-50 rounded-lg p-2.5">
<p class="text-gray-400 uppercase tracking-wider font-semibold mb-0.5">Sizes</p>
<p class="font-bold text-gray-700">${product.sizes.length} options</p>
</div>
<div class="bg-gray-50 rounded-lg p-2.5">
<p class="text-gray-400 uppercase tracking-wider font-semibold mb-0.5">Paper Types</p>
<p class="font-bold text-gray-700">${product.paperTypes.length} options</p>
</div>
<div class="bg-gray-50 rounded-lg p-2.5">
<p class="text-gray-400 uppercase tracking-wider font-semibold mb-0.5">Finishing</p>
<p class="font-bold text-gray-700">${product.finishing.length} options</p>
</div>
<div class="bg-gray-50 rounded-lg p-2.5">
<p class="text-gray-400 uppercase tracking-wider font-semibold mb-0.5">Tiers</p>
<p class="font-bold text-gray-700">${product.quantityTiers.length} tiers</p>
</div>
</div>
<div class="mt-4 flex gap-2">
<button onclick="openEditModal('${key}')"
class="flex-1 bg-brand-600 text-white font-semibold py-2.5 rounded-lg hover:bg-brand-700 transition-all text-sm flex items-center justify-center gap-2">
<i data-lucide="edit-3" class="w-4 h-4"></i> Edit Pricing
</button>
</div>
</div>
</div>
</div>
`).join('');
lucide.createIcons();
}
// --- EDIT MODAL ---
function openEditModal(productKey) {
editingProductKey = productKey;
// Deep clone the product data for editing
editingData = JSON.parse(JSON.stringify(pricing.products[productKey]));
modalProductName.textContent = editingData.name;
renderModalContent();
editModal.classList.remove('hidden');
editModal.style.display = 'flex';
document.body.style.overflow = 'hidden';
}
function closeModal() {
editModal.classList.add('hidden');
editModal.style.display = 'none';
document.body.style.overflow = '';
editingProductKey = null;
editingData = null;
}
function renderModalContent() {
const d = editingData;
modalContent.innerHTML = `
<!-- Base Price -->
<div class="mb-6">
<label class="block text-sm font-bold text-gray-700 mb-2 flex items-center gap-2">
<i data-lucide="dollar-sign" class="w-4 h-4 text-brand-500"></i> Base Price
</label>
<div class="relative">
<span class="absolute left-3 top-1/2 -translate-y-1/2 text-gray-500 font-bold text-lg">$</span>
<input type="number" id="editBasePrice" value="${d.basePrice}" step="0.01" min="0"
class="w-full pl-8 pr-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-brand-500 focus:border-brand-500 outline-none text-gray-900 font-semibold text-lg">
</div>
<p class="text-xs text-gray-400 mt-1">The starting price before any options or quantity discounts.</p>
</div>
<!-- Sizes -->
<div class="mb-6">
<label class="block text-sm font-bold text-gray-700 mb-2 flex items-center gap-2">
<i data-lucide="ruler" class="w-4 h-4 text-brand-500"></i> Size Options
</label>
<div id="editSizes" class="space-y-2">
${d.sizes.map((s, i) => `
<div class="flex items-center gap-2 bg-gray-50 rounded-lg p-2.5 tier-row">
<input type="text" value="${s.name}" data-size-name="${i}" placeholder="Size name"
class="flex-1 px-3 py-2 border border-gray-200 rounded-lg text-sm focus:ring-1 focus:ring-brand-400 outline-none">
<div class="relative">
<span class="absolute left-2 top-1/2 -translate-y-1/2 text-gray-400 text-sm">$</span>
<input type="number" value="${s.price}" data-size-price="${i}" step="0.01" placeholder="Price"
class="w-24 pl-6 pr-2 py-2 border border-gray-200 rounded-lg text-sm focus:ring-1 focus:ring-brand-400 outline-none">
</div>
<button onclick="removeSizeOption(${i})" class="p-1.5 text-red-400 hover:text-red-600 hover:bg-red-50 rounded-lg transition-colors" title="Remove">
<i data-lucide="trash-2" class="w-3.5 h-3.5"></i>
</button>
</div>
`).join('')}
</div>
<button onclick="addSizeOption()" class="mt-2 text-sm text-brand-600 font-semibold hover:text-brand-700 flex items-center gap-1 transition-colors">
<i data-lucide="plus-circle" class="w-4 h-4"></i> Add Size Option
</button>
</div>
<!-- Paper Types -->
<div class="mb-6">
<label class="block text-sm font-bold text-gray-700 mb-2 flex items-center gap-2">
<i data-lucide="file-text" class="w-4 h-4 text-brand-500"></i> Paper Types
</label>
<div id="editPapers" class="space-y-2">
${d.paperTypes.map((p, i) => `
<div class="flex items-center gap-2 bg-gray-50 rounded-lg p-2.5 tier-row">
<input type="text" value="${p.name}" data-paper-name="${i}" placeholder="Paper name"
class="flex-1 px-3 py-2 border border-gray-200 rounded-lg text-sm focus:ring-1 focus:ring-brand-400 outline-none">
<div class="relative">
<span class="absolute left-2 top-1/2 -translate-y-1/2 text-gray-400 text-sm">$</span>
<input type="number" value="${p.price}" data-paper-price="${i}" step="0.01" placeholder="Price"
class="w-24 pl-6 pr-2 py-2 border border-gray-200 rounded-lg text-sm focus:ring-1 focus:ring-brand-400 outline-none">
</div>
<button onclick="removePaperOption(${i})" class="p-1.5 text-red-400 hover:text-red-600 hover:bg-red-50 rounded-lg transition-colors" title="Remove">
<i data-lucide="trash-2" class="w-3.5 h-3.5"></i>
</button>
</div>
`).join('')}
</div>
<button onclick="addPaperOption()" class="mt-2 text-sm text-brand-600 font-semibold hover:text-brand-700 flex items-center gap-1 transition-colors">
<i data-lucide="plus-circle" class="w-4 h-4"></i> Add Paper Option
</button>
</div>
<!-- Finishing -->
<div class="mb-6">
<label class="block text-sm font-bold text-gray-700 mb-2 flex items-center gap-2">
<i data-lucide="sparkles" class="w-4 h-4 text-brand-500"></i> Finishing Options
</label>
<div id="editFinishing" class="space-y-2">
${d.finishing.map((f, i) => `
<div class="flex items-center gap-2 bg-gray-50 rounded-lg p-2.5 tier-row">
<input type="text" value="${f.name}" data-finishing-name="${i}" placeholder="Finishing name"
class="flex-1 px-3 py-2 border border-gray-200 rounded-lg text-sm focus:ring-1 focus:ring-brand-400 outline-none">
<div class="relative">
<span class="absolute left-2 top-1/2 -translate-y-1/2 text-gray-400 text-sm">$</span>
<input type="number" value="${f.price}" data-finishing-price="${i}" step="0.01" placeholder="Price"
class="w-24 pl-6 pr-2 py-2 border border-gray-200 rounded-lg text-sm focus:ring-1 focus:ring-brand-400 outline-none">
</div>
<button onclick="removeFinishingOption(${i})" class="p-1.5 text-red-400 hover:text-red-600 hover:bg-red-50 rounded-lg transition-colors" title="Remove">
<i data-lucide="trash-2" class="w-3.5 h-3.5"></i>
</button>
</div>
`).join('')}
</div>
<button onclick="addFinishingOption()" class="mt-2 text-sm text-brand-600 font-semibold hover:text-brand-700 flex items-center gap-1 transition-colors">
<i data-lucide="plus-circle" class="w-4 h-4"></i> Add Finishing Option
</button>
</div>
<!-- Quantity Tiers -->
<div class="mb-6">
<label class="block text-sm font-bold text-gray-700 mb-2 flex items-center gap-2">
<i data-lucide="bar-chart-3" class="w-4 h-4 text-brand-500"></i> Quantity Tiers
</label>
<div id="editTiers" class="space-y-2">
${d.quantityTiers.map((t, i) => `
<div class="flex items-center gap-2 bg-gray-50 rounded-lg p-2.5 tier-row flex-wrap">
<span class="text-xs text-gray-400 font-semibold w-6">#${i+1}</span>
<label class="text-xs text-gray-500">Min:</label>
<input type="number" value="${t.min}" data-tier-min="${i}" step="1" min="1"
class="w-20 px-2 py-1.5 border border-gray-200 rounded-lg text-sm focus:ring-1 focus:ring-brand-400 outline-none">
<label class="text-xs text-gray-500">Max:</label>
<input type="number" value="${t.max}" data-tier-max="${i}" step="1" min="1"
class="w-20 px-2 py-1.5 border border-gray-200 rounded-lg text-sm focus:ring-1 focus:ring-brand-400 outline-none">
<label class="text-xs text-gray-500">Multiplier:</label>
<input type="number" value="${t.multiplier}" data-tier-mult="${i}" step="0.01" min="0.01" max="1"
class="w-20 px-2 py-1.5 border border-gray-200 rounded-lg text-sm focus:ring-1 focus:ring-brand-400 outline-none">
<button onclick="removeTier(${i})" class="p-1.5 text-red-400 hover:text-red-600 hover:bg-red-50 rounded-lg transition-colors ml-auto" title="Remove">
<i data-lucide="trash-2" class="w-3.5 h-3.5"></i>
</button>
</div>
`).join('')}
</div>
<button onclick="addTier()" class="mt-2 text-sm text-brand-600 font-semibold hover:text-brand-700 flex items-center gap-1 transition-colors">
<i data-lucide="plus-circle" class="w-4 h-4"></i> Add Tier
</button>
</div>
<!-- Turnaround -->
<div class="mb-4">
<label class="block text-sm font-bold text-gray-700 mb-2 flex items-center gap-2">
<i data-lucide="clock" class="w-4 h-4 text-brand-500"></i> Turnaround Options
</label>
<div id="editTurnaround" class="space-y-2">
${d.turnaround.map((t, i) => `
<div class="flex items-center gap-2 bg-gray-50 rounded-lg p-2.5 tier-row">
<input type="text" value="${t.name}" data-turnaround-name="${i}" placeholder="Turnaround name"
class="flex-1 px-3 py-2 border border-gray-200 rounded-lg text-sm focus:ring-1 focus:ring-brand-400 outline-none">
<div class="relative">
<span class="absolute left-2 top-1/2 -translate-y-1/2 text-gray-400 text-sm">$</span>
<input type="number" value="${t.price}" data-turnaround-price="${i}" step="0.01" placeholder="Price"
class="w-24 pl-6 pr-2 py-2 border border-gray-200 rounded-lg text-sm focus:ring-1 focus:ring-brand-400 outline-none">
</div>
<button onclick="removeTurnaroundOption(${i})" class="p-1.5 text-red-400 hover:text-red-600 hover:bg-red-50 rounded-lg transition-colors" title="Remove">
<i data-lucide="trash-2" class="w-3.5 h-3.5"></i>
</button>
</div>
`).join('')}
</div>
<button onclick="addTurnaroundOption()" class="mt-2 text-sm text-brand-600 font-semibold hover:text-brand-700 flex items-center gap-1 transition-colors">
<i data-lucide="plus-circle" class="w-4 h-4"></i> Add Turnaround Option
</button>
</div>
`;
lucide.createIcons();
}
// --- MODAL: ADD/REMOVE OPTIONS ---
function addSizeOption() { editingData.sizes.push({ id: 'new-' + Date.now(), name: 'New Size', price: 0 }); renderModalContent(); }
function removeSizeOption(i) { if (editingData.sizes.length > 1) { editingData.sizes.splice(i, 1); renderModalContent(); } else { showToast('Must have at least one size option ⚠️'); } }
function addPaperOption() { editingData.paperTypes.push({ id: 'new-' + Date.now(), name: 'New Paper', price: 0 }); renderModalContent(); }
function removePaperOption(i) { if (editingData.paperTypes.length > 1) { editingData.paperTypes.splice(i, 1); renderModalContent(); } else { showToast('Must have at least one paper option ⚠️'); } }
function addFinishingOption() { editingData.finishing.push({ id: 'new-' + Date.now(), name: 'New Finish', price: 0 }); renderModalContent(); }
function removeFinishingOption(i) { if (editingData.finishing.length > 0) { editingData.finishing.splice(i, 1); renderModalContent(); } }
function addTier() {
const tiers = editingData.quantityTiers;
const lastTier = tiers[tiers.length - 1];
const newMin = lastTier ? lastTier.max + 1 : 1;
tiers.push({ min: newMin, max: newMin + 500, multiplier: 0.50, label: `${newMin}-${newMin+500}` });
renderModalContent();
}
function removeTier(i) { if (editingData.quantityTiers.length > 1) { editingData.quantityTiers.splice(i, 1); renderModalContent(); } else { showToast('Must have at least one tier ⚠️'); } }
function addTurnaroundOption() { editingData.turnaround.push({ id: 'new-' + Date.now(), name: 'New Turnaround', price: 0 }); renderModalContent(); }
function removeTurnaroundOption(i) { if (editingData.turnaround.length > 1) { editingData.turnaround.splice(i, 1); renderModalContent(); } else { showToast('Must have at least one turnaround option ⚠️'); } }
// --- SAVE MODAL CHANGES ---
function saveModalChanges() {
if (!editingProductKey || !editingData) return;
// Gather base price
const basePriceEl = document.getElementById('editBasePrice');
editingData.basePrice = parseFloat(basePriceEl.value) || 0;
// Gather sizes
const sizeNames = modalContent.querySelectorAll('[data-size-name]');
const sizePrices = modalContent.querySelectorAll('[data-size-price]');
editingData.sizes = editingData.sizes.map((s, i) => ({
...s,
name: sizeNames[i]?.value || s.name,
price: parseFloat(sizePrices[i]?.value) || 0
}));
// Gather paper types
const paperNames = modalContent.querySelectorAll('[data-paper-name]');
const paperPrices = modalContent.querySelectorAll('[data-paper-price]');
editingData.paperTypes = editingData.paperTypes.map((p, i) => ({
...p,
name: paperNames[i]?.value || p.name,
price: parseFloat(paperPrices[i]?.value) || 0
}));
// Gather finishing
const finishingNames = modalContent.querySelectorAll('[data-finishing-name]');
const finishingPrices = modalContent.querySelectorAll('[data-finishing-price]');
editingData.finishing = editingData.finishing.map((f, i) => ({
...f,
name: finishingNames[i]?.value || f.name,
price: parseFloat(finishingPrices[i]?.value) || 0
}));
// Gather tiers
const tierMins = modalContent.querySelectorAll('[data-tier-min]');
const tierMaxs = modalContent.querySelectorAll('[data-tier-max]');
const tierMults = modalContent.querySelectorAll('[data-tier-mult]');
editingData.quantityTiers = editingData.quantityTiers.map((t, i) => ({
...t,
min: parseInt(tierMins[i]?.value) || t.min,
max: parseInt(tierMaxs[i]?.value) || t.max,
multiplier: parseFloat(tierMults[i]?.value) || t.multiplier,
label: `${parseInt(tierMins[i]?.value) || t.min}-${parseInt(tierMaxs[i]?.value) || t.max}`
}));
// Gather turnaround
const turnaroundNames = modalContent.querySelectorAll('[data-turnaround-name]');
const turnaroundPrices = modalContent.querySelectorAll('[data-turnaround-price]');
editingData.turnaround = editingData.turnaround.map((t, i) => ({
...t,
name: turnaroundNames[i]?.value || t.name,
price: parseFloat(turnaroundPrices[i]?.value) || 0
}));
// Save to pricing
pricing.products[editingProductKey] = editingData;
savePricingToStorage(pricing);
closeModal();
updateStats();
renderProductCards();
showToast('Product pricing updated successfully! ✅');
}
// --- SAVE ALL ---
function saveAllPricing() {
savePricingToStorage(pricing);
showToast('All pricing saved successfully! 💾');
}
// --- RESET ---
function resetToDefault() {
if (confirm('Are you sure you want to reset ALL pricing to default values? This cannot be undone.')) {
pricing = JSON.parse(JSON.stringify(DEFAULT_PRICING));
savePricingToStorage(pricing);
updateStats();
renderProductCards();
showToast('Pricing reset to defaults! 🔄');
}
}
// --- CLOSE MODAL ON OVERLAY CLICK ---
editModal.addEventListener('click', (e) => {
if (e.target === editModal) {
closeModal();
}
});
// --- KEYBOARD SHORTCUTS ---
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && !editModal.classList.contains('hidden')) {
closeModal();
}
});
// --- INIT ON LOAD ---
function init() {
if (isLoggedIn) {
loginScreen.classList.add('hidden');
dashboard.classList.remove('hidden');
initDashboard();
} else {
loginScreen.classList.remove('hidden');
dashboard.classList.add('hidden');
}
// Update last saved display
document.getElementById('statLastSaved').textContent = localStorage.getItem('printpro_last_saved') || 'Never';
}
document.addEventListener('DOMContentLoaded', init);
// Expose functions to global scope
window.openEditModal = openEditModal;
window.closeModal = closeModal;
window.saveModalChanges = saveModalChanges;
window.addSizeOption = addSizeOption;
window.removeSizeOption = removeSizeOption;
window.addPaperOption = addPaperOption;
window.removePaperOption = removePaperOption;
window.addFinishingOption = addFinishingOption;
window.removeFinishingOption = removeFinishingOption;
window.addTier = addTier;
window.removeTier = removeTier;
window.addTurnaroundOption = addTurnaroundOption;
window.removeTurnaroundOption = removeTurnaroundOption;
window.saveAllPricing = saveAllPricing;
window.resetToDefault = resetToDefault;
window.logout = logout;