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