| |
| |
| |
|
|
| |
| 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 |
| } |
| } |
| }; |
|
|
| |
| let pricing = null; |
| let editingProductKey = null; |
| let editingData = null; |
|
|
| |
| const ADMIN_CREDENTIALS = { username: 'admin', password: 'admin123' }; |
| let isLoggedIn = sessionStorage.getItem('printpro_admin_loggedin') === 'true'; |
|
|
| |
| 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'); |
|
|
| |
| 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)); |
| |
| window.dispatchEvent(new Event('pricingUpdated')); |
| |
| 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); |
| } |
|
|
| |
| 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'); |
| } |
|
|
| |
| 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); |
| } |
|
|
| |
| 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(); |
| } |
|
|
| |
| function openEditModal(productKey) { |
| editingProductKey = productKey; |
| |
| 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(); |
| } |
|
|
| |
| 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 ⚠️'); } } |
|
|
| |
| function saveModalChanges() { |
| if (!editingProductKey || !editingData) return; |
|
|
| |
| const basePriceEl = document.getElementById('editBasePrice'); |
| editingData.basePrice = parseFloat(basePriceEl.value) || 0; |
|
|
| |
| 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 |
| })); |
|
|
| |
| 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 |
| })); |
|
|
| |
| 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 |
| })); |
|
|
| |
| 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}` |
| })); |
|
|
| |
| 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 |
| })); |
|
|
| |
| pricing.products[editingProductKey] = editingData; |
| savePricingToStorage(pricing); |
| closeModal(); |
| updateStats(); |
| renderProductCards(); |
| showToast('Product pricing updated successfully! ✅'); |
| } |
|
|
| |
| function saveAllPricing() { |
| savePricingToStorage(pricing); |
| showToast('All pricing saved successfully! 💾'); |
| } |
|
|
| |
| 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! 🔄'); |
| } |
| } |
|
|
| |
| editModal.addEventListener('click', (e) => { |
| if (e.target === editModal) { |
| closeModal(); |
| } |
| }); |
|
|
| |
| document.addEventListener('keydown', (e) => { |
| if (e.key === 'Escape' && !editModal.classList.contains('hidden')) { |
| closeModal(); |
| } |
| }); |
|
|
| |
| function init() { |
| if (isLoggedIn) { |
| loginScreen.classList.add('hidden'); |
| dashboard.classList.remove('hidden'); |
| initDashboard(); |
| } else { |
| loginScreen.classList.remove('hidden'); |
| dashboard.classList.add('hidden'); |
| } |
| |
| document.getElementById('statLastSaved').textContent = localStorage.getItem('printpro_last_saved') || 'Never'; |
| } |
|
|
| document.addEventListener('DOMContentLoaded', init); |
|
|
| |
| 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; |