// 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 itemsContainer = document.getElementById("items-container"); 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 = []; itemsContainer.querySelectorAll(".item-card").forEach((card) => { const desc = card.querySelector(".item-desc").value.trim(); if (desc) { // Only include cards with description items.push({ description: desc, hsn: card.querySelector(".item-hsn").value, qty: parseFloat(card.querySelector(".item-qty").value) || 0, price: parseFloat( card.querySelector(".item-price").value, ) || 0, discount: parseFloat( card.querySelector(".item-discount").value, ) || 0, amount: parseFloat( card .querySelector(".item-amount") .textContent.replace("₹", ""), ) || 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 < 20; 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 generateQuotationHTMLForPreview(form) { const html = generateQuotationHTML(form); // Remove print button from preview return html.replace( /]*onclick="window\.print\(\)"[^>]*>.*?<\/button>/s, "", ); } function updatePreview() { const form = document.getElementById("quotation-form"); const html = generateQuotationHTMLForPreview(form); previewContent.innerHTML = html; } function updateSerialNumbers() { itemsContainer.querySelectorAll(".item-card").forEach((card, i) => { card.querySelector(".item-slno").textContent = i + 1; }); } function validateItemInput(input) { const value = input.value; // Remove existing classes input.classList.remove("border-red-500", "border-green-500"); // Remove loading effect setTimeout(() => { if (input.classList.contains("item-desc")) { if (value.trim().length < 3) { input.classList.add("border-red-500"); // Removed tooltip error display } else { input.classList.add("border-green-500"); // Removed tooltip error display } } else if (input.type === "number") { const numValue = parseFloat(value); if (isNaN(numValue) || numValue < 0) { input.classList.add("border-red-500"); // Removed tooltip error display } else { input.classList.add("border-green-500"); // Removed tooltip error display } } }, 50); } function handleKeyNavigation(event) { if (event.key === "Tab" || event.key === "Enter") { const currentCard = event.target.closest(".item-card"); const inputs = Array.from( currentCard.querySelectorAll("input, textarea"), ); const currentIndex = inputs.indexOf(event.target); if (event.key === "Enter") { event.preventDefault(); // If we're at the last input in the card and it's the last card, add a new card if (currentIndex === inputs.length - 1) { const allCards = Array.from( itemsContainer.querySelectorAll(".item-card"), ); const currentCardIndex = allCards.indexOf(currentCard); if (currentCardIndex === allCards.length - 1) { // Last card - add new card and focus first input addItemRow(); setTimeout(() => { const newCard = itemsContainer.lastElementChild; newCard.querySelector(".item-desc").focus(); }, 50); } else { // Not last card - focus next card's first input const nextCard = allCards[currentCardIndex + 1]; nextCard.querySelector(".item-desc").focus(); } } else { // Move to next input in same card inputs[currentIndex + 1].focus(); } } } } function addItemRow() { const card = document.createElement("div"); card.className = "item-card bg-white border border-gray-200 rounded-lg p-4 shadow-sm"; card.innerHTML = `
Item Details
%
Total Amount ₹0.00
`; itemsContainer.appendChild(card); updateSerialNumbers(); // Add event listeners to inputs const inputs = card.querySelectorAll("input, textarea"); 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 card.querySelector(".remove-item").addEventListener( "click", (_event) => { if (itemsContainer.children.length > 1) { card.remove(); updateSerialNumbers(); updatePreview(); } else { // If it's the last card, 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( "border-red-500", "border-green-500", ); }); card.querySelector(".item-amount").textContent = "₹0.00"; updatePreview(); } }, ); // Auto-calculate initial amount updateItemAmount({ target: card.querySelector(".item-qty") }); } function updateItemAmount(event) { const card = event.target.closest(".item-card"); if (!card) return; const qty = parseFloat(card.querySelector(".item-qty").value) || 0; const price = parseFloat(card.querySelector(".item-price").value) || 0; const discountRate = parseFloat(card.querySelector(".item-discount").value) || 0; const discountAmount = (qty * price * discountRate) / 100; const amount = qty * price - discountAmount; card.querySelector(".item-amount").textContent = `₹${amount.toFixed(2)}`; // Remove complex visual feedback } // Add Item button addItemBtn.addEventListener("click", () => { addItemRow(); updatePreview(); // Focus the new card's first input const newCard = itemsContainer.lastElementChild; newCard.scrollIntoView({ behavior: "smooth", block: "center" }); setTimeout(() => { newCard.querySelector(".item-desc").focus(); }, 100); }); // 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( itemsContainer.querySelectorAll(".item-card"), ).some((card) => { return card.querySelector(".item-desc").value.trim() !== ""; }); if (!hasValidItems) { alert( "Please add at least one item with a description before generating the quotation.", ); return; } const quotationContent = document.getElementById("quotation-content"); const html = generateQuotationHTML(form); quotationContent.innerHTML = html; output.classList.remove("hidden"); // Add print-only class to modal for better print isolation output.classList.add("print-modal"); }); document .getElementById("close-quotation") .addEventListener("click", () => { output.classList.add("hidden"); }); // Enhanced keyboard shortcuts document.addEventListener("keydown", (event) => { if (event.ctrlKey || event.metaKey) { if (event.key === "i") { event.preventDefault(); addItemBtn.click(); } } }); // Auto-save functionality let saveTimeout; form.addEventListener("input", () => { clearTimeout(saveTimeout); saveTimeout = setTimeout(() => { // Auto-save form data (existing functionality) updatePreview(); }, 500); }); // 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 }; }