anuragshas commited on
Commit
0ea575d
·
1 Parent(s): 0439da7

fix: indentation issue

Browse files
Files changed (5) hide show
  1. eslint.config.mjs +30 -25
  2. index.html +94 -46
  3. script.js +322 -265
  4. style.css +191 -191
  5. tests/test.js +94 -64
eslint.config.mjs CHANGED
@@ -10,35 +10,40 @@ const __dirname = path.dirname(__filename);
10
  const compat = new FlatCompat({
11
  baseDirectory: __dirname,
12
  recommendedConfig: js.configs.recommended,
13
- allConfig: js.configs.all
14
  });
15
 
16
- export default defineConfig([{
17
- extends: compat.extends("eslint:recommended"),
 
18
 
19
- languageOptions: {
20
- globals: {
21
- ...globals.browser,
22
- ...globals.node,
23
- },
24
 
25
- ecmaVersion: 12,
26
- sourceType: "module",
27
- },
28
 
29
- rules: {
30
- indent: ["error", 2],
31
- quotes: ["error", "single"],
32
- semi: ["error", "always"],
33
 
34
- "no-unused-vars": ["error", {
35
- args: "all",
36
- argsIgnorePattern: "^_",
37
- caughtErrors: "all",
38
- caughtErrorsIgnorePattern: "^_",
39
- destructuredArrayIgnorePattern: "^_",
40
- varsIgnorePattern: "^_",
41
- ignoreRestSiblings: true,
42
- }],
 
 
 
 
43
  },
44
- }]);
 
10
  const compat = new FlatCompat({
11
  baseDirectory: __dirname,
12
  recommendedConfig: js.configs.recommended,
13
+ allConfig: js.configs.all,
14
  });
15
 
16
+ export default defineConfig([
17
+ {
18
+ extends: compat.extends("eslint:recommended"),
19
 
20
+ languageOptions: {
21
+ globals: {
22
+ ...globals.browser,
23
+ ...globals.node,
24
+ },
25
 
26
+ ecmaVersion: 12,
27
+ sourceType: "module",
28
+ },
29
 
30
+ rules: {
31
+ indent: ["error", 4],
32
+ quotes: ["error", "double"],
33
+ semi: ["error", "always"],
34
 
35
+ "no-unused-vars": [
36
+ "error",
37
+ {
38
+ args: "all",
39
+ argsIgnorePattern: "^_",
40
+ caughtErrors: "all",
41
+ caughtErrorsIgnorePattern: "^_",
42
+ destructuredArrayIgnorePattern: "^_",
43
+ varsIgnorePattern: "^_",
44
+ ignoreRestSiblings: true,
45
+ },
46
+ ],
47
+ },
48
  },
49
+ ]);
index.html CHANGED
@@ -1,111 +1,159 @@
1
- <!DOCTYPE html>
2
  <html lang="en">
3
 
4
  <head>
5
- <meta charset="UTF-8">
6
- <meta name="viewport" content="width=device-width, initial-scale=1">
7
  <title>Quotation Generator</title>
8
- <link rel="stylesheet" href="style.css">
9
  </head>
10
 
11
  <body class="bg-gray-100">
12
- <h1 class="text-3xl font-bold text-center my-8" id="top-header">Quotation Generator</h1>
 
 
13
  <div class="flex container mx-auto">
14
  <div id="form-container" class="w-1/2 pr-4">
15
  <form id="quotation-form" class="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4">
16
  <fieldset class="border border-gray-300 p-4 mb-4">
17
- <legend class="text-lg font-semibold mb-2">Your Company Details</legend>
 
 
18
  <input type="text" id="company-name" name="company-name" placeholder="Company Name" required
19
- class="w-full p-2 border border-gray-300 rounded mb-2">
20
  <textarea id="company-address" name="company-address" placeholder="Address" required
21
  class="w-full p-2 border border-gray-300 rounded mb-2"></textarea>
22
  <input type="text" id="company-phone" name="company-phone" placeholder="Phone"
23
- class="w-full p-2 border border-gray-300 rounded mb-2">
24
  <input type="email" id="company-email" name="company-email" placeholder="Email"
25
- class="w-full p-2 border border-gray-300 rounded mb-2">
26
  <input type="text" id="company-gstin" name="company-gstin" placeholder="GSTIN"
27
- class="w-full p-2 border border-gray-300 rounded">
28
  </fieldset>
29
  <fieldset class="border border-gray-300 p-4 mb-4">
30
- <legend class="text-lg font-semibold mb-2">Customer Company Details</legend>
 
 
31
  <input type="text" id="customer-name" name="customer-name" placeholder="Customer Name" required
32
- class="w-full p-2 border border-gray-300 rounded mb-2">
33
  <textarea id="customer-address" name="customer-address" placeholder="Address" required
34
  class="w-full p-2 border border-gray-300 rounded mb-2"></textarea>
35
  <input type="text" id="customer-phone" name="customer-phone" placeholder="Phone"
36
- class="w-full p-2 border border-gray-300 rounded mb-2">
37
  <input type="email" id="customer-email" name="customer-email" placeholder="Email"
38
- class="w-full p-2 border border-gray-300 rounded mb-2">
39
  <input type="text" id="customer-gstin" name="customer-gstin" placeholder="GSTIN"
40
- class="w-full p-2 border border-gray-300 rounded">
41
  </fieldset>
42
  <fieldset class="border border-gray-300 p-4 mb-4">
43
- <legend class="text-lg font-semibold mb-2">Quotation Details</legend>
 
 
44
  <input type="text" id="quotation-number" name="quotation-number" placeholder="Quotation Number" required
45
- class="w-full p-2 border border-gray-300 rounded mb-2">
46
  <input type="date" id="quotation-date" name="quotation-date" required
47
- class="w-full p-2 border border-gray-300 rounded">
48
  </fieldset>
49
  <fieldset class="border border-gray-300 p-4 mb-4">
50
- <legend class="text-lg font-semibold mb-2">Items</legend>
 
 
51
  <div class="bg-blue-50 border border-blue-200 rounded p-3 mb-4 text-sm text-blue-800">
52
  <strong>💡 Tips:</strong>
53
  <ul class="list-disc list-inside mt-1 space-y-1">
54
- <li>Use <kbd class="bg-gray-200 px-1 rounded">Enter</kbd> to move to the next field or add a new row</li>
55
- <li>Use <kbd class="bg-gray-200 px-1 rounded">Ctrl+I</kbd> to quickly add a new item</li>
56
- <li>Amounts are calculated automatically as you type</li>
57
- <li>At least one item with description is required</li>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
58
  </ul>
59
  </div>
60
  <table id="items-table" class="w-full mb-2">
61
  <thead>
62
  <tr class="bg-gray-200">
63
- <th class="p-2 border border-gray-300">S.No</th>
64
- <th class="p-2 border border-gray-300">Description *</th>
65
- <th class="p-2 border border-gray-300">HSN Code</th>
66
- <th class="p-2 border border-gray-300">Qty *</th>
67
- <th class="p-2 border border-gray-300">Unit Price *</th>
68
- <th class="p-2 border border-gray-300">Discount (%)</th>
69
- <th class="p-2 border border-gray-300">Amount</th>
70
- <th class="p-2 border border-gray-300">Action</th>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71
  </tr>
72
  </thead>
73
- <tbody>
74
- </tbody>
75
  </table>
76
  <button type="button" id="add-item"
77
- class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">Add Another Item</button>
 
 
78
  </fieldset>
79
  <fieldset class="border border-gray-300 p-4 mb-4">
80
- <legend class="text-lg font-semibold mb-2">Additional Charges</legend>
 
 
81
  <label class="block mb-2">IGST (%)<input type="number" id="igst-rate" name="igst-rate" value="0" min="0"
82
- class="w-full p-2 border border-gray-300 rounded"></label>
83
  <label class="block">Freight Charges<input type="number" id="freight-charges" name="freight-charges" value="0"
84
- min="0" class="w-full p-2 border border-gray-300 rounded"></label>
85
  </fieldset>
86
  <fieldset class="border border-gray-300 p-4 mb-4">
87
- <legend class="text-lg font-semibold mb-2">Bank Details</legend>
 
 
88
  <input type="text" id="bank-name" name="bank-name" placeholder="Bank Name" required
89
- class="w-full p-2 border border-gray-300 rounded mb-2">
90
  <input type="text" id="bank-account" name="bank-account" placeholder="Account Number" required
91
- class="w-full p-2 border border-gray-300 rounded mb-2">
92
  <input type="text" id="bank-ifsc" name="bank-ifsc" placeholder="IFSC Code" required
93
- class="w-full p-2 border border-gray-300 rounded mb-2">
94
  <input type="text" id="bank-branch" name="bank-branch" placeholder="Branch" required
95
- class="w-full p-2 border border-gray-300 rounded">
96
  </fieldset>
97
- <button type="submit" class="bg-green-500 hover:bg-green-700 text-white font-bold py-2 px-4 rounded">Generate
98
- Quotation</button>
 
99
  </form>
100
  </div>
101
  <div id="preview-container" class="w-1/2 pl-4">
102
  <div id="quotation-preview" class="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4">
103
- <h2 class="text-2xl font-bold text-center mb-4">Quotation Preview</h2>
 
 
104
  <div id="preview-content"></div>
105
  </div>
106
  </div>
107
  </div>
108
- <div id="quotation-output" style="display:none;"></div>
109
  <script src="script.js"></script>
110
  </body>
111
 
 
1
+ <!doctype html>
2
  <html lang="en">
3
 
4
  <head>
5
+ <meta charset="UTF-8" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
7
  <title>Quotation Generator</title>
8
+ <link rel="stylesheet" href="style.css" />
9
  </head>
10
 
11
  <body class="bg-gray-100">
12
+ <h1 class="text-3xl font-bold text-center my-8" id="top-header">
13
+ Quotation Generator
14
+ </h1>
15
  <div class="flex container mx-auto">
16
  <div id="form-container" class="w-1/2 pr-4">
17
  <form id="quotation-form" class="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4">
18
  <fieldset class="border border-gray-300 p-4 mb-4">
19
+ <legend class="text-lg font-semibold mb-2">
20
+ Your Company Details
21
+ </legend>
22
  <input type="text" id="company-name" name="company-name" placeholder="Company Name" required
23
+ class="w-full p-2 border border-gray-300 rounded mb-2" />
24
  <textarea id="company-address" name="company-address" placeholder="Address" required
25
  class="w-full p-2 border border-gray-300 rounded mb-2"></textarea>
26
  <input type="text" id="company-phone" name="company-phone" placeholder="Phone"
27
+ class="w-full p-2 border border-gray-300 rounded mb-2" />
28
  <input type="email" id="company-email" name="company-email" placeholder="Email"
29
+ class="w-full p-2 border border-gray-300 rounded mb-2" />
30
  <input type="text" id="company-gstin" name="company-gstin" placeholder="GSTIN"
31
+ class="w-full p-2 border border-gray-300 rounded" />
32
  </fieldset>
33
  <fieldset class="border border-gray-300 p-4 mb-4">
34
+ <legend class="text-lg font-semibold mb-2">
35
+ Customer Company Details
36
+ </legend>
37
  <input type="text" id="customer-name" name="customer-name" placeholder="Customer Name" required
38
+ class="w-full p-2 border border-gray-300 rounded mb-2" />
39
  <textarea id="customer-address" name="customer-address" placeholder="Address" required
40
  class="w-full p-2 border border-gray-300 rounded mb-2"></textarea>
41
  <input type="text" id="customer-phone" name="customer-phone" placeholder="Phone"
42
+ class="w-full p-2 border border-gray-300 rounded mb-2" />
43
  <input type="email" id="customer-email" name="customer-email" placeholder="Email"
44
+ class="w-full p-2 border border-gray-300 rounded mb-2" />
45
  <input type="text" id="customer-gstin" name="customer-gstin" placeholder="GSTIN"
46
+ class="w-full p-2 border border-gray-300 rounded" />
47
  </fieldset>
48
  <fieldset class="border border-gray-300 p-4 mb-4">
49
+ <legend class="text-lg font-semibold mb-2">
50
+ Quotation Details
51
+ </legend>
52
  <input type="text" id="quotation-number" name="quotation-number" placeholder="Quotation Number" required
53
+ class="w-full p-2 border border-gray-300 rounded mb-2" />
54
  <input type="date" id="quotation-date" name="quotation-date" required
55
+ class="w-full p-2 border border-gray-300 rounded" />
56
  </fieldset>
57
  <fieldset class="border border-gray-300 p-4 mb-4">
58
+ <legend class="text-lg font-semibold mb-2">
59
+ Items
60
+ </legend>
61
  <div class="bg-blue-50 border border-blue-200 rounded p-3 mb-4 text-sm text-blue-800">
62
  <strong>💡 Tips:</strong>
63
  <ul class="list-disc list-inside mt-1 space-y-1">
64
+ <li>
65
+ Use
66
+ <kbd class="bg-gray-200 px-1 rounded">Enter</kbd>
67
+ to move to the next field or add a new row
68
+ </li>
69
+ <li>
70
+ Use
71
+ <kbd class="bg-gray-200 px-1 rounded">Ctrl+I</kbd>
72
+ to quickly add a new item
73
+ </li>
74
+ <li>
75
+ Amounts are calculated automatically as you
76
+ type
77
+ </li>
78
+ <li>
79
+ At least one item with description is
80
+ required
81
+ </li>
82
  </ul>
83
  </div>
84
  <table id="items-table" class="w-full mb-2">
85
  <thead>
86
  <tr class="bg-gray-200">
87
+ <th class="p-2 border border-gray-300">
88
+ S.No
89
+ </th>
90
+ <th class="p-2 border border-gray-300">
91
+ Description *
92
+ </th>
93
+ <th class="p-2 border border-gray-300">
94
+ HSN Code
95
+ </th>
96
+ <th class="p-2 border border-gray-300">
97
+ Qty *
98
+ </th>
99
+ <th class="p-2 border border-gray-300">
100
+ Unit Price *
101
+ </th>
102
+ <th class="p-2 border border-gray-300">
103
+ Discount (%)
104
+ </th>
105
+ <th class="p-2 border border-gray-300">
106
+ Amount
107
+ </th>
108
+ <th class="p-2 border border-gray-300">
109
+ Action
110
+ </th>
111
  </tr>
112
  </thead>
113
+ <tbody></tbody>
 
114
  </table>
115
  <button type="button" id="add-item"
116
+ class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
117
+ Add Another Item
118
+ </button>
119
  </fieldset>
120
  <fieldset class="border border-gray-300 p-4 mb-4">
121
+ <legend class="text-lg font-semibold mb-2">
122
+ Additional Charges
123
+ </legend>
124
  <label class="block mb-2">IGST (%)<input type="number" id="igst-rate" name="igst-rate" value="0" min="0"
125
+ class="w-full p-2 border border-gray-300 rounded" /></label>
126
  <label class="block">Freight Charges<input type="number" id="freight-charges" name="freight-charges" value="0"
127
+ min="0" class="w-full p-2 border border-gray-300 rounded" /></label>
128
  </fieldset>
129
  <fieldset class="border border-gray-300 p-4 mb-4">
130
+ <legend class="text-lg font-semibold mb-2">
131
+ Bank Details
132
+ </legend>
133
  <input type="text" id="bank-name" name="bank-name" placeholder="Bank Name" required
134
+ class="w-full p-2 border border-gray-300 rounded mb-2" />
135
  <input type="text" id="bank-account" name="bank-account" placeholder="Account Number" required
136
+ class="w-full p-2 border border-gray-300 rounded mb-2" />
137
  <input type="text" id="bank-ifsc" name="bank-ifsc" placeholder="IFSC Code" required
138
+ class="w-full p-2 border border-gray-300 rounded mb-2" />
139
  <input type="text" id="bank-branch" name="bank-branch" placeholder="Branch" required
140
+ class="w-full p-2 border border-gray-300 rounded" />
141
  </fieldset>
142
+ <button type="submit" class="bg-green-500 hover:bg-green-700 text-white font-bold py-2 px-4 rounded">
143
+ Generate Quotation
144
+ </button>
145
  </form>
146
  </div>
147
  <div id="preview-container" class="w-1/2 pl-4">
148
  <div id="quotation-preview" class="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4">
149
+ <h2 class="text-2xl font-bold text-center mb-4">
150
+ Quotation Preview
151
+ </h2>
152
  <div id="preview-content"></div>
153
  </div>
154
  </div>
155
  </div>
156
+ <div id="quotation-output" style="display: none"></div>
157
  <script src="script.js"></script>
158
  </body>
159
 
script.js CHANGED
@@ -1,96 +1,137 @@
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
- const previewContent = document.getElementById('preview-content');
30
-
31
- // Add initial empty row
32
- addItemRow();
33
-
34
- function generateQuotationHTML(form) {
35
- const data = new FormData(form);
36
- const company = {
37
- name: data.get('company-name'),
38
- address: data.get('company-address'),
39
- phone: data.get('company-phone'),
40
- email: data.get('company-email'),
41
- gstin: data.get('company-gstin')
42
- };
43
- const customer = {
44
- name: data.get('customer-name'),
45
- address: data.get('customer-address'),
46
- phone: data.get('customer-phone'),
47
- email: data.get('customer-email'),
48
- gstin: data.get('customer-gstin')
49
- };
50
- const quotationNumber = data.get('quotation-number');
51
- const quotationDate = data.get('quotation-date');
52
- const igstRate = parseFloat(data.get('igst-rate')) || 0;
53
- const freightCharges = parseFloat(data.get('freight-charges')) || 0;
54
- const bank = {
55
- name: data.get('bank-name'),
56
- account: data.get('bank-account'),
57
- ifsc: data.get('bank-ifsc'),
58
- branch: data.get('bank-branch')
59
- };
60
- const items = [];
61
- itemsTableBody.querySelectorAll('tr').forEach(row => {
62
- const desc = row.querySelector('.item-desc').value.trim();
63
- if (desc) { // Only include rows with description
64
- items.push({
65
- description: desc,
66
- hsn: row.querySelector('.item-hsn').value,
67
- qty: parseFloat(row.querySelector('.item-qty').value) || 0,
68
- price: parseFloat(row.querySelector('.item-price').value) || 0,
69
- discount: parseFloat(row.querySelector('.item-discount').value) || 0,
70
- amount: parseFloat(row.querySelector('.item-amount').textContent) || 0
71
- });
72
- }
73
- });
74
-
75
- if (items.length === 0) {
76
- return '<div class="text-center text-gray-500 p-8">Please add items to generate quotation</div>';
77
- }
 
 
 
 
 
78
 
79
- const { subtotal, igstAmount, _total, rOff, finalTotal } = calculateQuotation(items, igstRate, freightCharges);
 
80
 
81
- // convert total to words (Rupees and Paise)
82
- const rupeePart = Math.floor(finalTotal);
83
- const paisePart = Math.round((finalTotal - rupeePart) * 100);
84
- const rupeeWords = numberToWords(rupeePart);
85
- const paiseWords = paisePart > 0 ? numberToWords(paisePart) : '';
86
 
87
- let html = `
88
  <div class="quotation-print">
89
  <div class="header">
90
  <div class="company-details">
91
  <h1>${company.name}</h1>
92
  {{address}}<br>
93
- GST NO. : ${company.gstin || ''}<br>
94
  CONTACT NO : ${company.phone} ${company.email}
95
  </div>
96
  <div class="quotation-title">
@@ -112,7 +153,7 @@ if (typeof document !== 'undefined') {
112
  <strong>CUSTOMER INFO</strong><br>
113
  ${customer.name}<br>
114
  {{cutomer_address}}<br>
115
- GST NO. : ${customer.gstin || ''}<br>
116
  CONTACT NO : ${customer.phone} ${customer.email}
117
  </div>
118
 
@@ -130,8 +171,8 @@ if (typeof document !== 'undefined') {
130
  </thead>
131
  <tbody>
132
  `;
133
- items.forEach((item, idx) => {
134
- html += `
135
  <tr>
136
  <td>${idx + 1}</td>
137
  <td>${item.description}</td>
@@ -142,13 +183,14 @@ if (typeof document !== 'undefined') {
142
  <td>${item.amount.toFixed(2)}</td>
143
  </tr>
144
  `;
145
- });
146
- // Add empty rows to fill the page
147
- for (let i = items.length; i < 7; i++) {
148
- html += '<tr><td>&nbsp;</td><td></td><td></td><td></td><td></td><td></td><td></td></tr>';
149
- }
 
150
 
151
- html += `
152
  </tbody>
153
  </table>
154
 
@@ -157,7 +199,7 @@ if (typeof document !== 'undefined') {
157
  <div class="notes">
158
  SPECIAL NOTES:<br><br>
159
  AMOUNT IN WORDS:<br>
160
- <strong>Rupees ${rupeeWords}${paiseWords ? ' and ' + paiseWords + ' Paise' : ''} only</strong><br><br>
161
  Delivery : 7 to 10 working days after receiving PO<br><br>
162
  BANK DETAILS FOR RTGS/ NEFT<br>
163
  BANK:${bank.name}<br>
@@ -200,81 +242,91 @@ if (typeof document !== 'undefined') {
200
  <button onclick="window.print()">Print / Save as PDF</button>
201
  </div>
202
  `;
203
- html = html.replace('{{address}}', company.address.replace(/\n/g, '<br>'));
204
- html = html.replace('{{cutomer_address}}', customer.address.replace(/\n/g, '<br>'));
205
- return html;
206
- }
207
-
208
- function updatePreview() {
209
- const html = generateQuotationHTML(form);
210
- previewContent.innerHTML = html;
211
- }
212
-
213
- function updateSerialNumbers() {
214
- itemsTableBody.querySelectorAll('tr').forEach((row, i) => {
215
- row.querySelector('.item-slno').textContent = i + 1;
216
- });
217
- }
218
-
219
- function validateItemInput(input) {
220
- const value = input.value;
221
- const type = input.type;
222
-
223
- input.classList.remove('input-error', 'input-success');
224
-
225
- if (type === 'number' && value !== '') {
226
- const num = parseFloat(value);
227
- if (isNaN(num) || num < 0) {
228
- input.classList.add('input-error');
229
- return false;
230
- } else {
231
- input.classList.add('input-success');
232
  }
233
- } else if (type === 'text' && input.hasAttribute('required') && value.trim() === '') {
234
- input.classList.add('input-error');
235
- return false;
236
- }
237
 
238
- return true;
239
- }
 
 
240
 
241
- function handleKeyNavigation(event) {
242
- if (event.key === 'Tab' || event.key === 'Enter') {
243
- const currentRow = event.target.closest('tr');
244
- const inputs = Array.from(currentRow.querySelectorAll('input'));
245
- const currentIndex = inputs.indexOf(event.target);
246
 
247
- if (event.key === 'Enter') {
248
- event.preventDefault();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
249
 
250
- // If we're at the last input in the row and it's the last row, add a new row
251
- if (currentIndex === inputs.length - 1) {
252
- const allRows = Array.from(itemsTableBody.querySelectorAll('tr'));
253
- const currentRowIndex = allRows.indexOf(currentRow);
254
 
255
- if (currentRowIndex === allRows.length - 1) {
256
- // Last row - add new row and focus first input
257
- addItemRow();
258
- setTimeout(() => {
259
- const newRow = itemsTableBody.lastElementChild;
260
- newRow.querySelector('.item-desc').focus();
261
- }, 50);
262
- } else {
263
- // Not last row - focus next row's first input
264
- const nextRow = allRows[currentRowIndex + 1];
265
- nextRow.querySelector('.item-desc').focus();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
266
  }
267
- } else {
268
- // Move to next input in same row
269
- inputs[currentIndex + 1].focus();
270
- }
271
  }
272
- }
273
- }
274
 
275
- function addItemRow() {
276
- const row = document.createElement('tr');
277
- row.innerHTML = `
278
  <td class="item-slno" data-label="S.No"></td>
279
  <td data-label="Description">
280
  <div class="tooltip">
@@ -314,134 +366,139 @@ if (typeof document !== 'undefined') {
314
  </td>
315
  `;
316
 
317
- itemsTableBody.appendChild(row);
318
- updateSerialNumbers();
319
-
320
- // Add event listeners to inputs
321
- const inputs = row.querySelectorAll('input');
322
- inputs.forEach(input => {
323
- input.addEventListener('input', (event) => {
324
- validateItemInput(event.target);
325
- updateItemAmount(event);
326
- updatePreview();
327
- });
328
-
329
- input.addEventListener('keydown', handleKeyNavigation);
330
-
331
- input.addEventListener('blur', (event) => {
332
- validateItemInput(event.target);
333
- });
334
- });
335
-
336
- // Add remove button listener
337
- row.querySelector('.remove-item').addEventListener('click', (_event) => {
338
- if (itemsTableBody.children.length > 1) {
339
- row.remove();
340
- updateSerialNumbers();
341
- updatePreview();
342
- } else {
343
- // If it's the last row, just clear it instead of removing
344
- inputs.forEach(input => {
345
- if (input.type === 'number') {
346
- input.value = input.classList.contains('item-qty') ? '1' : '0';
347
- } else {
348
- input.value = '';
349
- }
350
- input.classList.remove('input-error', 'input-success');
351
- });
352
- row.querySelector('.item-amount').textContent = '0.00';
353
- updatePreview();
 
 
 
 
 
354
  }
355
- });
356
 
357
- // Auto-calculate initial amount
358
- updateItemAmount({ target: row.querySelector('.item-qty') });
359
- }
360
 
361
- function updateItemAmount(event) {
362
- const row = event.target.closest('tr');
363
- if (!row) return;
 
364
 
365
- const qty = parseFloat(row.querySelector('.item-qty').value) || 0;
366
- const price = parseFloat(row.querySelector('.item-price').value) || 0;
367
- const discountRate = parseFloat(row.querySelector('.item-discount').value) || 0;
368
 
369
- const discountAmount = qty * price * discountRate / 100;
370
- const amount = qty * price - discountAmount;
371
 
372
- row.querySelector('.item-amount').textContent = amount.toFixed(2);
 
 
 
373
 
374
- // Add visual feedback for calculated amount
375
- const amountCell = row.querySelector('.item-amount');
376
- amountCell.style.backgroundColor = amount > 0 ? '#f0fdf4' : '#fef2f2';
377
- }
378
 
379
- // Enhanced Add Item button
380
- addItemBtn.addEventListener('click', () => {
381
- addItemRow();
382
- updatePreview();
 
 
383
 
384
- // Focus the new row's first input
385
- setTimeout(() => {
386
- const newRow = itemsTableBody.lastElementChild;
387
- newRow.querySelector('.item-desc').focus();
388
- }, 50);
389
- });
 
390
 
391
- // Add keyboard shortcut for adding items (Ctrl+I)
392
- document.addEventListener('keydown', (event) => {
393
- if (event.ctrlKey && event.key === 'i') {
394
- event.preventDefault();
395
- addItemBtn.click();
396
- }
397
- });
398
 
399
- form.addEventListener('input', updatePreview);
 
400
 
401
- form.addEventListener('submit', function (event) {
402
- event.preventDefault();
 
 
 
 
403
 
404
- // Validate that we have at least one item with description
405
- const hasValidItems = Array.from(itemsTableBody.querySelectorAll('tr')).some(row => {
406
- return row.querySelector('.item-desc').value.trim() !== '';
407
- });
 
 
408
 
409
- if (!hasValidItems) {
410
- alert('Please add at least one item with a description before generating the quotation.');
411
- return;
412
- }
 
 
 
413
 
414
- const html = generateQuotationHTML(form);
415
- output.innerHTML = html;
416
- output.style.display = 'block';
417
- document.getElementById('form-container').style.display = 'none';
418
- document.getElementById('preview-container').style.display = 'none';
419
- document.getElementById('top-header').style.display = 'none';
420
  });
421
-
422
- // Initial preview
423
- updatePreview();
424
- });
425
  }
426
 
427
  function calculateQuotation(items, igstRate, freightCharges) {
428
- const subtotal = items.reduce((sum, i) => sum + i.amount, 0);
429
- const igstAmount = (subtotal * igstRate) / 100;
430
- const total = subtotal + igstAmount + freightCharges;
431
- const totalBeforeRoundOff = total;
432
- const finalTotal = Math.round(totalBeforeRoundOff);
433
- const rOff = totalBeforeRoundOff - finalTotal;
434
-
435
- return {
436
- subtotal,
437
- igstAmount,
438
- total,
439
- rOff,
440
- finalTotal
441
- };
442
  }
443
 
444
  // Export for testing (Node.js)
445
- if (typeof module !== 'undefined' && module.exports) {
446
- module.exports = { numberToWords, calculateQuotation };
447
  }
 
1
  // helper: convert number to Indian-notation words (Crore, Lakh, Thousand, Hundred)
2
  function numberToWords(num) {
3
+ const small = [
4
+ "",
5
+ "One",
6
+ "Two",
7
+ "Three",
8
+ "Four",
9
+ "Five",
10
+ "Six",
11
+ "Seven",
12
+ "Eight",
13
+ "Nine",
14
+ "Ten",
15
+ "Eleven",
16
+ "Twelve",
17
+ "Thirteen",
18
+ "Fourteen",
19
+ "Fifteen",
20
+ "Sixteen",
21
+ "Seventeen",
22
+ "Eighteen",
23
+ "Nineteen",
24
+ ];
25
+ const tens = [
26
+ "",
27
+ "",
28
+ "Twenty",
29
+ "Thirty",
30
+ "Forty",
31
+ "Fifty",
32
+ "Sixty",
33
+ "Seventy",
34
+ "Eighty",
35
+ "Ninety",
36
+ ];
37
+ function twoDigit(n) {
38
+ if (n < 20) return small[n];
39
+ return tens[Math.floor(n / 10)] + (n % 10 ? " " + small[n % 10] : "");
40
+ }
41
+ let words = "";
42
+ const crore = Math.floor(num / 10000000);
43
+ num %= 10000000;
44
+ if (crore) words += twoDigit(crore) + " Crore ";
45
+ const lakh = Math.floor(num / 100000);
46
+ num %= 100000;
47
+ if (lakh) words += twoDigit(lakh) + " Lakh ";
48
+ const thousand = Math.floor(num / 1000);
49
+ num %= 1000;
50
+ if (thousand) words += twoDigit(thousand) + " Thousand ";
51
+ const hundred = Math.floor(num / 100);
52
+ num %= 100;
53
+ if (hundred) words += small[hundred] + " Hundred ";
54
+ if (num) words += (words ? "and " : "") + twoDigit(num) + " ";
55
+ return words.trim() || "Zero";
56
  }
57
 
58
+ if (typeof document !== "undefined") {
59
+ document.addEventListener("DOMContentLoaded", function () {
60
+ const addItemBtn = document.getElementById("add-item");
61
+ const itemsTableBody = document.querySelector("#items-table tbody");
62
+ const form = document.getElementById("quotation-form");
63
+ const output = document.getElementById("quotation-output");
64
+ const previewContent = document.getElementById("preview-content");
65
+
66
+ // Add initial empty row
67
+ addItemRow();
68
+
69
+ function generateQuotationHTML(form) {
70
+ const data = new FormData(form);
71
+ const company = {
72
+ name: data.get("company-name"),
73
+ address: data.get("company-address"),
74
+ phone: data.get("company-phone"),
75
+ email: data.get("company-email"),
76
+ gstin: data.get("company-gstin"),
77
+ };
78
+ const customer = {
79
+ name: data.get("customer-name"),
80
+ address: data.get("customer-address"),
81
+ phone: data.get("customer-phone"),
82
+ email: data.get("customer-email"),
83
+ gstin: data.get("customer-gstin"),
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
+ const desc = row.querySelector(".item-desc").value.trim();
98
+ if (desc) {
99
+ // Only include rows with description
100
+ items.push({
101
+ description: desc,
102
+ hsn: row.querySelector(".item-hsn").value,
103
+ qty: parseFloat(row.querySelector(".item-qty").value) || 0,
104
+ price: parseFloat(row.querySelector(".item-price").value) || 0,
105
+ discount:
106
+ parseFloat(row.querySelector(".item-discount").value) || 0,
107
+ amount:
108
+ parseFloat(row.querySelector(".item-amount").textContent) || 0,
109
+ });
110
+ }
111
+ });
112
+
113
+ if (items.length === 0) {
114
+ return `
115
+ <div class="text-center text-gray-500 p-8">Please add items to generate quotation</div>
116
+ `;
117
+ }
118
 
119
+ const { subtotal, igstAmount, _total, rOff, finalTotal } =
120
+ calculateQuotation(items, igstRate, freightCharges);
121
 
122
+ // convert total to words (Rupees and Paise)
123
+ const rupeePart = Math.floor(finalTotal);
124
+ const paisePart = Math.round((finalTotal - rupeePart) * 100);
125
+ const rupeeWords = numberToWords(rupeePart);
126
+ const paiseWords = paisePart > 0 ? numberToWords(paisePart) : "";
127
 
128
+ let html = `
129
  <div class="quotation-print">
130
  <div class="header">
131
  <div class="company-details">
132
  <h1>${company.name}</h1>
133
  {{address}}<br>
134
+ GST NO. : ${company.gstin || ""}<br>
135
  CONTACT NO : ${company.phone} ${company.email}
136
  </div>
137
  <div class="quotation-title">
 
153
  <strong>CUSTOMER INFO</strong><br>
154
  ${customer.name}<br>
155
  {{cutomer_address}}<br>
156
+ GST NO. : ${customer.gstin || ""}<br>
157
  CONTACT NO : ${customer.phone} ${customer.email}
158
  </div>
159
 
 
171
  </thead>
172
  <tbody>
173
  `;
174
+ items.forEach((item, idx) => {
175
+ html += `
176
  <tr>
177
  <td>${idx + 1}</td>
178
  <td>${item.description}</td>
 
183
  <td>${item.amount.toFixed(2)}</td>
184
  </tr>
185
  `;
186
+ });
187
+ // Add empty rows to fill the page
188
+ for (let i = items.length; i < 7; i++) {
189
+ html +=
190
+ "<tr><td>&nbsp;</td><td></td><td></td><td></td><td></td><td></td><td></td></tr>";
191
+ }
192
 
193
+ html += `
194
  </tbody>
195
  </table>
196
 
 
199
  <div class="notes">
200
  SPECIAL NOTES:<br><br>
201
  AMOUNT IN WORDS:<br>
202
+ <strong>Rupees ${rupeeWords}${paiseWords ? " and " + paiseWords + " Paise" : ""} only</strong><br><br>
203
  Delivery : 7 to 10 working days after receiving PO<br><br>
204
  BANK DETAILS FOR RTGS/ NEFT<br>
205
  BANK:${bank.name}<br>
 
242
  <button onclick="window.print()">Print / Save as PDF</button>
243
  </div>
244
  `;
245
+ html = html.replace(
246
+ "{{address}}",
247
+ company.address.replace(/\n/g, "<br>"),
248
+ );
249
+ html = html.replace(
250
+ "{{cutomer_address}}",
251
+ customer.address.replace(/\n/g, "<br>"),
252
+ );
253
+ return html;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
254
  }
 
 
 
 
255
 
256
+ function updatePreview() {
257
+ const html = generateQuotationHTML(form);
258
+ previewContent.innerHTML = html;
259
+ }
260
 
261
+ function updateSerialNumbers() {
262
+ itemsTableBody.querySelectorAll("tr").forEach((row, i) => {
263
+ row.querySelector(".item-slno").textContent = i + 1;
264
+ });
265
+ }
266
 
267
+ function validateItemInput(input) {
268
+ const value = input.value;
269
+ const type = input.type;
270
+
271
+ input.classList.remove("input-error", "input-success");
272
+
273
+ if (type === "number" && value !== "") {
274
+ const num = parseFloat(value);
275
+ if (isNaN(num) || num < 0) {
276
+ input.classList.add("input-error");
277
+ return false;
278
+ } else {
279
+ input.classList.add("input-success");
280
+ }
281
+ } else if (
282
+ type === "text" &&
283
+ input.hasAttribute("required") &&
284
+ value.trim() === ""
285
+ ) {
286
+ input.classList.add("input-error");
287
+ return false;
288
+ }
289
 
290
+ return true;
291
+ }
 
 
292
 
293
+ function handleKeyNavigation(event) {
294
+ if (event.key === "Tab" || event.key === "Enter") {
295
+ const currentRow = event.target.closest("tr");
296
+ const inputs = Array.from(currentRow.querySelectorAll("input"));
297
+ const currentIndex = inputs.indexOf(event.target);
298
+
299
+ if (event.key === "Enter") {
300
+ event.preventDefault();
301
+
302
+ // If we're at the last input in the row and it's the last row, add a new row
303
+ if (currentIndex === inputs.length - 1) {
304
+ const allRows = Array.from(itemsTableBody.querySelectorAll("tr"));
305
+ const currentRowIndex = allRows.indexOf(currentRow);
306
+
307
+ if (currentRowIndex === allRows.length - 1) {
308
+ // Last row - add new row and focus first input
309
+ addItemRow();
310
+ setTimeout(() => {
311
+ const newRow = itemsTableBody.lastElementChild;
312
+ newRow.querySelector(".item-desc").focus();
313
+ }, 50);
314
+ } else {
315
+ // Not last row - focus next row's first input
316
+ const nextRow = allRows[currentRowIndex + 1];
317
+ nextRow.querySelector(".item-desc").focus();
318
+ }
319
+ } else {
320
+ // Move to next input in same row
321
+ inputs[currentIndex + 1].focus();
322
+ }
323
+ }
324
  }
 
 
 
 
325
  }
 
 
326
 
327
+ function addItemRow() {
328
+ const row = document.createElement("tr");
329
+ row.innerHTML = `
330
  <td class="item-slno" data-label="S.No"></td>
331
  <td data-label="Description">
332
  <div class="tooltip">
 
366
  </td>
367
  `;
368
 
369
+ itemsTableBody.appendChild(row);
370
+ updateSerialNumbers();
371
+
372
+ // Add event listeners to inputs
373
+ const inputs = row.querySelectorAll("input");
374
+ inputs.forEach((input) => {
375
+ input.addEventListener("input", (event) => {
376
+ validateItemInput(event.target);
377
+ updateItemAmount(event);
378
+ updatePreview();
379
+ });
380
+
381
+ input.addEventListener("keydown", handleKeyNavigation);
382
+
383
+ input.addEventListener("blur", (event) => {
384
+ validateItemInput(event.target);
385
+ });
386
+ });
387
+
388
+ // Add remove button listener
389
+ row.querySelector(".remove-item").addEventListener("click", (_event) => {
390
+ if (itemsTableBody.children.length > 1) {
391
+ row.remove();
392
+ updateSerialNumbers();
393
+ updatePreview();
394
+ } else {
395
+ // If it's the last row, just clear it instead of removing
396
+ inputs.forEach((input) => {
397
+ if (input.type === "number") {
398
+ input.value = input.classList.contains("item-qty") ? "1" : "0";
399
+ } else {
400
+ input.value = "";
401
+ }
402
+ input.classList.remove("input-error", "input-success");
403
+ });
404
+ row.querySelector(".item-amount").textContent = "0.00";
405
+ updatePreview();
406
+ }
407
+ });
408
+
409
+ // Auto-calculate initial amount
410
+ updateItemAmount({ target: row.querySelector(".item-qty") });
411
  }
 
412
 
413
+ function updateItemAmount(event) {
414
+ const row = event.target.closest("tr");
415
+ if (!row) return;
416
 
417
+ const qty = parseFloat(row.querySelector(".item-qty").value) || 0;
418
+ const price = parseFloat(row.querySelector(".item-price").value) || 0;
419
+ const discountRate =
420
+ parseFloat(row.querySelector(".item-discount").value) || 0;
421
 
422
+ const discountAmount = (qty * price * discountRate) / 100;
423
+ const amount = qty * price - discountAmount;
 
424
 
425
+ row.querySelector(".item-amount").textContent = amount.toFixed(2);
 
426
 
427
+ // Add visual feedback for calculated amount
428
+ const amountCell = row.querySelector(".item-amount");
429
+ amountCell.style.backgroundColor = amount > 0 ? "#f0fdf4" : "#fef2f2";
430
+ }
431
 
432
+ // Enhanced Add Item button
433
+ addItemBtn.addEventListener("click", () => {
434
+ addItemRow();
435
+ updatePreview();
436
 
437
+ // Focus the new row's first input
438
+ setTimeout(() => {
439
+ const newRow = itemsTableBody.lastElementChild;
440
+ newRow.querySelector(".item-desc").focus();
441
+ }, 50);
442
+ });
443
 
444
+ // Add keyboard shortcut for adding items (Ctrl+I)
445
+ document.addEventListener("keydown", (event) => {
446
+ if (event.ctrlKey && event.key === "i") {
447
+ event.preventDefault();
448
+ addItemBtn.click();
449
+ }
450
+ });
451
 
452
+ form.addEventListener("input", updatePreview);
 
 
 
 
 
 
453
 
454
+ form.addEventListener("submit", function (event) {
455
+ event.preventDefault();
456
 
457
+ // Validate that we have at least one item with description
458
+ const hasValidItems = Array.from(
459
+ itemsTableBody.querySelectorAll("tr"),
460
+ ).some((row) => {
461
+ return row.querySelector(".item-desc").value.trim() !== "";
462
+ });
463
 
464
+ if (!hasValidItems) {
465
+ alert(
466
+ "Please add at least one item with a description before generating the quotation.",
467
+ );
468
+ return;
469
+ }
470
 
471
+ const html = generateQuotationHTML(form);
472
+ output.innerHTML = html;
473
+ output.style.display = "block";
474
+ document.getElementById("form-container").style.display = "none";
475
+ document.getElementById("preview-container").style.display = "none";
476
+ document.getElementById("top-header").style.display = "none";
477
+ });
478
 
479
+ // Initial preview
480
+ updatePreview();
 
 
 
 
481
  });
 
 
 
 
482
  }
483
 
484
  function calculateQuotation(items, igstRate, freightCharges) {
485
+ const subtotal = items.reduce((sum, i) => sum + i.amount, 0);
486
+ const igstAmount = (subtotal * igstRate) / 100;
487
+ const total = subtotal + igstAmount + freightCharges;
488
+ const totalBeforeRoundOff = total;
489
+ const finalTotal = Math.round(totalBeforeRoundOff);
490
+ const rOff = totalBeforeRoundOff - finalTotal;
491
+
492
+ return {
493
+ subtotal,
494
+ igstAmount,
495
+ total,
496
+ rOff,
497
+ finalTotal,
498
+ };
499
  }
500
 
501
  // Export for testing (Node.js)
502
+ if (typeof module !== "undefined" && module.exports) {
503
+ module.exports = { numberToWords, calculateQuotation };
504
  }
style.css CHANGED
@@ -2,362 +2,362 @@
2
 
3
  /* Enhanced Items Table Styling */
4
  #items-table {
5
- border-collapse: collapse;
6
- margin-bottom: 1rem;
7
  }
8
 
9
  #items-table th {
10
- background-color: #f8fafc;
11
- font-weight: 600;
12
- font-size: 0.875rem;
13
- color: #374151;
14
- border: 1px solid #d1d5db;
15
- padding: 0.75rem 0.5rem;
16
- text-align: left;
17
  }
18
 
19
  #items-table td {
20
- border: 1px solid #d1d5db;
21
- padding: 0.25rem;
22
- vertical-align: middle;
23
  }
24
 
25
  #items-table input {
26
- width: 100%;
27
- padding: 0.5rem;
28
- border: 1px solid #d1d5db;
29
- border-radius: 0.375rem;
30
- font-size: 0.875rem;
31
- transition:
32
- border-color 0.2s,
33
- box-shadow 0.2s;
34
  }
35
 
36
  #items-table input:focus {
37
- outline: none;
38
- border-color: #3b82f6;
39
- box-shadow: 0 0 0 3px rgb(59 130 246 / 10%);
40
  }
41
 
42
  #items-table input:invalid {
43
- border-color: #ef4444;
44
  }
45
 
46
  #items-table .item-slno {
47
- text-align: center;
48
- font-weight: 600;
49
- color: #6b7280;
50
- background-color: #f9fafb;
51
- width: 3rem;
52
  }
53
 
54
  #items-table .item-amount {
55
- text-align: right;
56
- font-weight: 600;
57
- color: #059669;
58
- background-color: #f0fdf4;
59
- font-family: "Courier New", monospace;
60
- padding: 0.5rem;
61
- border: 2px solid #d1fae5;
62
  }
63
 
64
  #items-table .remove-item {
65
- background-color: #ef4444;
66
- color: white;
67
- border: none;
68
- padding: 0.375rem 0.75rem;
69
- border-radius: 0.375rem;
70
- font-size: 0.75rem;
71
- cursor: pointer;
72
- transition: background-color 0.2s;
73
- display: flex;
74
- align-items: center;
75
- gap: 0.25rem;
76
  }
77
 
78
  #items-table .remove-item:hover {
79
- background-color: #dc2626;
80
  }
81
 
82
  #items-table .remove-item:focus {
83
- outline: none;
84
- box-shadow: 0 0 0 2px rgb(239 68 68 / 50%);
85
  }
86
 
87
  /* Column widths for better UX */
88
  #items-table th:nth-child(1) {
89
- width: 4rem;
90
  }
91
 
92
  /* S.No */
93
  #items-table th:nth-child(2) {
94
- width: 30%;
95
  }
96
 
97
  /* Description */
98
  #items-table th:nth-child(3) {
99
- width: 8rem;
100
  }
101
 
102
  /* HSN Code */
103
  #items-table th:nth-child(4) {
104
- width: 5rem;
105
  }
106
 
107
  /* Qty */
108
  #items-table th:nth-child(5) {
109
- width: 8rem;
110
  }
111
 
112
  /* Unit Price */
113
  #items-table th:nth-child(6) {
114
- width: 7rem;
115
  }
116
 
117
  /* Discount */
118
  #items-table th:nth-child(7) {
119
- width: 8rem;
120
  }
121
 
122
  /* Amount */
123
  #items-table th:nth-child(8) {
124
- width: 6rem;
125
  }
126
 
127
  /* Action */
128
 
129
  /* Add Item Button Enhancement */
130
  #add-item {
131
- display: flex;
132
- align-items: center;
133
- gap: 0.5rem;
134
- margin-top: 0.5rem;
135
  }
136
 
137
  #add-item::before {
138
- content: "+";
139
- font-size: 1.25rem;
140
- font-weight: bold;
141
  }
142
 
143
  /* Empty state styling */
144
  .items-empty-state {
145
- text-align: center;
146
- color: #6b7280;
147
- font-style: italic;
148
- padding: 2rem;
149
- background-color: #f9fafb;
150
- border: 2px dashed #d1d5db;
151
- margin-bottom: 1rem;
152
  }
153
 
154
  /* Row highlighting on hover */
155
  #items-table tbody tr:hover {
156
- background-color: #f8fafc;
157
  }
158
 
159
  /* Input error states */
160
  .input-error {
161
- border-color: #ef4444 !important;
162
- background-color: #fef2f2;
163
  }
164
 
165
  .input-success {
166
- border-color: #10b981 !important;
167
  }
168
 
169
  /* Tooltips */
170
  .tooltip {
171
- position: relative;
172
- display: inline-block;
173
  }
174
 
175
  .tooltip .tooltiptext {
176
- visibility: hidden;
177
- width: 200px;
178
- background-color: #374151;
179
- color: white;
180
- text-align: center;
181
- border-radius: 6px;
182
- padding: 5px;
183
- position: absolute;
184
- z-index: 1;
185
- bottom: 125%;
186
- left: 50%;
187
- margin-left: -100px;
188
- opacity: 0;
189
- transition: opacity 0.3s;
190
- font-size: 0.75rem;
191
  }
192
 
193
  .tooltip:hover .tooltiptext {
194
- visibility: visible;
195
- opacity: 1;
196
  }
197
 
198
  .quotation-print {
199
- max-width: 800px;
200
- margin: auto;
201
- border: 1px solid #000;
202
- padding: 20px;
203
  }
204
 
205
  .quotation-print h2 {
206
- text-align: center;
207
- margin-bottom: 20px;
208
  }
209
 
210
  .quotation-print .section {
211
- margin-bottom: 15px;
212
  }
213
 
214
  .totals table {
215
- float: right;
216
- border: none;
217
- width: 100%;
218
- border-collapse: collapse;
219
  }
220
 
221
  .totals td {
222
- border: 1px solid #000;
223
- padding: 8px;
224
  }
225
 
226
  .disclaimer {
227
- color: #555;
228
- margin-top: 20px;
229
- font-size: 10px;
230
- text-align: justify;
231
- border-top: 1px solid #000;
232
- padding-top: 10px;
233
  }
234
 
235
  .quote-meta {
236
- text-align: right;
237
  }
238
 
239
  .header {
240
- display: flex;
241
- justify-content: space-between;
242
- margin-bottom: 20px;
243
  }
244
 
245
  .company-details h1 {
246
- margin: 0;
247
- font-size: 24px;
248
- color: #036;
249
- font-family: Arial, Helvetica, sans-serif;
250
  }
251
 
252
  .quotation-title h1 {
253
- margin: 0;
254
- font-size: 24px;
255
- color: #036;
256
- text-align: right;
257
  }
258
 
259
  .quote-meta table {
260
- border-collapse: collapse;
261
- width: 100%;
262
  }
263
 
264
  .quote-meta td {
265
- border: 1px solid #000;
266
- padding: 5px;
267
  }
268
 
269
  .customer-info {
270
- border: 1px solid #000;
271
- padding: 10px;
272
- margin-bottom: 20px;
273
  }
274
 
275
  .items-table-print {
276
- width: 100%;
277
- border-collapse: collapse;
278
- margin-bottom: 20px;
279
  }
280
 
281
  .items-table-print th,
282
  .items-table-print td {
283
- border: 1px solid #000;
284
- padding: 8px;
285
- text-align: left;
286
  }
287
 
288
  .items-table-print thead th {
289
- background-color: #e0f2f7;
290
  }
291
 
292
  .footer {
293
- margin-top: 20px;
294
  }
295
 
296
  .notes-and-total {
297
- display: flex;
298
- justify-content: space-between;
299
  }
300
 
301
  .notes {
302
- width: 60%;
303
- font-size: 12px;
304
  }
305
 
306
  .totals {
307
- width: 35%;
308
  }
309
 
310
  .totals td:first-child {
311
- text-align: right;
312
  }
313
 
314
  .thank-you {
315
- text-align: center;
316
- margin-top: 20px;
317
- font-weight: bold;
318
  }
319
 
320
  @media print {
321
 
322
- html,
323
- body {
324
- height: 100%;
325
- margin: 0;
326
- padding: 0;
327
- }
328
-
329
- @page {
330
- size: a4;
331
- margin: 10mm;
332
- }
333
-
334
- body {
335
- font-size: 12pt;
336
- }
337
-
338
- .quotation-print {
339
- max-width: 100%;
340
- padding: 20px;
341
- margin: auto;
342
- border: 1px solid #000;
343
- }
344
-
345
- .items-table-print,
346
- .items-table-print tr,
347
- .items-table-print td {
348
- page-break-inside: avoid !important;
349
- }
350
-
351
- .header,
352
- .customer-info,
353
- .footer,
354
- .notes-and-total,
355
- .disclaimer,
356
- .thank-you {
357
- page-break-inside: avoid !important;
358
- }
359
-
360
- button {
361
- display: none;
362
- }
363
  }
 
2
 
3
  /* Enhanced Items Table Styling */
4
  #items-table {
5
+ border-collapse: collapse;
6
+ margin-bottom: 1rem;
7
  }
8
 
9
  #items-table th {
10
+ background-color: #f8fafc;
11
+ font-weight: 600;
12
+ font-size: 0.875rem;
13
+ color: #374151;
14
+ border: 1px solid #d1d5db;
15
+ padding: 0.75rem 0.5rem;
16
+ text-align: left;
17
  }
18
 
19
  #items-table td {
20
+ border: 1px solid #d1d5db;
21
+ padding: 0.25rem;
22
+ vertical-align: middle;
23
  }
24
 
25
  #items-table input {
26
+ width: 100%;
27
+ padding: 0.5rem;
28
+ border: 1px solid #d1d5db;
29
+ border-radius: 0.375rem;
30
+ font-size: 0.875rem;
31
+ transition:
32
+ border-color 0.2s,
33
+ box-shadow 0.2s;
34
  }
35
 
36
  #items-table input:focus {
37
+ outline: none;
38
+ border-color: #3b82f6;
39
+ box-shadow: 0 0 0 3px rgb(59 130 246 / 10%);
40
  }
41
 
42
  #items-table input:invalid {
43
+ border-color: #ef4444;
44
  }
45
 
46
  #items-table .item-slno {
47
+ text-align: center;
48
+ font-weight: 600;
49
+ color: #6b7280;
50
+ background-color: #f9fafb;
51
+ width: 3rem;
52
  }
53
 
54
  #items-table .item-amount {
55
+ text-align: right;
56
+ font-weight: 600;
57
+ color: #059669;
58
+ background-color: #f0fdf4;
59
+ font-family: "Courier New", monospace;
60
+ padding: 0.5rem;
61
+ border: 2px solid #d1fae5;
62
  }
63
 
64
  #items-table .remove-item {
65
+ background-color: #ef4444;
66
+ color: white;
67
+ border: none;
68
+ padding: 0.375rem 0.75rem;
69
+ border-radius: 0.375rem;
70
+ font-size: 0.75rem;
71
+ cursor: pointer;
72
+ transition: background-color 0.2s;
73
+ display: flex;
74
+ align-items: center;
75
+ gap: 0.25rem;
76
  }
77
 
78
  #items-table .remove-item:hover {
79
+ background-color: #dc2626;
80
  }
81
 
82
  #items-table .remove-item:focus {
83
+ outline: none;
84
+ box-shadow: 0 0 0 2px rgb(239 68 68 / 50%);
85
  }
86
 
87
  /* Column widths for better UX */
88
  #items-table th:nth-child(1) {
89
+ width: 4rem;
90
  }
91
 
92
  /* S.No */
93
  #items-table th:nth-child(2) {
94
+ width: 30%;
95
  }
96
 
97
  /* Description */
98
  #items-table th:nth-child(3) {
99
+ width: 8rem;
100
  }
101
 
102
  /* HSN Code */
103
  #items-table th:nth-child(4) {
104
+ width: 5rem;
105
  }
106
 
107
  /* Qty */
108
  #items-table th:nth-child(5) {
109
+ width: 8rem;
110
  }
111
 
112
  /* Unit Price */
113
  #items-table th:nth-child(6) {
114
+ width: 7rem;
115
  }
116
 
117
  /* Discount */
118
  #items-table th:nth-child(7) {
119
+ width: 8rem;
120
  }
121
 
122
  /* Amount */
123
  #items-table th:nth-child(8) {
124
+ width: 6rem;
125
  }
126
 
127
  /* Action */
128
 
129
  /* Add Item Button Enhancement */
130
  #add-item {
131
+ display: flex;
132
+ align-items: center;
133
+ gap: 0.5rem;
134
+ margin-top: 0.5rem;
135
  }
136
 
137
  #add-item::before {
138
+ content: "+";
139
+ font-size: 1.25rem;
140
+ font-weight: bold;
141
  }
142
 
143
  /* Empty state styling */
144
  .items-empty-state {
145
+ text-align: center;
146
+ color: #6b7280;
147
+ font-style: italic;
148
+ padding: 2rem;
149
+ background-color: #f9fafb;
150
+ border: 2px dashed #d1d5db;
151
+ margin-bottom: 1rem;
152
  }
153
 
154
  /* Row highlighting on hover */
155
  #items-table tbody tr:hover {
156
+ background-color: #f8fafc;
157
  }
158
 
159
  /* Input error states */
160
  .input-error {
161
+ border-color: #ef4444 !important;
162
+ background-color: #fef2f2;
163
  }
164
 
165
  .input-success {
166
+ border-color: #10b981 !important;
167
  }
168
 
169
  /* Tooltips */
170
  .tooltip {
171
+ position: relative;
172
+ display: inline-block;
173
  }
174
 
175
  .tooltip .tooltiptext {
176
+ visibility: hidden;
177
+ width: 200px;
178
+ background-color: #374151;
179
+ color: white;
180
+ text-align: center;
181
+ border-radius: 6px;
182
+ padding: 5px;
183
+ position: absolute;
184
+ z-index: 1;
185
+ bottom: 125%;
186
+ left: 50%;
187
+ margin-left: -100px;
188
+ opacity: 0;
189
+ transition: opacity 0.3s;
190
+ font-size: 0.75rem;
191
  }
192
 
193
  .tooltip:hover .tooltiptext {
194
+ visibility: visible;
195
+ opacity: 1;
196
  }
197
 
198
  .quotation-print {
199
+ max-width: 800px;
200
+ margin: auto;
201
+ border: 1px solid #000;
202
+ padding: 20px;
203
  }
204
 
205
  .quotation-print h2 {
206
+ text-align: center;
207
+ margin-bottom: 20px;
208
  }
209
 
210
  .quotation-print .section {
211
+ margin-bottom: 15px;
212
  }
213
 
214
  .totals table {
215
+ float: right;
216
+ border: none;
217
+ width: 100%;
218
+ border-collapse: collapse;
219
  }
220
 
221
  .totals td {
222
+ border: 1px solid #000;
223
+ padding: 8px;
224
  }
225
 
226
  .disclaimer {
227
+ color: #555;
228
+ margin-top: 20px;
229
+ font-size: 10px;
230
+ text-align: justify;
231
+ border-top: 1px solid #000;
232
+ padding-top: 10px;
233
  }
234
 
235
  .quote-meta {
236
+ text-align: right;
237
  }
238
 
239
  .header {
240
+ display: flex;
241
+ justify-content: space-between;
242
+ margin-bottom: 20px;
243
  }
244
 
245
  .company-details h1 {
246
+ margin: 0;
247
+ font-size: 24px;
248
+ color: #036;
249
+ font-family: Arial, Helvetica, sans-serif;
250
  }
251
 
252
  .quotation-title h1 {
253
+ margin: 0;
254
+ font-size: 24px;
255
+ color: #036;
256
+ text-align: right;
257
  }
258
 
259
  .quote-meta table {
260
+ border-collapse: collapse;
261
+ width: 100%;
262
  }
263
 
264
  .quote-meta td {
265
+ border: 1px solid #000;
266
+ padding: 5px;
267
  }
268
 
269
  .customer-info {
270
+ border: 1px solid #000;
271
+ padding: 10px;
272
+ margin-bottom: 20px;
273
  }
274
 
275
  .items-table-print {
276
+ width: 100%;
277
+ border-collapse: collapse;
278
+ margin-bottom: 20px;
279
  }
280
 
281
  .items-table-print th,
282
  .items-table-print td {
283
+ border: 1px solid #000;
284
+ padding: 8px;
285
+ text-align: left;
286
  }
287
 
288
  .items-table-print thead th {
289
+ background-color: #e0f2f7;
290
  }
291
 
292
  .footer {
293
+ margin-top: 20px;
294
  }
295
 
296
  .notes-and-total {
297
+ display: flex;
298
+ justify-content: space-between;
299
  }
300
 
301
  .notes {
302
+ width: 60%;
303
+ font-size: 12px;
304
  }
305
 
306
  .totals {
307
+ width: 35%;
308
  }
309
 
310
  .totals td:first-child {
311
+ text-align: right;
312
  }
313
 
314
  .thank-you {
315
+ text-align: center;
316
+ margin-top: 20px;
317
+ font-weight: bold;
318
  }
319
 
320
  @media print {
321
 
322
+ html,
323
+ body {
324
+ height: 100%;
325
+ margin: 0;
326
+ padding: 0;
327
+ }
328
+
329
+ @page {
330
+ size: a4;
331
+ margin: 10mm;
332
+ }
333
+
334
+ body {
335
+ font-size: 12pt;
336
+ }
337
+
338
+ .quotation-print {
339
+ max-width: 100%;
340
+ padding: 20px;
341
+ margin: auto;
342
+ border: 1px solid #000;
343
+ }
344
+
345
+ .items-table-print,
346
+ .items-table-print tr,
347
+ .items-table-print td {
348
+ page-break-inside: avoid !important;
349
+ }
350
+
351
+ .header,
352
+ .customer-info,
353
+ .footer,
354
+ .notes-and-total,
355
+ .disclaimer,
356
+ .thank-you {
357
+ page-break-inside: avoid !important;
358
+ }
359
+
360
+ button {
361
+ display: none;
362
+ }
363
  }
tests/test.js CHANGED
@@ -1,79 +1,109 @@
1
- const assert = require('assert');
2
- const { numberToWords, calculateQuotation } = 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');
34
 
35
  // --- Tests for calculateQuotation ---
36
 
37
  const calculationCases = [
38
- {
39
- name: 'Basic case with one item',
40
- items: [{ amount: 100 }],
41
- igstRate: 18,
42
- freightCharges: 50,
43
- expected: { subtotal: 100, igstAmount: 18, total: 168, finalTotal: 168, rOff: 0 }
44
- },
45
- {
46
- name: 'Multiple items',
47
- items: [{ amount: 100 }, { amount: 250.50 }],
48
- igstRate: 18,
49
- freightCharges: 50,
50
- expected: { subtotal: 350.50, igstAmount: 63.09, total: 463.59, finalTotal: 464, rOff: -0.41 }
51
- },
52
- {
53
- name: 'No IGST or freight',
54
- items: [{ amount: 500 }],
55
- igstRate: 0,
56
- freightCharges: 0,
57
- expected: { subtotal: 500, igstAmount: 0, total: 500, finalTotal: 500, rOff: 0 }
58
- },
59
- {
60
- name: 'Rounding down',
61
- items: [{ amount: 99.20 }],
62
- igstRate: 10, // 9.92
63
- freightCharges: 0, // total = 109.12
64
- expected: { subtotal: 99.20, igstAmount: 9.92, total: 109.12, finalTotal: 109, rOff: 0.12 }
65
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
  ];
67
 
68
- calculationCases.forEach(({ name, items, igstRate, freightCharges, expected }) => {
69
- const actual = calculateQuotation(items, igstRate, freightCharges);
70
- // Need to compare floating point numbers with a tolerance
71
- Object.keys(expected).forEach(key => {
72
- assert(
73
- Math.abs(actual[key] - expected[key]) < 0.001,
74
- `[${name}] ${key} => ${actual[key]} (expected ${expected[key]})`
75
- );
76
- });
77
- });
 
 
78
 
79
- console.log('✅ All calculateQuotation tests passed');
 
1
+ const assert = require("assert");
2
+ const { numberToWords, calculateQuotation } = 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
+ {
20
+ num: 9999999,
21
+ expected:
22
+ "Ninety Nine Lakh Ninety Nine Thousand Nine Hundred and Ninety Nine",
23
+ },
24
+ { num: 10000000, expected: "One Crore" },
25
+ { num: 12500000, expected: "One Crore Twenty Five Lakh" },
26
  ];
27
 
28
  cases.forEach(({ num, expected }) => {
29
+ const actual = numberToWords(num);
30
+ assert.strictEqual(
31
+ actual,
32
+ expected,
33
+ `${num} => "${actual}" (expected "${expected}")`,
34
+ );
35
  });
36
 
37
+ console.log("✅ All numberToWords tests passed");
38
 
39
  // --- Tests for calculateQuotation ---
40
 
41
  const calculationCases = [
42
+ {
43
+ name: "Basic case with one item",
44
+ items: [{ amount: 100 }],
45
+ igstRate: 18,
46
+ freightCharges: 50,
47
+ expected: {
48
+ subtotal: 100,
49
+ igstAmount: 18,
50
+ total: 168,
51
+ finalTotal: 168,
52
+ rOff: 0,
53
+ },
54
+ },
55
+ {
56
+ name: "Multiple items",
57
+ items: [{ amount: 100 }, { amount: 250.5 }],
58
+ igstRate: 18,
59
+ freightCharges: 50,
60
+ expected: {
61
+ subtotal: 350.5,
62
+ igstAmount: 63.09,
63
+ total: 463.59,
64
+ finalTotal: 464,
65
+ rOff: -0.41,
66
+ },
67
+ },
68
+ {
69
+ name: "No IGST or freight",
70
+ items: [{ amount: 500 }],
71
+ igstRate: 0,
72
+ freightCharges: 0,
73
+ expected: {
74
+ subtotal: 500,
75
+ igstAmount: 0,
76
+ total: 500,
77
+ finalTotal: 500,
78
+ rOff: 0,
79
+ },
80
+ },
81
+ {
82
+ name: "Rounding down",
83
+ items: [{ amount: 99.2 }],
84
+ igstRate: 10, // 9.92
85
+ freightCharges: 0, // total = 109.12
86
+ expected: {
87
+ subtotal: 99.2,
88
+ igstAmount: 9.92,
89
+ total: 109.12,
90
+ finalTotal: 109,
91
+ rOff: 0.12,
92
+ },
93
+ },
94
  ];
95
 
96
+ calculationCases.forEach(
97
+ ({ name, items, igstRate, freightCharges, expected }) => {
98
+ const actual = calculateQuotation(items, igstRate, freightCharges);
99
+ // Need to compare floating point numbers with a tolerance
100
+ Object.keys(expected).forEach((key) => {
101
+ assert(
102
+ Math.abs(actual[key] - expected[key]) < 0.001,
103
+ `[${name}] ${key} => ${actual[key]} (expected ${expected[key]})`,
104
+ );
105
+ });
106
+ },
107
+ );
108
 
109
+ console.log("✅ All calculateQuotation tests passed");