Sales Proposal Generator

Sales Proposal Generator

${escapeHTML(proposal.timeline).replace(/\n/g, '
')}

Investment

${proposal.pricing.map(item => ` `).join('')}
ItemCost
${escapeHTML(item.name)} $${parseFloat(item.value).toLocaleString()}
Total Investment $${proposal.pricing.reduce((sum, item) => sum + parseFloat(item.value), 0).toLocaleString()}

Terms & Conditions

${escapeHTML(proposal.terms).replace(/\n/g, '
')}

`; proposalContent.innerHTML = html; // 2. Render KPIs const totalValue = proposal.pricing.reduce((sum, item) => sum + parseFloat(item.value), 0); kpiTotal.textContent = `$${totalValue.toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2})}`; // 3. Render Chart renderCostChart(); // 4. Show buttons pdfDownloadBtn.style.display = 'inline-block'; copyBtn.style.display = 'inline-block'; } function renderCostChart() { if (!costChartCanvas) return; const ctx = costChartCanvas.getContext('2d'); const labels = proposal.pricing.map(item => item.name); const data = proposal.pricing.map(item => item.value); if (costChartInstance) { costChartInstance.destroy(); } if (data.length === 0) { ctx.clearRect(0, 0, costChartCanvas.width, costChartCanvas.height); ctx.font = "14px Inter"; ctx.fillStyle = "#a8a29e"; // stone-400 ctx.textAlign = "center"; ctx.fillText("No pricing items to display chart.", costChartCanvas.width / 2, costChartCanvas.height / 2); return; } costChartInstance = new Chart(ctx, { type: 'doughnut', data: { labels: labels, datasets: [{ label: 'Cost Breakdown', data: data, backgroundColor: [ '#f97316', // orange-500 '#fb923c', // orange-400 '#fdba74', // orange-300 '#78716c', // stone-500 '#a8a29e', // stone-400 ], borderColor: '#ffffff', borderWidth: 3 }] }, options: { responsive: true, maintainAspectRatio: false, // Per spec plugins: { legend: { position: 'bottom' }, tooltip: { callbacks: { label: (context) => `${context.label}: $${parseFloat(context.raw).toLocaleString()}` } } } } }); } // --- Event Handlers --- function handleGenerateProposal() { // Save data from form to proposal object proposal.clientName = clientNameInput.value; proposal.clientCompany = clientCompanyInput.value; proposal.date = dateInput.value; proposal.overview = overviewInput.value; proposal.timeline = timelineInput.value; proposal.terms = termsInput.value; // Check for required fields if (!proposal.clientName || !proposal.clientCompany || !proposal.date) { showMessage("Please fill in Client Name, Company, and Date.", "error"); return; } renderDashboard(); showTab(0); } function handleAddDeliverable() { const text = newDeliverableInput.value.trim(); if (text) { proposal.deliverables.push(text); newDeliverableInput.value = ''; renderBuilderLists(); } } function handleAddPriceItem() { const name = newPriceItemInput.value.trim(); const value = parseFloat(newPriceValueInput.value); if (name && value > 0) { proposal.pricing.push({ name, value }); newPriceItemInput.value = ''; newPriceValueInput.value = ''; renderBuilderLists(); } } function handleBuilderListDelete(e) { const target = e.target; if (!target.classList.contains('delete-btn')) return; const type = target.dataset.type; const index = parseInt(target.dataset.index, 10); if (type === 'deliverable') proposal.deliverables.splice(index, 1); if (type === 'pricing') proposal.pricing.splice(index, 1); renderBuilderLists(); } function handleConfigDelete(e) { const target = e.target; if (!target.classList.contains('delete-btn')) return; const type = target.dataset.type; const id = target.dataset.id; if (type === 'reusableItem') deleteConfigItem('reusableItems', id); if (type === 'reusableTerm') deleteConfigItem('reusableTerms', id); } async function downloadPDF() { pdfTarget.classList.add('spg-pdf-view'); try { // Ensure chart is rendered for PDF const chartCanvasForPdf = document.createElement('canvas'); chartCanvasForPdf.width = costChartCanvas.width; chartCanvasForPdf.height = costChartCanvas.height; const chartCtxPdf = chartCanvasForPdf.getContext('2d'); chartCtxPdf.fillStyle = '#FFFFFF'; chartCtxPdf.fillRect(0, 0, chartCanvasForPdf.width, chartCanvasForPdf.height); if (costChartInstance) { chartCtxPdf.drawImage(costChartCanvas, 0, 0); } else { chartCtxPdf.font = "14px Inter"; chartCtxPdf.fillStyle = "#a8a29e"; chartCtxPdf.textAlign = "center"; chartCtxPdf.fillText("No pricing items.", chartCanvasForPdf.width / 2, chartCanvasForPdf.height / 2); } const chartImage = chartCanvasForPdf.toDataURL('image/png'); const canvas = await html2canvas(pdfTarget, { scale: 2, logging: false, useCORS: true, ignoreElements: (element) => element.id === 'spg-cost-chart' // Ignore live chart }); const imgData = canvas.toDataURL('image/png'); const pdf = new jsPDF('p', 'mm', 'a4'); const pdfWidth = pdf.internal.pageSize.getWidth(); const pdfHeight = pdf.internal.pageSize.getHeight(); const margin = 15; const imgWidth = pdfWidth - (margin * 2); const imgHeight = (canvas.height * imgWidth) / canvas.width; let yPosition = margin; pdf.addImage(imgData, 'PNG', margin, yPosition, imgWidth, imgHeight); yPosition += imgHeight + 10; // Add Chart const chartHeightInPdf = (chartCanvasForPdf.height * imgWidth) / chartCanvasForPdf.width; if (yPosition + chartHeightInPdf > pdfHeight - margin) { pdf.addPage(); yPosition = margin; } pdf.setFontSize(14); pdf.setTextColor(55, 65, 81); pdf.text("Cost Breakdown", margin, yPosition); yPosition += 8; pdf.addImage(chartImage, 'PNG', margin, yPosition, imgWidth, chartHeightInPdf); pdf.save(`${proposal.clientCompany || 'Sales_Proposal'}.pdf`); } catch (error) { console.error("Error generating PDF:", error); showMessage("Error generating PDF. Please try again.", "error"); } finally { pdfTarget.classList.remove('spg-pdf-view'); } } function copyProposalText() { let text = `Sales Proposal for: ${proposal.clientCompany}\n`; text += `Prepared for: ${proposal.clientName}\nDate: ${proposal.date}\n\n`; text += `--- PROJECT OVERVIEW ---\n${proposal.overview}\n\n`; text += `--- SCOPE & DELIVERABLES ---\n${proposal.deliverables.map(item => `• ${item}`).join('\n')}\n\n`; text += `--- PROJECT TIMELINE ---\n${proposal.timeline}\n\n`; text += `--- INVESTMENT ---\n`; let total = 0; proposal.pricing.forEach(item => { text += `• ${item.name}: $${parseFloat(item.value).toLocaleString()}\n`; total += parseFloat(item.value); }); text += `\nTotal: $${total.toLocaleString()}\n\n`; text += `--- TERMS & CONDITIONS ---\n${proposal.terms}`; const el = document.createElement('textarea'); el.value = text; document.body.appendChild(el); el.select(); try { document.execCommand('copy'); showMessage('Proposal text copied to clipboard!'); } catch (err) { showMessage('Failed to copy text.', 'error'); } document.body.removeChild(el); } // --- Utilities --- function showMessage(message, type = "info") { let feedbackEl = document.getElementById('feedback-message'); if (!feedbackEl) { feedbackEl = document.createElement('div'); feedbackEl.id = 'feedback-message'; feedbackEl.style.position = 'fixed'; feedbackEl.style.bottom = '20px'; feedbackEl.style.left = '50%'; feedbackEl.style.transform = 'translateX(-50%)'; feedbackEl.style.padding = '10px 20px'; feedbackEl.style.borderRadius = '8px'; feedbackEl.style.zIndex = '1000'; feedbackEl.style.opacity = '0'; feedbackEl.style.transition = 'opacity 0.5s ease'; document.body.appendChild(feedbackEl); } feedbackEl.textContent = message; feedbackEl.style.opacity = '1'; const colors = { error: { bg: '#fef2f2', text: '#991b1b' }, success: { bg: '#f0fdf4', text: '#14532d' }, info: { bg: '#eff6ff', text: '#1e3a8a' }}; feedbackEl.style.backgroundColor = colors[type].bg; feedbackEl.style.color = colors[type].text; setTimeout(() => { feedbackEl.style.opacity = '0'; }, 3000); } function escapeHTML(str) { if (typeof str !== 'string') return ''; return str ? str.replace(/[&<>"']/g, m => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[m])) : ''; } function loadSampleData() { // Set default date dateInput.value = new Date().toISOString().split('T')[0]; // Sample data (USA-centric) addConfigItem('reusableItems', { name: "Website Design (5 Pages)", value: 5000 }); addConfigItem('reusableItems', { name: "E-commerce Setup (Shopify, USA)", value: 7000 }); addConfigItem('reusableTerms', { text: "Payment due 30 days upon receipt of invoice. Overdue invoices subject to 1.5% monthly interest." }); addConfigItem('reusableTerms', { text: "All work is property of [Your Company] until final payment is received." }); } // --- Event Listeners --- navTabs.forEach((tab, index) => tab.addEventListener('click', () => showTab(index))); navPrev.addEventListener('click', () => showTab(currentTab - 1)); navNext.addEventListener('click', () => showTab(currentTab + 1)); // Tab 1 pdfDownloadBtn.addEventListener('click', downloadPDF); copyBtn.addEventListener('click', copyProposalText); // Tab 2 generateBtn.addEventListener('click', handleGenerateProposal); addDeliverableBtn.addEventListener('click', handleAddDeliverable); addPriceItemBtn.addEventListener('click', handleAddPriceItem); deliverablesList.addEventListener('click', handleBuilderListDelete); pricingList.addEventListener('click', handleBuilderListDelete); addSavedItemBtn.addEventListener('click', () => { if (dbState.reusableItems.length > 0) { proposal.pricing.push(...dbState.reusableItems); renderBuilderLists(); } else { showMessage("No saved items to add.", "info"); } }); addSavedTermBtn.addEventListener('click', () => { if (dbState.reusableTerms.length > 0) { const allTerms = dbState.reusableTerms.map(t => t.text).join('\n\n'); termsInput.value = (termsInput.value + '\n\n' + allTerms).trim(); } else { showMessage("No saved terms to add.", "info"); } }); // Tab 3 addReusableItemForm.addEventListener('submit', (e) => { e.preventDefault(); addConfigItem('reusableItems', { name: newReusableItemName.value, value: parseFloat(newReusableItemValue.value) }); addReusableItemForm.reset(); }); addReusableTermForm.addEventListener('submit', (e) => { e.preventDefault(); addConfigItem('reusableTerms', { text: newReusableTermText.value }); addReusableTermForm.reset(); }); reusableItemsList.addEventListener('click', handleConfigDelete); reusableTermsList.addEventListener('click', handleConfigDelete); // --- Initial Load --- loadSampleData(); renderAll(); showTab(1); // Start on builder

Sales Proposal Generator

Build, configure, and export professional sales proposals.

Generated Proposal

Proposal Builder

Client Information

Project Overview

Scope & Deliverables

Project Timeline

Investment / Pricing

Terms & Conditions

Configuration

Save reusable pricing items and terms clauses to your database. These will be available to add in the 'Proposal Builder' tab.

Saved Pricing Items

Saved Terms Clauses

Scroll to Top