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