anuragshas commited on
Commit
0439da7
·
1 Parent(s): 659f9d7

feat: update experience for adding items

Browse files
Files changed (3) hide show
  1. index.html +14 -5
  2. script.js +186 -26
  3. style.css +191 -0
index.html CHANGED
@@ -48,24 +48,33 @@
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
  <table id="items-table" class="w-full mb-2">
52
  <thead>
53
  <tr class="bg-gray-200">
54
  <th class="p-2 border border-gray-300">S.No</th>
55
- <th class="p-2 border border-gray-300">Description</th>
56
  <th class="p-2 border border-gray-300">HSN Code</th>
57
- <th class="p-2 border border-gray-300">Qty</th>
58
- <th class="p-2 border border-gray-300">Unit Price</th>
59
  <th class="p-2 border border-gray-300">Discount (%)</th>
60
  <th class="p-2 border border-gray-300">Amount</th>
61
- <th class="p-2 border border-gray-300"></th>
62
  </tr>
63
  </thead>
64
  <tbody>
65
  </tbody>
66
  </table>
67
  <button type="button" id="add-item"
68
- class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">Add Item</button>
69
  </fieldset>
70
  <fieldset class="border border-gray-300 p-4 mb-4">
71
  <legend class="text-lg font-semibold mb-2">Additional Charges</legend>
 
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>
script.js CHANGED
@@ -28,6 +28,9 @@ if (typeof document !== 'undefined') {
28
  const output = document.getElementById('quotation-output');
29
  const previewContent = document.getElementById('preview-content');
30
 
 
 
 
31
  function generateQuotationHTML(form) {
32
  const data = new FormData(form);
33
  const company = {
@@ -56,15 +59,23 @@ if (typeof document !== 'undefined') {
56
  };
57
  const items = [];
58
  itemsTableBody.querySelectorAll('tr').forEach(row => {
59
- items.push({
60
- description: row.querySelector('.item-desc').value,
61
- hsn: row.querySelector('.item-hsn').value,
62
- qty: parseFloat(row.querySelector('.item-qty').value) || 0,
63
- price: parseFloat(row.querySelector('.item-price').value) || 0,
64
- discount: parseFloat(row.querySelector('.item-discount').value) || 0,
65
- amount: parseFloat(row.querySelector('.item-amount').textContent) || 0
66
- });
 
 
 
67
  });
 
 
 
 
 
68
  const { subtotal, igstAmount, _total, rOff, finalTotal } = calculateQuotation(items, igstRate, freightCharges);
69
 
70
  // convert total to words (Rupees and Paise)
@@ -205,52 +216,201 @@ if (typeof document !== 'undefined') {
205
  });
206
  }
207
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
208
  function addItemRow() {
209
  const row = document.createElement('tr');
210
  row.innerHTML = `
211
  <td class="item-slno" data-label="S.No"></td>
212
- <td data-label="Description"><input type="text" class="item-desc" placeholder="Item Description" required></td>
213
- <td data-label="HSN Code"><input type="text" class="item-hsn" placeholder="HSN Code"></td>
214
- <td data-label="Qty"><input type="number" class="item-qty" value="1" min="0" required></td>
215
- <td data-label="Unit Price"><input type="number" class="item-price" value="0" min="0" required></td>
216
- <td data-label="Discount (%)"><input type="number" class="item-discount" value="0" min="0" max="100" step="0.01" placeholder="Discount %"></td>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
217
  <td class="item-amount" data-label="Amount">0.00</td>
218
- <td data-label="Action"><button type="button" class="remove-item">Remove</button></td>
 
 
 
 
219
  `;
 
220
  itemsTableBody.appendChild(row);
221
  updateSerialNumbers();
 
 
222
  const inputs = row.querySelectorAll('input');
223
- inputs.forEach(input => input.addEventListener('input', () => {
224
- updateItemAmount(event);
225
- updatePreview();
226
- }));
227
- row.querySelector('.remove-item').addEventListener('click', () => {
228
- row.remove();
229
- updateSerialNumbers();
230
- updatePreview();
 
 
 
 
231
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
232
  }
233
 
234
- function updateItemAmount(e) {
235
- const row = e.target.closest('tr');
236
  if (!row) return;
 
237
  const qty = parseFloat(row.querySelector('.item-qty').value) || 0;
238
  const price = parseFloat(row.querySelector('.item-price').value) || 0;
239
  const discountRate = parseFloat(row.querySelector('.item-discount').value) || 0;
 
240
  const discountAmount = qty * price * discountRate / 100;
241
  const amount = qty * price - discountAmount;
 
242
  row.querySelector('.item-amount').textContent = amount.toFixed(2);
 
 
 
 
243
  }
244
 
 
245
  addItemBtn.addEventListener('click', () => {
246
  addItemRow();
247
  updatePreview();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
248
  });
249
 
250
  form.addEventListener('input', updatePreview);
251
 
252
- form.addEventListener('submit', function (e) {
253
- e.preventDefault();
 
 
 
 
 
 
 
 
 
 
 
254
  const html = generateQuotationHTML(form);
255
  output.innerHTML = html;
256
  output.style.display = 'block';
 
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 = {
 
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)
 
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">
281
+ <input type="text" class="item-desc" placeholder="Enter item description" required>
282
+ <span class="tooltiptext">Describe the item or service being quoted</span>
283
+ </div>
284
+ </td>
285
+ <td data-label="HSN Code">
286
+ <div class="tooltip">
287
+ <input type="text" class="item-hsn" placeholder="HSN Code">
288
+ <span class="tooltiptext">Harmonized System of Nomenclature code for tax purposes</span>
289
+ </div>
290
+ </td>
291
+ <td data-label="Qty">
292
+ <div class="tooltip">
293
+ <input type="number" class="item-qty" value="1" min="0" step="0.01" required>
294
+ <span class="tooltiptext">Quantity of items</span>
295
+ </div>
296
+ </td>
297
+ <td data-label="Unit Price">
298
+ <div class="tooltip">
299
+ <input type="number" class="item-price" value="0" min="0" step="0.01" required>
300
+ <span class="tooltiptext">Price per unit before discount</span>
301
+ </div>
302
+ </td>
303
+ <td data-label="Discount (%)">
304
+ <div class="tooltip">
305
+ <input type="number" class="item-discount" value="0" min="0" max="100" step="0.01" placeholder="0">
306
+ <span class="tooltiptext">Discount percentage (0-100)</span>
307
+ </div>
308
+ </td>
309
  <td class="item-amount" data-label="Amount">0.00</td>
310
+ <td data-label="Action">
311
+ <button type="button" class="remove-item" title="Remove this item">
312
+ <span>×</span> Remove
313
+ </button>
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';
style.css CHANGED
@@ -1,7 +1,198 @@
1
  @import "tailwindcss";
2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  #items-table input {
4
  width: 100%;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
  }
6
 
7
  .quotation-print {
 
1
  @import "tailwindcss";
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 {