// 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 = `
CUSTOMER INFO
${customer.name}
{{cutomer_address}}
GST NO. : ${customer.gstin || ""}
CONTACT NO : ${customer.phone} ${customer.email}
| SL NO |
DESCRIPTION |
HSN CODE |
QTY |
UNIT PRICE |
DISCOUNT |
AMOUNT |
`;
items.forEach((item, idx) => {
html += `
| ${idx + 1} |
${item.description} |
${item.hsn} |
${item.qty} |
${item.price.toFixed(2)} |
${item.discount.toFixed(2)}% |
${item.amount.toFixed(2)} |
`;
});
// Add empty rows to fill the page
for (let i = items.length; i < 7; i++) {
html +=
"| | | | | | | |
";
}
html += `
`;
html = html.replace(
"{{address}}",
company.address.replace(/\n/g, "
"),
);
html = html.replace(
"{{cutomer_address}}",
customer.address.replace(/\n/g, "
"),
);
return html;
}
function updatePreview() {
const form = document.getElementById("quotation-form");
const html = generateQuotationHTML(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 = `
`;
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");
});
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 };
}