Spaces:
Sleeping
Sleeping
Commit
·
063f5af
1
Parent(s):
9d38c41
add: serial number and linters
Browse files- .eslintrc.json +17 -0
- .gitignore +3 -0
- .htmlhintrc +10 -0
- .stylelintrc.json +7 -0
- README.md +39 -1
- index.html +70 -0
- package-lock.json +0 -0
- package.json +18 -0
- script.js +145 -0
- style.css +62 -0
- tests/test.js +33 -0
.eslintrc.json
ADDED
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"env": {
|
3 |
+
"browser": true,
|
4 |
+
"es2021": true,
|
5 |
+
"node": true
|
6 |
+
},
|
7 |
+
"extends": "eslint:recommended",
|
8 |
+
"parserOptions": {
|
9 |
+
"ecmaVersion": 12,
|
10 |
+
"sourceType": "module"
|
11 |
+
},
|
12 |
+
"rules": {
|
13 |
+
"indent": ["error", 2],
|
14 |
+
"quotes": ["error", "single"],
|
15 |
+
"semi": ["error", "always"]
|
16 |
+
}
|
17 |
+
}
|
.gitignore
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
.env*
|
2 |
+
!.env.example
|
3 |
+
node_modules
|
.htmlhintrc
ADDED
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"tagname-lowercase": true,
|
3 |
+
"attr-lowercase": true,
|
4 |
+
"attr-value-double-quotes": true,
|
5 |
+
"doctype-first": true,
|
6 |
+
"tag-pair": true,
|
7 |
+
"spec-char-escape": true,
|
8 |
+
"id-unique": true,
|
9 |
+
"src-not-empty": true
|
10 |
+
}
|
.stylelintrc.json
ADDED
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"extends": "stylelint-config-standard",
|
3 |
+
"rules": {
|
4 |
+
"rule-empty-line-before": null,
|
5 |
+
"no-descending-specificity": null
|
6 |
+
}
|
7 |
+
}
|
README.md
CHANGED
@@ -7,4 +7,42 @@ sdk: docker
|
|
7 |
pinned: false
|
8 |
---
|
9 |
|
10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
7 |
pinned: false
|
8 |
---
|
9 |
|
10 |
+
# Quotation Generator
|
11 |
+
|
12 |
+
This is a simple client-side application to generate quotations.
|
13 |
+
Fill in your company details, customer details, itemized list, and additional charges.
|
14 |
+
The application calculates totals and allows you to print or save the quotation as PDF.
|
15 |
+
|
16 |
+
## Usage
|
17 |
+
|
18 |
+
1. Open `index.html` in a web browser.
|
19 |
+
2. Enter your company and customer information.
|
20 |
+
3. Add item rows as needed (auto-generated S.No, description, HSN code, quantity, unit price, discount %).
|
21 |
+
4. Specify IGST rate and freight charges.
|
22 |
+
5. Fill in bank payment details.
|
23 |
+
6. Click **Generate Quotation**.
|
24 |
+
7. The formatted quotation appears with the total amount shown in words (Rupees and Paise, using Crore/Lakh notation) above the totals.
|
25 |
+
8. Click **Print / Save as PDF** to export.
|
26 |
+
|
27 |
+
## Running Tests
|
28 |
+
|
29 |
+
Ensure you have Node.js installed, then run:
|
30 |
+
```
|
31 |
+
node tests/test.js
|
32 |
+
```
|
33 |
+
|
34 |
+
## Linting
|
35 |
+
|
36 |
+
Install the linting tools:
|
37 |
+
```
|
38 |
+
npm install
|
39 |
+
```
|
40 |
+
|
41 |
+
To run all linters (HTMLHint, Stylelint, ESLint):
|
42 |
+
```
|
43 |
+
npm run lint
|
44 |
+
```
|
45 |
+
Or individually:
|
46 |
+
- `npm run lint:html`
|
47 |
+
- `npm run lint:css`
|
48 |
+
- `npm run lint:js`
|
index.html
ADDED
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<!DOCTYPE html>
|
2 |
+
<html lang="en">
|
3 |
+
<head>
|
4 |
+
<meta charset="UTF-8">
|
5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
6 |
+
<title>Quotation Generator</title>
|
7 |
+
<link rel="stylesheet" href="style.css">
|
8 |
+
</head>
|
9 |
+
<body>
|
10 |
+
<h1>Quotation Generator</h1>
|
11 |
+
<div id="form-container">
|
12 |
+
<form id="quotation-form">
|
13 |
+
<fieldset>
|
14 |
+
<legend>Your Company Details</legend>
|
15 |
+
<input type="text" id="company-name" name="company-name" placeholder="Company Name" required>
|
16 |
+
<textarea id="company-address" name="company-address" placeholder="Address" required></textarea>
|
17 |
+
<input type="text" id="company-phone" name="company-phone" placeholder="Phone">
|
18 |
+
<input type="email" id="company-email" name="company-email" placeholder="Email">
|
19 |
+
</fieldset>
|
20 |
+
<fieldset>
|
21 |
+
<legend>Customer Company Details</legend>
|
22 |
+
<input type="text" id="customer-name" name="customer-name" placeholder="Customer Name" required>
|
23 |
+
<textarea id="customer-address" name="customer-address" placeholder="Address" required></textarea>
|
24 |
+
<input type="text" id="customer-phone" name="customer-phone" placeholder="Phone">
|
25 |
+
<input type="email" id="customer-email" name="customer-email" placeholder="Email">
|
26 |
+
</fieldset>
|
27 |
+
<fieldset>
|
28 |
+
<legend>Quotation Details</legend>
|
29 |
+
<input type="text" id="quotation-number" name="quotation-number" placeholder="Quotation Number" required>
|
30 |
+
<input type="date" id="quotation-date" name="quotation-date" required>
|
31 |
+
</fieldset>
|
32 |
+
<fieldset>
|
33 |
+
<legend>Items</legend>
|
34 |
+
<table id="items-table">
|
35 |
+
<thead>
|
36 |
+
<tr>
|
37 |
+
<th>S.No</th>
|
38 |
+
<th>Description</th>
|
39 |
+
<th>HSN Code</th>
|
40 |
+
<th>Qty</th>
|
41 |
+
<th>Unit Price</th>
|
42 |
+
<th>Discount (%)</th>
|
43 |
+
<th>Amount</th>
|
44 |
+
<th></th>
|
45 |
+
</tr>
|
46 |
+
</thead>
|
47 |
+
<tbody>
|
48 |
+
</tbody>
|
49 |
+
</table>
|
50 |
+
<button type="button" id="add-item">Add Item</button>
|
51 |
+
</fieldset>
|
52 |
+
<fieldset>
|
53 |
+
<legend>Additional Charges</legend>
|
54 |
+
<label>IGST (%)<input type="number" id="igst-rate" name="igst-rate" value="0" min="0"></label>
|
55 |
+
<label>Freight Charges<input type="number" id="freight-charges" name="freight-charges" value="0" min="0"></label>
|
56 |
+
</fieldset>
|
57 |
+
<fieldset>
|
58 |
+
<legend>Bank Details</legend>
|
59 |
+
<input type="text" id="bank-name" name="bank-name" placeholder="Bank Name" required>
|
60 |
+
<input type="text" id="bank-account" name="bank-account" placeholder="Account Number" required>
|
61 |
+
<input type="text" id="bank-ifsc" name="bank-ifsc" placeholder="IFSC Code" required>
|
62 |
+
<input type="text" id="bank-branch" name="bank-branch" placeholder="Branch" required>
|
63 |
+
</fieldset>
|
64 |
+
<button type="submit">Generate Quotation</button>
|
65 |
+
</form>
|
66 |
+
</div>
|
67 |
+
<div id="quotation-output" style="display:none;"></div>
|
68 |
+
<script src="script.js"></script>
|
69 |
+
</body>
|
70 |
+
</html>
|
package-lock.json
ADDED
The diff for this file is too large to render.
See raw diff
|
|
package.json
ADDED
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"name": "quotation-generator",
|
3 |
+
"version": "1.0.0",
|
4 |
+
"description": "Client-side quotation generator with linting setup",
|
5 |
+
"scripts": {
|
6 |
+
"lint:js": "eslint script.js tests",
|
7 |
+
"lint:css": "stylelint '**/*.css'",
|
8 |
+
"lint:html": "htmlhint index.html",
|
9 |
+
"lint": "npm-run-all lint:html lint:css lint:js"
|
10 |
+
},
|
11 |
+
"devDependencies": {
|
12 |
+
"eslint": "^8.0.0",
|
13 |
+
"htmlhint": "^0.15.0",
|
14 |
+
"npm-run-all": "^4.1.5",
|
15 |
+
"stylelint": "^15.0.0",
|
16 |
+
"stylelint-config-standard": "^32.0.0"
|
17 |
+
}
|
18 |
+
}
|
script.js
ADDED
@@ -0,0 +1,145 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
// helper: convert number to Indian-notation words (Crore, Lakh, Thousand, Hundred)
|
2 |
+
function numberToWords(num) {
|
3 |
+
const small = ['', 'One', 'Two', 'Three', 'Four', 'Five', 'Six', 'Seven', 'Eight', 'Nine',
|
4 |
+
'Ten', 'Eleven', 'Twelve', 'Thirteen', 'Fourteen', 'Fifteen', 'Sixteen', 'Seventeen', 'Eighteen', 'Nineteen'];
|
5 |
+
const tens = ['', '', 'Twenty', 'Thirty', 'Forty', 'Fifty', 'Sixty', 'Seventy', 'Eighty', 'Ninety'];
|
6 |
+
function twoDigit(n) {
|
7 |
+
if (n < 20) return small[n];
|
8 |
+
return tens[Math.floor(n / 10)] + (n % 10 ? ' ' + small[n % 10] : '');
|
9 |
+
}
|
10 |
+
let words = '';
|
11 |
+
const crore = Math.floor(num / 10000000); num %= 10000000;
|
12 |
+
if (crore) words += twoDigit(crore) + ' Crore ';
|
13 |
+
const lakh = Math.floor(num / 100000); num %= 100000;
|
14 |
+
if (lakh) words += twoDigit(lakh) + ' Lakh ';
|
15 |
+
const thousand = Math.floor(num / 1000); num %= 1000;
|
16 |
+
if (thousand) words += twoDigit(thousand) + ' Thousand ';
|
17 |
+
const hundred = Math.floor(num / 100); num %= 100;
|
18 |
+
if (hundred) words += small[hundred] + ' Hundred ';
|
19 |
+
if (num) words += (words ? 'and ' : '') + twoDigit(num) + ' ';
|
20 |
+
return words.trim() || 'Zero';
|
21 |
+
}
|
22 |
+
|
23 |
+
if (typeof document !== 'undefined') {
|
24 |
+
document.addEventListener('DOMContentLoaded', function() {
|
25 |
+
const addItemBtn = document.getElementById('add-item');
|
26 |
+
const itemsTableBody = document.querySelector('#items-table tbody');
|
27 |
+
const form = document.getElementById('quotation-form');
|
28 |
+
const output = document.getElementById('quotation-output');
|
29 |
+
|
30 |
+
function updateSerialNumbers() {
|
31 |
+
itemsTableBody.querySelectorAll('tr').forEach((row, i) => {
|
32 |
+
row.querySelector('.item-slno').textContent = i + 1;
|
33 |
+
});
|
34 |
+
}
|
35 |
+
|
36 |
+
function addItemRow() {
|
37 |
+
const row = document.createElement('tr');
|
38 |
+
row.innerHTML = `
|
39 |
+
<td class="item-slno"></td>
|
40 |
+
<td><input type="text" class="item-desc" placeholder="Item Description" required></td>
|
41 |
+
<td><input type="text" class="item-hsn" placeholder="HSN Code"></td>
|
42 |
+
<td><input type="number" class="item-qty" value="1" min="0" required></td>
|
43 |
+
<td><input type="number" class="item-price" value="0" min="0" required></td>
|
44 |
+
<td><input type="number" class="item-discount" value="0" min="0" max="100" step="0.01" placeholder="Discount %"></td>
|
45 |
+
<td class="item-amount">0.00</td>
|
46 |
+
<td><button type="button" class="remove-item">Remove</button></td>
|
47 |
+
`;
|
48 |
+
itemsTableBody.appendChild(row);
|
49 |
+
updateSerialNumbers();
|
50 |
+
const inputs = row.querySelectorAll('input');
|
51 |
+
inputs.forEach(input => input.addEventListener('input', updateItemAmount));
|
52 |
+
row.querySelector('.remove-item').addEventListener('click', () => {
|
53 |
+
row.remove();
|
54 |
+
updateSerialNumbers();
|
55 |
+
});
|
56 |
+
}
|
57 |
+
|
58 |
+
function updateItemAmount(e) {
|
59 |
+
const row = e.target.closest('tr');
|
60 |
+
const qty = parseFloat(row.querySelector('.item-qty').value) || 0;
|
61 |
+
const price = parseFloat(row.querySelector('.item-price').value) || 0;
|
62 |
+
const discountRate = parseFloat(row.querySelector('.item-discount').value) || 0;
|
63 |
+
const discountAmount = qty * price * discountRate / 100;
|
64 |
+
const amount = qty * price - discountAmount;
|
65 |
+
row.querySelector('.item-amount').textContent = amount.toFixed(2);
|
66 |
+
}
|
67 |
+
|
68 |
+
addItemBtn.addEventListener('click', addItemRow);
|
69 |
+
|
70 |
+
form.addEventListener('submit', function(e) {
|
71 |
+
e.preventDefault();
|
72 |
+
const data = new FormData(form);
|
73 |
+
const company = {
|
74 |
+
name: data.get('company-name'),
|
75 |
+
address: data.get('company-address'),
|
76 |
+
phone: data.get('company-phone'),
|
77 |
+
email: data.get('company-email')
|
78 |
+
};
|
79 |
+
const customer = {
|
80 |
+
name: data.get('customer-name'),
|
81 |
+
address: data.get('customer-address'),
|
82 |
+
phone: data.get('customer-phone'),
|
83 |
+
email: data.get('customer-email')
|
84 |
+
};
|
85 |
+
const quotationNumber = data.get('quotation-number');
|
86 |
+
const quotationDate = data.get('quotation-date');
|
87 |
+
const igstRate = parseFloat(data.get('igst-rate')) || 0;
|
88 |
+
const freightCharges = parseFloat(data.get('freight-charges')) || 0;
|
89 |
+
const bank = {
|
90 |
+
name: data.get('bank-name'),
|
91 |
+
account: data.get('bank-account'),
|
92 |
+
ifsc: data.get('bank-ifsc'),
|
93 |
+
branch: data.get('bank-branch')
|
94 |
+
};
|
95 |
+
const items = [];
|
96 |
+
itemsTableBody.querySelectorAll('tr').forEach(row => {
|
97 |
+
items.push({
|
98 |
+
description: row.querySelector('.item-desc').value,
|
99 |
+
hsn: row.querySelector('.item-hsn').value,
|
100 |
+
qty: parseFloat(row.querySelector('.item-qty').value) || 0,
|
101 |
+
price: parseFloat(row.querySelector('.item-price').value) || 0,
|
102 |
+
discount: parseFloat(row.querySelector('.item-discount').value) || 0,
|
103 |
+
amount: parseFloat(row.querySelector('.item-amount').textContent) || 0
|
104 |
+
});
|
105 |
+
});
|
106 |
+
const subtotal = items.reduce((sum, i) => sum + i.amount, 0);
|
107 |
+
const igstAmount = (subtotal * igstRate) / 100;
|
108 |
+
const total = subtotal + igstAmount + freightCharges;
|
109 |
+
// convert total to words (Rupees and Paise)
|
110 |
+
const rupeePart = Math.floor(total);
|
111 |
+
const paisePart = Math.round((total - rupeePart) * 100);
|
112 |
+
const rupeeWords = numberToWords(rupeePart);
|
113 |
+
const paiseWords = paisePart > 0 ? numberToWords(paisePart) : '';
|
114 |
+
let html = '<div class="quotation-print">';
|
115 |
+
html += `<div class="section"><strong>From:</strong><br>${company.name}<br>${company.address.replace(/\n/g,'<br>')}<br>Phone: ${company.phone}<br>Email: ${company.email}</div>`;
|
116 |
+
html += `<div class="section"><strong>To:</strong><br>${customer.name}<br>${customer.address.replace(/\n/g,'<br>')}<br>Phone: ${customer.phone}<br>Email: ${customer.email}</div>`;
|
117 |
+
html += `<div class="section quote-meta">Quotation No: ${quotationNumber}<br>Date: ${quotationDate}</div>`;
|
118 |
+
html += '<table class="quotation-table"><thead><tr><th>S.No</th><th>Description</th><th>HSN</th><th>Qty</th><th>Unit Price</th><th>Discount (%)</th><th>Amount</th></tr></thead><tbody>';
|
119 |
+
items.forEach((item, idx) => {
|
120 |
+
html += `<tr><td>${idx + 1}</td><td>${item.description}</td><td>${item.hsn}</td><td>${item.qty}</td><td>${item.price.toFixed(2)}</td><td>${item.discount.toFixed(2)}%</td><td>${item.amount.toFixed(2)}</td></tr>`;
|
121 |
+
});
|
122 |
+
html += '</tbody></table>';
|
123 |
+
html += `<div class="section amount-words"><strong>Amount in Words:</strong> Rupees ${rupeeWords}${paiseWords ? ' and ' + paiseWords + ' Paise' : ''} only</div>`;
|
124 |
+
html += '<div class="totals"><table>';
|
125 |
+
html += `<tr><td>Subtotal:</td><td>${subtotal.toFixed(2)}</td></tr>`;
|
126 |
+
html += `<tr><td>IGST (${igstRate}%):</td><td>${igstAmount.toFixed(2)}</td></tr>`;
|
127 |
+
html += `<tr><td>Freight:</td><td>${freightCharges.toFixed(2)}</td></tr>`;
|
128 |
+
html += `<tr><td><strong>Total:</strong></td><td><strong>${total.toFixed(2)}</strong></td></tr>`;
|
129 |
+
html += '</table></div>';
|
130 |
+
html += `<div class="section"><strong>Bank Details:</strong><br>Bank: ${bank.name}<br>Account: ${bank.account}<br>IFSC: ${bank.ifsc}<br>Branch: ${bank.branch}</div>`;
|
131 |
+
html += '<p class="disclaimer">This quotation is not a contract or a bill. It is our best guess at the total price for the service and goods described above. The customer will be billed after indicating acceptance of this quote. Payment will be due prior to the delivery of service and goods. Please fax or mail the signed quote to the address listed above.</p>';
|
132 |
+
html += '<button onclick="window.print()">Print / Save as PDF</button>';
|
133 |
+
html += '</div>';
|
134 |
+
output.innerHTML = html;
|
135 |
+
output.style.display = 'block';
|
136 |
+
document.getElementById('form-container').style.display = 'none';
|
137 |
+
addItemRow();
|
138 |
+
});
|
139 |
+
});
|
140 |
+
}
|
141 |
+
|
142 |
+
// Export for testing (Node.js)
|
143 |
+
if (typeof module !== 'undefined' && module.exports) {
|
144 |
+
module.exports = { numberToWords };
|
145 |
+
}
|
style.css
ADDED
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
body {
|
2 |
+
font-family: Arial, sans-serif;
|
3 |
+
margin: 20px;
|
4 |
+
}
|
5 |
+
fieldset {
|
6 |
+
margin-bottom: 15px;
|
7 |
+
border: 1px solid #ccc;
|
8 |
+
padding: 10px;
|
9 |
+
}
|
10 |
+
legend {
|
11 |
+
font-weight: bold;
|
12 |
+
}
|
13 |
+
input, textarea {
|
14 |
+
width: 100%;
|
15 |
+
padding: 5px;
|
16 |
+
margin: 5px 0;
|
17 |
+
box-sizing: border-box;
|
18 |
+
}
|
19 |
+
#items-table, .quotation-table {
|
20 |
+
width: 100%;
|
21 |
+
border-collapse: collapse;
|
22 |
+
margin-bottom: 10px;
|
23 |
+
}
|
24 |
+
#items-table th, #items-table td,
|
25 |
+
.quotation-table th, .quotation-table td {
|
26 |
+
border: 1px solid #ccc;
|
27 |
+
padding: 5px;
|
28 |
+
text-align: left;
|
29 |
+
}
|
30 |
+
button {
|
31 |
+
padding: 8px 12px;
|
32 |
+
margin: 5px;
|
33 |
+
}
|
34 |
+
.quotation-print {
|
35 |
+
max-width: 800px;
|
36 |
+
margin: auto;
|
37 |
+
}
|
38 |
+
.quotation-print h2 {
|
39 |
+
text-align: center;
|
40 |
+
margin-bottom: 20px;
|
41 |
+
}
|
42 |
+
.quotation-print .section {
|
43 |
+
margin-bottom: 15px;
|
44 |
+
}
|
45 |
+
.totals table {
|
46 |
+
float: right;
|
47 |
+
border: none;
|
48 |
+
}
|
49 |
+
.totals td {
|
50 |
+
padding: 4px 8px;
|
51 |
+
border: none;
|
52 |
+
}
|
53 |
+
|
54 |
+
.disclaimer {
|
55 |
+
font-size: 0.9em;
|
56 |
+
color: #555;
|
57 |
+
margin-top: 20px;
|
58 |
+
}
|
59 |
+
|
60 |
+
.quote-meta {
|
61 |
+
text-align: right;
|
62 |
+
}
|
tests/test.js
ADDED
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
const assert = require('assert');
|
2 |
+
const { numberToWords } = require('../script.js');
|
3 |
+
|
4 |
+
// Test cases for Indian number-to-words conversion (Crore/Lakh system)
|
5 |
+
const cases = [
|
6 |
+
{ num: 0, expected: 'Zero' },
|
7 |
+
{ num: 5, expected: 'Five' },
|
8 |
+
{ num: 15, expected: 'Fifteen' },
|
9 |
+
{ num: 75, expected: 'Seventy Five' },
|
10 |
+
{ num: 100, expected: 'One Hundred' },
|
11 |
+
{ num: 569, expected: 'Five Hundred and Sixty Nine' },
|
12 |
+
{ num: 1000, expected: 'One Thousand' },
|
13 |
+
{ num: 1100, expected: 'One Thousand One Hundred' },
|
14 |
+
{ num: 1234, expected: 'One Thousand Two Hundred and Thirty Four' },
|
15 |
+
{ num: 10000, expected: 'Ten Thousand' },
|
16 |
+
{ num: 54000, expected: 'Fifty Four Thousand' },
|
17 |
+
{ num: 100000, expected: 'One Lakh' },
|
18 |
+
{ num: 510000, expected: 'Five Lakh Ten Thousand' },
|
19 |
+
{ num: 9999999, expected: 'Ninety Nine Lakh Ninety Nine Thousand Nine Hundred and Ninety Nine' },
|
20 |
+
{ num: 10000000, expected: 'One Crore' },
|
21 |
+
{ num: 12500000, expected: 'One Crore Twenty Five Lakh' },
|
22 |
+
];
|
23 |
+
|
24 |
+
cases.forEach(({ num, expected }) => {
|
25 |
+
const actual = numberToWords(num);
|
26 |
+
assert.strictEqual(
|
27 |
+
actual,
|
28 |
+
expected,
|
29 |
+
`${num} => "${actual}" (expected "${expected}")`
|
30 |
+
);
|
31 |
+
});
|
32 |
+
|
33 |
+
console.log('✅ All numberToWords tests passed');
|