// helper: convert number to Indian-notation words (Crore, Lakh, Thousand, Hundred) function numberToWords(num) { const small = ['', 'One', 'Two', 'Three', 'Four', 'Five', 'Six', 'Seven', 'Eight', 'Nine', 'Ten', 'Eleven', 'Twelve', 'Thirteen', 'Fourteen', 'Fifteen', 'Sixteen', 'Seventeen', 'Eighteen', 'Nineteen']; const tens = ['', '', 'Twenty', 'Thirty', 'Forty', 'Fifty', 'Sixty', 'Seventy', 'Eighty', 'Ninety']; function twoDigit(n) { if (n < 20) return small[n]; return tens[Math.floor(n / 10)] + (n % 10 ? ' ' + small[n % 10] : ''); } let words = ''; const crore = Math.floor(num / 10000000); num %= 10000000; if (crore) words += twoDigit(crore) + ' Crore '; const lakh = Math.floor(num / 100000); num %= 100000; if (lakh) words += twoDigit(lakh) + ' Lakh '; const thousand = Math.floor(num / 1000); num %= 1000; if (thousand) words += twoDigit(thousand) + ' Thousand '; const hundred = Math.floor(num / 100); num %= 100; if (hundred) words += small[hundred] + ' Hundred '; if (num) words += (words ? 'and ' : '') + twoDigit(num) + ' '; return words.trim() || 'Zero'; } if (typeof document !== 'undefined') { document.addEventListener('DOMContentLoaded', function () { const addItemBtn = document.getElementById('add-item'); const itemsTableBody = document.querySelector('#items-table tbody'); const form = document.getElementById('quotation-form'); const output = document.getElementById('quotation-output'); const previewContent = document.getElementById('preview-content'); // Add initial empty row addItemRow(); function generateQuotationHTML(form) { const data = new FormData(form); const company = { name: data.get('company-name'), address: data.get('company-address'), phone: data.get('company-phone'), email: data.get('company-email'), gstin: data.get('company-gstin') }; const customer = { name: data.get('customer-name'), address: data.get('customer-address'), phone: data.get('customer-phone'), email: data.get('customer-email'), gstin: data.get('customer-gstin') }; const quotationNumber = data.get('quotation-number'); const quotationDate = data.get('quotation-date'); const igstRate = parseFloat(data.get('igst-rate')) || 0; const freightCharges = parseFloat(data.get('freight-charges')) || 0; const bank = { name: data.get('bank-name'), account: data.get('bank-account'), ifsc: data.get('bank-ifsc'), branch: data.get('bank-branch') }; const items = []; itemsTableBody.querySelectorAll('tr').forEach(row => { const desc = row.querySelector('.item-desc').value.trim(); if (desc) { // Only include rows with description items.push({ description: desc, hsn: row.querySelector('.item-hsn').value, qty: parseFloat(row.querySelector('.item-qty').value) || 0, price: parseFloat(row.querySelector('.item-price').value) || 0, discount: parseFloat(row.querySelector('.item-discount').value) || 0, amount: parseFloat(row.querySelector('.item-amount').textContent) || 0 }); } }); if (items.length === 0) { return '
Please add items to generate quotation
'; } const { subtotal, igstAmount, _total, rOff, finalTotal } = calculateQuotation(items, igstRate, freightCharges); // convert total to words (Rupees and Paise) const rupeePart = Math.floor(finalTotal); const paisePart = Math.round((finalTotal - rupeePart) * 100); const rupeeWords = numberToWords(rupeePart); const paiseWords = paisePart > 0 ? numberToWords(paisePart) : ''; let html = `

${company.name}

{{address}}
GST NO. : ${company.gstin || ''}
CONTACT NO : ${company.phone} ${company.email}

QUOTATION

QUO. NO ${quotationNumber}
DATE ${quotationDate}
CUSTOMER INFO
${customer.name}
{{cutomer_address}}
GST NO. : ${customer.gstin || ''}
CONTACT NO : ${customer.phone} ${customer.email}
`; items.forEach((item, idx) => { html += ` `; }); // Add empty rows to fill the page for (let i = items.length; i < 7; i++) { html += ''; } html += `
SL NO DESCRIPTION HSN CODE QTY UNIT PRICE DISCOUNT AMOUNT
${idx + 1} ${item.description} ${item.hsn} ${item.qty} ${item.price.toFixed(2)} ${item.discount.toFixed(2)}% ${item.amount.toFixed(2)}
 
`; html = html.replace('{{address}}', company.address.replace(/\n/g, '
')); html = html.replace('{{cutomer_address}}', customer.address.replace(/\n/g, '
')); return html; } function updatePreview() { const html = generateQuotationHTML(form); previewContent.innerHTML = html; } function updateSerialNumbers() { itemsTableBody.querySelectorAll('tr').forEach((row, i) => { row.querySelector('.item-slno').textContent = i + 1; }); } function validateItemInput(input) { const value = input.value; const type = input.type; input.classList.remove('input-error', 'input-success'); if (type === 'number' && value !== '') { const num = parseFloat(value); if (isNaN(num) || num < 0) { input.classList.add('input-error'); return false; } else { input.classList.add('input-success'); } } else if (type === 'text' && input.hasAttribute('required') && value.trim() === '') { input.classList.add('input-error'); return false; } return true; } function handleKeyNavigation(event) { if (event.key === 'Tab' || event.key === 'Enter') { const currentRow = event.target.closest('tr'); const inputs = Array.from(currentRow.querySelectorAll('input')); const currentIndex = inputs.indexOf(event.target); if (event.key === 'Enter') { event.preventDefault(); // If we're at the last input in the row and it's the last row, add a new row if (currentIndex === inputs.length - 1) { const allRows = Array.from(itemsTableBody.querySelectorAll('tr')); const currentRowIndex = allRows.indexOf(currentRow); if (currentRowIndex === allRows.length - 1) { // Last row - add new row and focus first input addItemRow(); setTimeout(() => { const newRow = itemsTableBody.lastElementChild; newRow.querySelector('.item-desc').focus(); }, 50); } else { // Not last row - focus next row's first input const nextRow = allRows[currentRowIndex + 1]; nextRow.querySelector('.item-desc').focus(); } } else { // Move to next input in same row inputs[currentIndex + 1].focus(); } } } } function addItemRow() { const row = document.createElement('tr'); row.innerHTML = `
Describe the item or service being quoted
Harmonized System of Nomenclature code for tax purposes
Quantity of items
Price per unit before discount
Discount percentage (0-100)
0.00 `; itemsTableBody.appendChild(row); updateSerialNumbers(); // Add event listeners to inputs const inputs = row.querySelectorAll('input'); inputs.forEach(input => { input.addEventListener('input', (event) => { validateItemInput(event.target); updateItemAmount(event); updatePreview(); }); input.addEventListener('keydown', handleKeyNavigation); input.addEventListener('blur', (event) => { validateItemInput(event.target); }); }); // Add remove button listener row.querySelector('.remove-item').addEventListener('click', (_event) => { if (itemsTableBody.children.length > 1) { row.remove(); updateSerialNumbers(); updatePreview(); } else { // If it's the last row, just clear it instead of removing inputs.forEach(input => { if (input.type === 'number') { input.value = input.classList.contains('item-qty') ? '1' : '0'; } else { input.value = ''; } input.classList.remove('input-error', 'input-success'); }); row.querySelector('.item-amount').textContent = '0.00'; updatePreview(); } }); // Auto-calculate initial amount updateItemAmount({ target: row.querySelector('.item-qty') }); } function updateItemAmount(event) { const row = event.target.closest('tr'); if (!row) return; const qty = parseFloat(row.querySelector('.item-qty').value) || 0; const price = parseFloat(row.querySelector('.item-price').value) || 0; const discountRate = parseFloat(row.querySelector('.item-discount').value) || 0; const discountAmount = qty * price * discountRate / 100; const amount = qty * price - discountAmount; row.querySelector('.item-amount').textContent = amount.toFixed(2); // Add visual feedback for calculated amount const amountCell = row.querySelector('.item-amount'); amountCell.style.backgroundColor = amount > 0 ? '#f0fdf4' : '#fef2f2'; } // Enhanced Add Item button addItemBtn.addEventListener('click', () => { addItemRow(); updatePreview(); // Focus the new row's first input setTimeout(() => { const newRow = itemsTableBody.lastElementChild; newRow.querySelector('.item-desc').focus(); }, 50); }); // Add keyboard shortcut for adding items (Ctrl+I) document.addEventListener('keydown', (event) => { if (event.ctrlKey && event.key === 'i') { event.preventDefault(); addItemBtn.click(); } }); form.addEventListener('input', updatePreview); form.addEventListener('submit', function (event) { event.preventDefault(); // Validate that we have at least one item with description const hasValidItems = Array.from(itemsTableBody.querySelectorAll('tr')).some(row => { return row.querySelector('.item-desc').value.trim() !== ''; }); if (!hasValidItems) { alert('Please add at least one item with a description before generating the quotation.'); return; } const html = generateQuotationHTML(form); output.innerHTML = html; output.style.display = 'block'; document.getElementById('form-container').style.display = 'none'; document.getElementById('preview-container').style.display = 'none'; document.getElementById('top-header').style.display = 'none'; }); // Initial preview updatePreview(); }); } function calculateQuotation(items, igstRate, freightCharges) { const subtotal = items.reduce((sum, i) => sum + i.amount, 0); const igstAmount = (subtotal * igstRate) / 100; const total = subtotal + igstAmount + freightCharges; const totalBeforeRoundOff = total; const finalTotal = Math.round(totalBeforeRoundOff); const rOff = totalBeforeRoundOff - finalTotal; return { subtotal, igstAmount, total, rOff, finalTotal }; } // Export for testing (Node.js) if (typeof module !== 'undefined' && module.exports) { module.exports = { numberToWords, calculateQuotation }; }