@visns-studio/visns-components 5.1.24 → 5.1.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -77,7 +77,7 @@
77
77
  "react-dom": "^17.0.0 || ^18.0.0"
78
78
  },
79
79
  "name": "@visns-studio/visns-components",
80
- "version": "5.1.24",
80
+ "version": "5.1.25",
81
81
  "description": "Various packages to assist in the development of our Custom Applications.",
82
82
  "main": "src/index.js",
83
83
  "files": [
@@ -124,14 +124,16 @@ const GenericEditableTable = ({
124
124
  schedulingConfig,
125
125
  data,
126
126
  dataId,
127
- modalOpen,
128
127
  preloadedOptions,
129
128
  setData,
130
129
  }) => {
131
- const { columns, rows, form } = schedulingConfig;
130
+ const { columns, rows, form, title } = schedulingConfig;
132
131
  const hasCategory = rows && rows.categoryKey; // Only group if defined
133
132
 
134
133
  const [localData, setLocalData] = useState([]);
134
+ // For new (unsaved) entries: if grouped, store per group; if not, a single object.
135
+ const [newEntries, setNewEntries] = useState({});
136
+ const [newEntry, setNewEntry] = useState({});
135
137
 
136
138
  // Determine if a total column exists and its index.
137
139
  const totalColumnIndex = columns.findIndex((col) => col.type === 'total');
@@ -152,7 +154,6 @@ const GenericEditableTable = ({
152
154
  const calculateTotal = (entry, keys) => {
153
155
  let total = 0;
154
156
  if (Array.isArray(keys)) {
155
- // Legacy format: simply sum up the values.
156
157
  total = keys.reduce(
157
158
  (sum, key) => sum + (parseFloat(entry[key]) || 0),
158
159
  0
@@ -165,7 +166,6 @@ const GenericEditableTable = ({
165
166
  ) {
166
167
  const priceKeys = keys.price;
167
168
  const unitKeys = keys.unit;
168
- // If there's one unit key, use its value for all price keys.
169
169
  if (unitKeys.length === 1) {
170
170
  const unitValue = parseFloat(entry[unitKeys[0]]) || 0;
171
171
  total = priceKeys.reduce(
@@ -173,9 +173,7 @@ const GenericEditableTable = ({
173
173
  sum + (parseFloat(entry[key]) || 0) * unitValue,
174
174
  0
175
175
  );
176
- }
177
- // Otherwise, if both arrays have the same length, multiply pair‑wise.
178
- else if (priceKeys.length === unitKeys.length) {
176
+ } else if (priceKeys.length === unitKeys.length) {
179
177
  total = priceKeys.reduce(
180
178
  (sum, key, index) =>
181
179
  sum +
@@ -236,8 +234,10 @@ const GenericEditableTable = ({
236
234
 
237
235
  if (!grouped[groupKey]) {
238
236
  grouped[groupKey] = {
239
- id: categoryId || 'no_category',
240
- category: categoryType || '',
237
+ id: groupKey,
238
+ category:
239
+ categoryType ||
240
+ (groupKey === 'no_category' ? 'No Category' : ''),
241
241
  entries: [],
242
242
  };
243
243
  }
@@ -293,7 +293,7 @@ const GenericEditableTable = ({
293
293
  .catch(() => toast.error('Failed to copy to clipboard'));
294
294
  };
295
295
 
296
- // Update a field’s value (keyCounter corresponds to the row index in sorted data)
296
+ // Update an existing field’s value
297
297
  const handleFieldChange = (value, fieldId, keyCounter) => {
298
298
  setLocalData((prev) => {
299
299
  const updatedDetail = [...prev];
@@ -400,6 +400,7 @@ const GenericEditableTable = ({
400
400
  });
401
401
  };
402
402
 
403
+ // Render existing row field (same as before)
403
404
  const renderScheduledTableField = (entry, column, keyCounter) => {
404
405
  if (!entry || !column) return null;
405
406
 
@@ -524,6 +525,222 @@ const GenericEditableTable = ({
524
525
  }
525
526
  };
526
527
 
528
+ // --- New row functions for inline creation ---
529
+
530
+ // Update new entry fields (for grouped or ungrouped)
531
+ const handleNewFieldChange = (value, fieldId, groupId = null) => {
532
+ if (hasCategory) {
533
+ setNewEntries((prev) => ({
534
+ ...prev,
535
+ [groupId]: {
536
+ ...prev[groupId],
537
+ [fieldId]: value,
538
+ },
539
+ }));
540
+ } else {
541
+ setNewEntry((prev) => ({
542
+ ...prev,
543
+ [fieldId]: value,
544
+ }));
545
+ }
546
+ };
547
+
548
+ // Add the new row to localData and update the backend
549
+ const handleAddNewEntry = (groupId = null) => {
550
+ const newRowData = hasCategory ? newEntries[groupId] || {} : newEntry;
551
+ if (!newRowData || Object.keys(newRowData).length === 0) {
552
+ toast.error('Please fill in some data before adding.');
553
+ return;
554
+ }
555
+ // Determine a new sort_order
556
+ const maxSortOrder = localData.reduce(
557
+ (max, row) => Math.max(max, row.sort_order || 0),
558
+ 0
559
+ );
560
+ const newRow = {
561
+ ...newRowData,
562
+ id: `new-${Date.now()}`, // temporary ID; adjust as needed
563
+ sort_order: maxSortOrder + 1,
564
+ };
565
+
566
+ // For grouped data, prepopulate the category field if needed
567
+ if (hasCategory && groupId !== 'no_category') {
568
+ newRow[rows.categoryKey.id] = {
569
+ id: groupId,
570
+ [rows.categoryKey.label]: groupId,
571
+ };
572
+ }
573
+
574
+ setLocalData((prev) => {
575
+ const updated = [...prev, newRow];
576
+ debouncedUpdate({ [rows.key]: updated });
577
+ return updated;
578
+ });
579
+ // Reset the new entry state for the corresponding group or overall
580
+ if (hasCategory) {
581
+ setNewEntries((prev) => ({ ...prev, [groupId]: {} }));
582
+ } else {
583
+ setNewEntry({});
584
+ }
585
+ toast.success('Row added');
586
+ };
587
+
588
+ // Render a new row field (similar to renderScheduledTableField)
589
+ const renderNewRowField = (newRowData, column, groupId = null) => {
590
+ const value = newRowData ? newRowData[column.id] : '';
591
+ switch (column.type) {
592
+ case 'colour':
593
+ return (
594
+ <ColourPicker
595
+ value={value}
596
+ columnId={column.id}
597
+ keyCounter={0}
598
+ onChange={(val) =>
599
+ handleNewFieldChange(val, column.id, groupId)
600
+ }
601
+ />
602
+ );
603
+ case 'currency':
604
+ case 'number':
605
+ return (
606
+ <input
607
+ type="text"
608
+ value={value || ''}
609
+ onChange={(e) =>
610
+ handleNewFieldChange(
611
+ e.target.value,
612
+ column.id,
613
+ groupId
614
+ )
615
+ }
616
+ style={{ textAlign: 'right' }}
617
+ />
618
+ );
619
+ case 'date':
620
+ return (
621
+ <input
622
+ type="date"
623
+ value={value || ''}
624
+ onChange={(e) =>
625
+ handleNewFieldChange(
626
+ e.target.value,
627
+ column.id,
628
+ groupId
629
+ )
630
+ }
631
+ style={{ textAlign: 'right' }}
632
+ />
633
+ );
634
+ case 'dropdown':
635
+ return (
636
+ <select
637
+ value={value || ''}
638
+ onChange={(e) =>
639
+ handleNewFieldChange(
640
+ e.target.value,
641
+ column.id,
642
+ groupId
643
+ )
644
+ }
645
+ >
646
+ {column.options.map((option) => (
647
+ <option key={option.id} value={option.id}>
648
+ {option.label}
649
+ </option>
650
+ ))}
651
+ </select>
652
+ );
653
+ case 'dropdown-ajax': {
654
+ const options = preloadedOptions[column.id] || [];
655
+ return (
656
+ <select
657
+ value={value || ''}
658
+ onChange={(e) =>
659
+ handleNewFieldChange(
660
+ e.target.value,
661
+ column.id,
662
+ groupId
663
+ )
664
+ }
665
+ >
666
+ <option>Select an Option</option>
667
+ {options.map((option) => (
668
+ <option key={option.id} value={option.id}>
669
+ {option.label}
670
+ </option>
671
+ ))}
672
+ </select>
673
+ );
674
+ }
675
+ case 'multi-dropdown-ajax': {
676
+ const multiOptions = preloadedOptions[column.id] || [];
677
+ return (
678
+ <MultiSelect
679
+ settings={{ id: column.id, compact: true }}
680
+ className={styles.selectFullWidth}
681
+ multi={false}
682
+ onChange={(value) =>
683
+ handleNewFieldChange(value, column.id, groupId)
684
+ }
685
+ inputValue={value || ''}
686
+ options={multiOptions}
687
+ />
688
+ );
689
+ }
690
+ case 'total': {
691
+ const subtotal = calculateTotal(newRowData || {}, column.keys);
692
+ return new Intl.NumberFormat('en-AU', {
693
+ style: 'currency',
694
+ currency: 'AUD',
695
+ }).format(subtotal);
696
+ }
697
+ default:
698
+ return (
699
+ <input
700
+ type="text"
701
+ value={value || ''}
702
+ onChange={(e) =>
703
+ handleNewFieldChange(
704
+ e.target.value,
705
+ column.id,
706
+ groupId
707
+ )
708
+ }
709
+ />
710
+ );
711
+ }
712
+ };
713
+
714
+ // Render the new row form (for grouped or ungrouped data)
715
+ const renderNewRowForm = (groupId = null) => {
716
+ const newRowData = hasCategory ? newEntries[groupId] || {} : newEntry;
717
+ return (
718
+ <tr className={styles.newRow}>
719
+ {columns.map((column) => (
720
+ <td
721
+ key={column.id}
722
+ style={{
723
+ textAlign:
724
+ column.type === 'currency' ? 'right' : 'left',
725
+ }}
726
+ >
727
+ {renderNewRowField(newRowData, column, groupId)}
728
+ </td>
729
+ ))}
730
+ <td>
731
+ <div className={styles.tdactions}>
732
+ <button
733
+ className={styles.btn_compact}
734
+ onClick={() => handleAddNewEntry(groupId)}
735
+ >
736
+ Add
737
+ </button>
738
+ </div>
739
+ </td>
740
+ </tr>
741
+ );
742
+ };
743
+
527
744
  // Render header cell with a label and copy icon
528
745
  const renderHeaderCell = (column, group = null) => (
529
746
  <th
@@ -556,7 +773,7 @@ const GenericEditableTable = ({
556
773
  return (
557
774
  <div className={styles.gridtxt}>
558
775
  <div className={styles.gridtxt__header}>
559
- <span>{schedulingConfig.title || 'Estimation Schedule'}</span>
776
+ <span>{title || 'Estimation Schedule'}</span>
560
777
  </div>
561
778
  {hasCategory ? (
562
779
  groupedCategories.length > 0 ? (
@@ -652,6 +869,8 @@ const GenericEditableTable = ({
652
869
  </td>
653
870
  </tr>
654
871
  ))}
872
+ {/* Render the inline new row for this category */}
873
+ {renderNewRowForm(group.id)}
655
874
  {/* Render subtotal row for group only if a total column exists */}
656
875
  {totalColumnIndex !== -1 && (
657
876
  <tr className={styles.subtotalRow}>
@@ -781,6 +1000,8 @@ const GenericEditableTable = ({
781
1000
  </td>
782
1001
  </tr>
783
1002
  ))}
1003
+ {/* Render the inline new row at the bottom */}
1004
+ {renderNewRowForm()}
784
1005
  {/* Render overall subtotal row only if a total column exists */}
785
1006
  {totalColumnIndex !== -1 && (
786
1007
  <tr className={styles.subtotalRow}>
@@ -829,17 +1050,6 @@ const GenericEditableTable = ({
829
1050
  reloading the data.
830
1051
  </div>
831
1052
  )}
832
-
833
- <div className={styles.polActions}>
834
- <button
835
- className={styles.btn}
836
- onClick={() => {
837
- modalOpen('create', 0);
838
- }}
839
- >
840
- Add
841
- </button>
842
- </div>
843
1053
  </div>
844
1054
  );
845
1055
  };
@@ -144,12 +144,35 @@
144
144
  outline: none;
145
145
  transition: all 0.2s cubic-bezier(0.85, 0, 0.15, 1) 0s;
146
146
  font-size: 1.25em;
147
+
148
+ &:hover {
149
+ color: var(--primary-color);
150
+ background: var(--highlight-color);
151
+ border: 1px solid rgba(var(--highlight-rgb), 1.05);
152
+ }
147
153
  }
148
154
 
149
- .btn:hover {
150
- color: var(--primary-color);
151
- background: var(--highlight-color);
152
- border: 1px solid rgba(var(--highlight-rgb), 1.05);
155
+ .btn_compact {
156
+ width: max-content;
157
+ display: inline-block;
158
+ position: relative;
159
+ padding: 0.4rem 0.75rem; /* Reduced padding for a compact look */
160
+ cursor: pointer;
161
+ font-size: 1rem; /* Slightly smaller font size */
162
+ color: var(--tertiary-color);
163
+ text-decoration: none;
164
+ overflow: hidden;
165
+ background: var(--primary-color);
166
+ border: 1px solid rgba(var(--primary-color--rgb), 1.1);
167
+ border-radius: var(--br);
168
+ outline: none;
169
+ transition: all 0.2s cubic-bezier(0.85, 0, 0.15, 1) 0s;
170
+
171
+ &:hover {
172
+ color: var(--primary-color);
173
+ background: var(--highlight-color);
174
+ border: 1px solid rgba(var(--highlight-rgb), 1.05);
175
+ }
153
176
  }
154
177
 
155
178
  .tdactions {
@@ -326,3 +349,12 @@
326
349
  .react-select__indicators {
327
350
  padding: 0 0.25rem;
328
351
  }
352
+
353
+ .newRow {
354
+ background-color: rgba(
355
+ var(--primary-rgb),
356
+ 0.15
357
+ ); /* Slightly lighter background */
358
+ border-top: 1px dashed rgba(var(--primary-rgb), 0.3);
359
+ border-bottom: 1px dashed rgba(var(--primary-rgb), 0.3);
360
+ }