// 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 }; }