Balloon Decoration Designer

Balloon Decoration Designer

Event Details

Decoration Elements

Adjust the unit prices for each item. All data is customizable (Spec Other 4).

Arch Prices

Element Prices

Fees

No colors selected.

'; // 2. Get Prices (Tab 3) const priceArch = bdd_getFloat(`bdd-price-arch-${archType}`); const priceColumns = bdd_getFloat("bdd-price-columns"); const priceBouquets = bdd_getFloat("bdd-price-bouquets"); const priceCenterpieces = bdd_getFloat("bdd-price-centerpieces"); const priceFoil = bdd_getFloat("bdd-price-foil"); const priceGarland = bdd_getFloat("bdd-price-garland"); const priceFee = bdd_getFloat("bdd-price-fee"); // 3. Calculate Totals const items = []; let subtotal = 0; if (archType > 0) { items.push({ name: `Balloon Arch (${archText})`, qty: 1, unitPrice: priceArch, total: priceArch }); subtotal += priceArch; } if (qtyColumns > 0) { const total = qtyColumns * priceColumns; items.push({ name: 'Balloon Columns (Pair)', qty: qtyColumns, unitPrice: priceColumns, total: total }); subtotal += total; } if (qtyBouquets > 0) { const total = qtyBouquets * priceBouquets; items.push({ name: 'Balloon Bouquets', qty: qtyBouquets, unitPrice: priceBouquets, total: total }); subtotal += total; } if (qtyCenterpieces > 0) { const total = qtyCenterpieces * priceCenterpieces; items.push({ name: 'Table Centerpieces', qty: qtyCenterpieces, unitPrice: priceCenterpieces, total: total }); subtotal += total; } if (qtyFoil > 0) { const total = qtyFoil * priceFoil; items.push({ name: 'Jumbo Foil Balloons', qty: qtyFoil, unitPrice: priceFoil, total: total }); subtotal += total; } if (qtyGarland > 0) { const total = qtyGarland * priceGarland; items.push({ name: 'Organic Garland (ft)', qty: qtyGarland, unitPrice: priceGarland, total: total }); subtotal += total; } const totalCost = subtotal + priceFee; // 4. Build HTML Output // Using tables for professional formatting (Spec II.C.2) let html = `

Event Summary

Event Type ${eventType}
Color Palette ${colorListHTML}
`; if (items.length > 0) { html += `

Itemized Estimate

${items.map(item => ` `).join('')}
Item Quantity Unit Price Total
${item.name} ${item.qty} ${bdd_formatCurrency(item.unitPrice)} ${bdd_formatCurrency(item.total)}
`; } else { html += `

No items selected. Go to the "Design Studio" tab to add decorations.

`; } html += `

Cost Summary

Subtotal ${bdd_formatCurrency(subtotal)}
Setup & Delivery Fee ${bdd_formatCurrency(priceFee)}
Total Estimated Cost ${bdd_formatCurrency(totalCost)}
`; outputDiv.innerHTML = html; } /** * PDF Download Function (Spec II.C) */ function bdd_downloadPdf() { // Check for jsPDF (Spec IV.B - Robust check) if (typeof jspdf === "undefined" || typeof jspdf.plugin.autotable === "undefined") { alert("Error: PDF library is missing. Please refresh the page."); return; } const { jsPDF } = window.jspdf; const doc = new jsPDF(); // --- Get Data Again (cleanly, for PDF) --- const eventType = bdd_getString("bdd-event-type"); const archType = bdd_getFloat("bdd-item-arch"); const archText = bdd_getSelectText("bdd-item-arch"); const qtyColumns = bdd_getFloat("bdd-item-columns"); const qtyBouquets = bdd_getFloat("bdd-item-bouquets"); const qtyCenterpieces = bdd_getFloat("bdd-item-centerpieces"); const qtyFoil = bdd_getFloat("bdd-item-foil"); const qtyGarland = bdd_getFloat("bdd-item-garland"); const selectedColors = []; colorCheckboxes.forEach(cb => { if (cb.checked) selectedColors.push(cb.value); }); const colorString = selectedColors.length > 0 ? selectedColors.join(', ') : 'N/A'; const priceArch = bdd_getFloat(`bdd-price-arch-${archType}`); const priceColumns = bdd_getFloat("bdd-price-columns"); const priceBouquets = bdd_getFloat("bdd-price-bouquets"); const priceCenterpieces = bdd_getFloat("bdd-price-centerpieces"); const priceFoil = bdd_getFloat("bdd-price-foil"); const priceGarland = bdd_getFloat("bdd-price-garland"); const priceFee = bdd_getFloat("bdd-price-fee"); const itemBody = []; let subtotal = 0; if (archType > 0) { itemBody.push([ `Balloon Arch (${archText})`, 1, bdd_formatCurrency(priceArch), bdd_formatCurrency(priceArch) ]); subtotal += priceArch; } if (qtyColumns > 0) { const total = qtyColumns * priceColumns; itemBody.push([ 'Balloon Columns (Pair)', qtyColumns, bdd_formatCurrency(priceColumns), bdd_formatCurrency(total) ]); subtotal += total; } if (qtyBouquets > 0) { const total = qtyBouquets * priceBouquets; itemBody.push([ 'Balloon Bouquets', qtyBouquets, bdd_formatCurrency(priceBouquets), bdd_formatCurrency(total) ]); subtotal += total; } if (qtyCenterpieces > 0) { const total = qtyCenterpieces * priceCenterpieces; itemBody.push([ 'Table Centerpieces', qtyCenterpieces, bdd_formatCurrency(priceCenterpieces), bdd_formatCurrency(total) ]); subtotal += total; } if (qtyFoil > 0) { const total = qtyFoil * priceFoil; itemBody.push([ 'Jumbo Foil Balloons', qtyFoil, bdd_formatCurrency(priceFoil), bdd_formatCurrency(total) ]); subtotal += total; } if (qtyGarland > 0) { const total = qtyGarland * priceGarland; itemBody.push([ 'Organic Garland (ft)', qtyGarland, bdd_formatCurrency(priceGarland), bdd_formatCurrency(total) ]); subtotal += total; } const totalCost = subtotal + priceFee; // --- Build PDF Document (Spec II.C.2 - Professional Formatting) --- doc.setFontSize(18); doc.text("Balloon Decoration Summary", 14, 22); // Event Details doc.autoTable({ startY: 30, head: [['Event Details', '']], body: [ ['Event Type', eventType], ['Color Palette', colorString], ], theme: 'striped', headStyles: { fillColor: '#007bff' // Consistent color (Spec II.C.3) }, }); // Itemized Estimate if (itemBody.length > 0) { doc.autoTable({ startY: doc.lastAutoTable.finalY + 10, head: [['Item', 'Quantity', 'Unit Price', 'Total']], body: itemBody, theme: 'striped', headStyles: { fillColor: '#007bff' }, didDrawCell: (data) => { if (data.section === 'body' && data.column.index >= 1) { data.cell.styles.halign = 'right'; } } }); } // Cost Summary doc.autoTable({ startY: doc.lastAutoTable.finalY + 10, head: [['Cost Summary', 'Amount']], body: [ ['Subtotal', bdd_formatCurrency(subtotal)], ['Setup & Delivery Fee', bdd_formatCurrency(priceFee)], ['Total Estimated Cost', bdd_formatCurrency(totalCost)], ], theme: 'striped', headStyles: { fillColor: '#343a40' }, bodyStyles: { fontStyle: 'bold' }, didDrawCell: (data) => { if (data.column.index === 0) { data.cell.styles.halign = 'right'; data.cell.styles.fontStyle = 'bold'; } if (data.column.index === 1) { data.cell.styles.halign = 'right'; } if(data.row.index === 2) { data.cell.styles.fontSize = 12; } } }); // Save the PDF (Spec II.C.4 - Functional Button) doc.save("Balloon-Decoration-Summary.pdf"); } // --- Checkbox Limitation --- function bdd_limitColorCheckboxes() { let checkedCount = 0; colorCheckboxes.forEach(cb => { if (cb.checked) checkedCount++; }); if (checkedCount >= 4) { colorCheckboxes.forEach(cb => { if (!cb.checked) cb.disabled = true; }); } else { colorCheckboxes.forEach(cb => { cb.disabled = false; }); } bdd_updateSummary(); // Update summary on check } // --- Event Listener Attachment (Spec IV.A) --- // Attach to all inputs for live calculation calcInputs.forEach((input) => { input.addEventListener("input", bdd_updateSummary); }); // Attach to color checkboxes colorCheckboxes.forEach((input) => { input.addEventListener("change", bdd_limitColorCheckboxes); }); // Attach to event type select if (eventTypeSelect) { eventTypeSelect.addEventListener("change", bdd_updateSummary); } // Attach to PDF button if (pdfButton) { pdfButton.addEventListener("click", bdd_downloadPdf); } // --- Initial Load --- // Open the first tab by default const defaultTab = container.querySelector(".bdd-tab-link.bdd-active"); if (defaultTab) { // Use the element's 'onclick' handler to open it // This is a robust way to ensure the correct function runs defaultTab.click(); } // Run initial calculation bdd_updateSummary(); bdd_limitColorCheckboxes(); // Run limit check on load }); // --- Global Functions (required for inline 'onclick') (Spec IV.C) --- /** * Tab navigation function * @param {Event} evt - The click event * @param {string} tabName - The ID of the tab content to show */ function bdd_openTab(evt, tabName) { const container = document.getElementById("bdd-container"); if (!container) return; // Guard clause // Get all elements with class="bdd-tab-content" and hide them const tabcontent = container.getElementsByClassName("bdd-tab-content"); for (let i = 0; i < tabcontent.length; i++) { tabcontent[i].style.display = "none"; tabcontent[i].classList.remove("bdd-active"); } // Get all elements with class="bdd-tab-link" and remove the class "bdd-active" const tablinks = container.getElementsByClassName("bdd-tab-link"); for (let i = 0; i < tablinks.length; i++) { tablinks[i].classList.remove("bdd-active"); } // Show the current tab, and add an "bdd-active" class to the button that opened the tab const activeTab = document.getElementById(tabName); if (activeTab) { // Guard clause (Spec IV.B) activeTab.style.display = "block"; activeTab.classList.add("bdd-active"); } if (evt && evt.currentTarget) { evt.currentTarget.classList.add("bdd-active"); } // If navigating to Tab 2, refresh the summary if (tabName === 'bdd-tab-2') { // Find the global 'bdd_updateSummary' function (it's defined inside DOMContentLoaded) // To make it accessible, we need to find it on the window or redefine it globally. // For simplicity with inline onclick, we will call the global function // that was defined inside the DOMContentLoaded scope *if* it was attached. // This is tricky. A better way: make bdd_updateSummary global. // Let's redefine the update function outside the DOMContentLoaded // and just call it from inside and from here. // Re-thinking: The bdd_updateSummary is already called on *every input change*. // So, when the user *clicks* the tab, the summary is already up-to-date. // This is a better UX. No extra call needed here. } } /** * Next/Previous button navigation (Spec II.B.4) * @param {number} direction - 1 for next, -1 for previous */ function bdd_navTab(direction) { const container = document.getElementById("bdd-container"); if (!container) return; // Guard clause const tabs = container.querySelectorAll(".bdd-tab-link"); const tabContents = container.querySelectorAll(".bdd-tab-content"); let currentIndex = -1; // Find the current active tab index for (let i = 0; i < tabContents.length; i++) { if (tabContents[i].classList.contains("bdd-active")) { currentIndex = i; break; } } if (currentIndex === -1) return; // Should not happen // Calculate new index let newIndex = currentIndex + direction; // Loop around if (newIndex >= tabs.length) { newIndex = 0; } if (newIndex < 0) { newIndex = tabs.length - 1; } // Simulate a click on the new tab button tabs[newIndex].click(); }