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

'; // 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(); }