@visns-studio/visns-components 5.1.24 → 5.1.26

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.26",
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 +
@@ -228,16 +226,27 @@ const GenericEditableTable = ({
228
226
 
229
227
  dataArray.forEach((entry, index) => {
230
228
  const categoryInfo = entry[rows.categoryKey.id];
231
- const categoryType = categoryInfo
229
+ const categoryLabel = categoryInfo
232
230
  ? categoryInfo[rows.categoryKey.label]
233
231
  : null;
234
232
  const categoryId = categoryInfo ? categoryInfo.id : null;
235
233
  const groupKey = categoryId || 'no_category';
234
+ // Use the category's sort_order if provided; otherwise, default to a high number (e.g. 9999)
235
+
236
+ console.info(categoryInfo);
237
+
238
+ const catSortOrder =
239
+ categoryInfo && typeof categoryInfo.sort_order !== 'undefined'
240
+ ? categoryInfo.sort_order
241
+ : 9999;
236
242
 
237
243
  if (!grouped[groupKey]) {
238
244
  grouped[groupKey] = {
239
- id: categoryId || 'no_category',
240
- category: categoryType || '',
245
+ id: groupKey,
246
+ category:
247
+ categoryLabel ||
248
+ (groupKey === 'no_category' ? 'No Category' : ''),
249
+ catSortOrder, // assign the sort order (or fallback)
241
250
  entries: [],
242
251
  };
243
252
  }
@@ -247,15 +256,23 @@ const GenericEditableTable = ({
247
256
  });
248
257
  });
249
258
 
259
+ // Sort entries within each group by their universal sort_order
250
260
  Object.values(grouped).forEach((group) => {
251
261
  group.entries.sort((a, b) => a.sort_order - b.sort_order);
252
262
  });
253
263
 
254
- // Ensure 'no_category' group is at the top
264
+ // Now sort the groups by the cost category's sort_order (catSortOrder)
255
265
  const groupedArray = Object.values(grouped);
256
- return groupedArray.sort((a, b) =>
257
- a.id === 'no_category' ? -1 : b.id === 'no_category' ? 1 : 0
258
- );
266
+ groupedArray.sort((a, b) => {
267
+ // Optionally, place 'no_category' first (or adjust as needed)
268
+ if (a.id === 'no_category') return -1;
269
+ if (b.id === 'no_category') return 1;
270
+ return a.catSortOrder - b.catSortOrder;
271
+ });
272
+
273
+ console.info(groupedArray);
274
+
275
+ return groupedArray;
259
276
  };
260
277
 
261
278
  const groupedCategories = useMemo(() => {
@@ -293,7 +310,7 @@ const GenericEditableTable = ({
293
310
  .catch(() => toast.error('Failed to copy to clipboard'));
294
311
  };
295
312
 
296
- // Update a field’s value (keyCounter corresponds to the row index in sorted data)
313
+ // Update an existing field’s value
297
314
  const handleFieldChange = (value, fieldId, keyCounter) => {
298
315
  setLocalData((prev) => {
299
316
  const updatedDetail = [...prev];
@@ -400,6 +417,7 @@ const GenericEditableTable = ({
400
417
  });
401
418
  };
402
419
 
420
+ // Render existing row field (same as before)
403
421
  const renderScheduledTableField = (entry, column, keyCounter) => {
404
422
  if (!entry || !column) return null;
405
423
 
@@ -524,6 +542,222 @@ const GenericEditableTable = ({
524
542
  }
525
543
  };
526
544
 
545
+ // --- New row functions for inline creation ---
546
+
547
+ // Update new entry fields (for grouped or ungrouped)
548
+ const handleNewFieldChange = (value, fieldId, groupId = null) => {
549
+ if (hasCategory) {
550
+ setNewEntries((prev) => ({
551
+ ...prev,
552
+ [groupId]: {
553
+ ...prev[groupId],
554
+ [fieldId]: value,
555
+ },
556
+ }));
557
+ } else {
558
+ setNewEntry((prev) => ({
559
+ ...prev,
560
+ [fieldId]: value,
561
+ }));
562
+ }
563
+ };
564
+
565
+ // Add the new row to localData and update the backend
566
+ const handleAddNewEntry = (groupId = null) => {
567
+ const newRowData = hasCategory ? newEntries[groupId] || {} : newEntry;
568
+ if (!newRowData || Object.keys(newRowData).length === 0) {
569
+ toast.error('Please fill in some data before adding.');
570
+ return;
571
+ }
572
+ // Determine a new sort_order
573
+ const maxSortOrder = localData.reduce(
574
+ (max, row) => Math.max(max, row.sort_order || 0),
575
+ 0
576
+ );
577
+ const newRow = {
578
+ ...newRowData,
579
+ id: `new-${Date.now()}`, // temporary ID; adjust as needed
580
+ sort_order: maxSortOrder + 1,
581
+ };
582
+
583
+ // For grouped data, prepopulate the category field if needed
584
+ if (hasCategory && groupId !== 'no_category') {
585
+ newRow[rows.categoryKey.id] = {
586
+ id: groupId,
587
+ [rows.categoryKey.label]: groupId,
588
+ };
589
+ }
590
+
591
+ setLocalData((prev) => {
592
+ const updated = [...prev, newRow];
593
+ debouncedUpdate({ [rows.key]: updated });
594
+ return updated;
595
+ });
596
+ // Reset the new entry state for the corresponding group or overall
597
+ if (hasCategory) {
598
+ setNewEntries((prev) => ({ ...prev, [groupId]: {} }));
599
+ } else {
600
+ setNewEntry({});
601
+ }
602
+ toast.success('Row added');
603
+ };
604
+
605
+ // Render a new row field (similar to renderScheduledTableField)
606
+ const renderNewRowField = (newRowData, column, groupId = null) => {
607
+ const value = newRowData ? newRowData[column.id] : '';
608
+ switch (column.type) {
609
+ case 'colour':
610
+ return (
611
+ <ColourPicker
612
+ value={value}
613
+ columnId={column.id}
614
+ keyCounter={0}
615
+ onChange={(val) =>
616
+ handleNewFieldChange(val, column.id, groupId)
617
+ }
618
+ />
619
+ );
620
+ case 'currency':
621
+ case 'number':
622
+ return (
623
+ <input
624
+ type="text"
625
+ value={value || ''}
626
+ onChange={(e) =>
627
+ handleNewFieldChange(
628
+ e.target.value,
629
+ column.id,
630
+ groupId
631
+ )
632
+ }
633
+ style={{ textAlign: 'right' }}
634
+ />
635
+ );
636
+ case 'date':
637
+ return (
638
+ <input
639
+ type="date"
640
+ value={value || ''}
641
+ onChange={(e) =>
642
+ handleNewFieldChange(
643
+ e.target.value,
644
+ column.id,
645
+ groupId
646
+ )
647
+ }
648
+ style={{ textAlign: 'right' }}
649
+ />
650
+ );
651
+ case 'dropdown':
652
+ return (
653
+ <select
654
+ value={value || ''}
655
+ onChange={(e) =>
656
+ handleNewFieldChange(
657
+ e.target.value,
658
+ column.id,
659
+ groupId
660
+ )
661
+ }
662
+ >
663
+ {column.options.map((option) => (
664
+ <option key={option.id} value={option.id}>
665
+ {option.label}
666
+ </option>
667
+ ))}
668
+ </select>
669
+ );
670
+ case 'dropdown-ajax': {
671
+ const options = preloadedOptions[column.id] || [];
672
+ return (
673
+ <select
674
+ value={value || ''}
675
+ onChange={(e) =>
676
+ handleNewFieldChange(
677
+ e.target.value,
678
+ column.id,
679
+ groupId
680
+ )
681
+ }
682
+ >
683
+ <option>Select an Option</option>
684
+ {options.map((option) => (
685
+ <option key={option.id} value={option.id}>
686
+ {option.label}
687
+ </option>
688
+ ))}
689
+ </select>
690
+ );
691
+ }
692
+ case 'multi-dropdown-ajax': {
693
+ const multiOptions = preloadedOptions[column.id] || [];
694
+ return (
695
+ <MultiSelect
696
+ settings={{ id: column.id, compact: true }}
697
+ className={styles.selectFullWidth}
698
+ multi={false}
699
+ onChange={(value) =>
700
+ handleNewFieldChange(value, column.id, groupId)
701
+ }
702
+ inputValue={value || ''}
703
+ options={multiOptions}
704
+ />
705
+ );
706
+ }
707
+ case 'total': {
708
+ const subtotal = calculateTotal(newRowData || {}, column.keys);
709
+ return new Intl.NumberFormat('en-AU', {
710
+ style: 'currency',
711
+ currency: 'AUD',
712
+ }).format(subtotal);
713
+ }
714
+ default:
715
+ return (
716
+ <input
717
+ type="text"
718
+ value={value || ''}
719
+ onChange={(e) =>
720
+ handleNewFieldChange(
721
+ e.target.value,
722
+ column.id,
723
+ groupId
724
+ )
725
+ }
726
+ />
727
+ );
728
+ }
729
+ };
730
+
731
+ // Render the new row form (for grouped or ungrouped data)
732
+ const renderNewRowForm = (groupId = null) => {
733
+ const newRowData = hasCategory ? newEntries[groupId] || {} : newEntry;
734
+ return (
735
+ <tr className={styles.newRow}>
736
+ {columns.map((column) => (
737
+ <td
738
+ key={column.id}
739
+ style={{
740
+ textAlign:
741
+ column.type === 'currency' ? 'right' : 'left',
742
+ }}
743
+ >
744
+ {renderNewRowField(newRowData, column, groupId)}
745
+ </td>
746
+ ))}
747
+ <td>
748
+ <div className={styles.tdactions}>
749
+ <button
750
+ className={styles.btn_compact}
751
+ onClick={() => handleAddNewEntry(groupId)}
752
+ >
753
+ Add
754
+ </button>
755
+ </div>
756
+ </td>
757
+ </tr>
758
+ );
759
+ };
760
+
527
761
  // Render header cell with a label and copy icon
528
762
  const renderHeaderCell = (column, group = null) => (
529
763
  <th
@@ -556,7 +790,7 @@ const GenericEditableTable = ({
556
790
  return (
557
791
  <div className={styles.gridtxt}>
558
792
  <div className={styles.gridtxt__header}>
559
- <span>{schedulingConfig.title || 'Estimation Schedule'}</span>
793
+ <span>{title || 'Estimation Schedule'}</span>
560
794
  </div>
561
795
  {hasCategory ? (
562
796
  groupedCategories.length > 0 ? (
@@ -652,6 +886,8 @@ const GenericEditableTable = ({
652
886
  </td>
653
887
  </tr>
654
888
  ))}
889
+ {/* Render the inline new row for this category */}
890
+ {renderNewRowForm(group.id)}
655
891
  {/* Render subtotal row for group only if a total column exists */}
656
892
  {totalColumnIndex !== -1 && (
657
893
  <tr className={styles.subtotalRow}>
@@ -781,6 +1017,8 @@ const GenericEditableTable = ({
781
1017
  </td>
782
1018
  </tr>
783
1019
  ))}
1020
+ {/* Render the inline new row at the bottom */}
1021
+ {renderNewRowForm()}
784
1022
  {/* Render overall subtotal row only if a total column exists */}
785
1023
  {totalColumnIndex !== -1 && (
786
1024
  <tr className={styles.subtotalRow}>
@@ -829,17 +1067,6 @@ const GenericEditableTable = ({
829
1067
  reloading the data.
830
1068
  </div>
831
1069
  )}
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
1070
  </div>
844
1071
  );
845
1072
  };
@@ -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
+ }