@visns-studio/visns-components 5.0.0 → 5.0.1-8.1

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.
Files changed (37) hide show
  1. package/package.json +3 -1
  2. package/src/components/crm/AsyncSelect.jsx +121 -15
  3. package/src/components/crm/DataGrid.jsx +26 -5
  4. package/src/components/crm/Field.jsx +174 -148
  5. package/src/components/crm/Form.jsx +46 -33
  6. package/src/components/crm/MultiSelect.jsx +96 -29
  7. package/src/components/crm/Navigation.jsx +2 -81
  8. package/src/components/crm/QrCode.jsx +12 -8
  9. package/src/components/crm/auth/Login.jsx +9 -1
  10. package/src/components/crm/auth/Profile.jsx +4 -2
  11. package/src/components/crm/auth/styles/Login.module.scss +52 -82
  12. package/src/components/crm/auth/styles/Profile.module.scss +150 -44
  13. package/src/components/crm/auth/styles/Reset.module.scss +55 -37
  14. package/src/components/crm/auth/styles/Verify.module.scss +55 -76
  15. package/src/components/crm/generic/GenericAuth.jsx +17 -6
  16. package/src/components/crm/generic/GenericDetail.jsx +509 -18
  17. package/src/components/crm/generic/GenericEditableTable.jsx +407 -0
  18. package/src/components/crm/generic/styles/AuditLog.module.scss +4 -37
  19. package/src/components/crm/generic/styles/AuditLogs.module.scss +0 -51
  20. package/src/components/crm/generic/styles/GenericDetail.module.scss +263 -193
  21. package/src/components/crm/generic/styles/GenericDynamic.module.scss +100 -198
  22. package/src/components/crm/generic/styles/GenericEditableTable.module.scss +178 -0
  23. package/src/components/crm/generic/styles/GenericFormBuilder.module.scss +63 -96
  24. package/src/components/crm/generic/styles/GenericIndex.module.scss +46 -66
  25. package/src/components/crm/generic/styles/GenericMain.module.scss +7 -7
  26. package/src/components/crm/generic/styles/GenericSort.module.scss +0 -36
  27. package/src/components/crm/generic/styles/NotificationList.module.scss +1 -32
  28. package/src/components/crm/styles/Autocomplete.module.scss +12 -20
  29. package/src/components/crm/styles/DataGrid.module.scss +6 -89
  30. package/src/components/crm/styles/Field.module.scss +72 -42
  31. package/src/components/crm/styles/Form.module.scss +148 -379
  32. package/src/components/crm/styles/Navigation.module.scss +356 -501
  33. package/src/components/crm/styles/Notification.module.scss +1 -0
  34. package/src/components/crm/styles/QrCode.module.scss +4 -0
  35. package/src/components/crm/styles/SwitchAccount.module.scss +0 -1
  36. package/src/components/crm/styles/TableFilter.module.scss +2 -1
  37. package/src/components/styles/global.css +164 -34
@@ -0,0 +1,407 @@
1
+ import React, { useEffect, useMemo, useState } from 'react';
2
+ import debounce from 'lodash.debounce';
3
+ import { toast } from 'react-toastify';
4
+ import Popup from 'reactjs-popup';
5
+ import { confirmAlert } from 'react-confirm-alert';
6
+ import { TrashCan, ArrowUp, ArrowDown } from 'akar-icons';
7
+
8
+ import CustomFetch from '../Fetch';
9
+ import Form from '../Form';
10
+ import MultiSelect from '../MultiSelect';
11
+
12
+ import 'react-confirm-alert/src/react-confirm-alert.css';
13
+ import styles from './styles/GenericEditableTable.module.scss';
14
+
15
+ const GenericEditableTable = ({
16
+ schedulingConfig,
17
+ data,
18
+ dataId,
19
+ modalOpen,
20
+ preloadedOptions,
21
+ }) => {
22
+ const { columns, rows, form } = schedulingConfig;
23
+
24
+ const [localData, setLocalData] = useState([]);
25
+ const [groupedCategories, setGroupedCategories] = useState([]);
26
+
27
+ useEffect(() => {
28
+ const enrichedData = data[rows.key]?.map((entry, index) => ({
29
+ ...entry,
30
+ sort_order: entry.sort_order ?? index + 1,
31
+ }));
32
+ setLocalData(enrichedData || []);
33
+ }, [data, rows.key]);
34
+
35
+ const debouncedUpdate = useMemo(
36
+ () =>
37
+ debounce(async (updatedData) => {
38
+ try {
39
+ const res = await CustomFetch(
40
+ `${form.url}/${dataId}`,
41
+ 'PUT',
42
+ updatedData
43
+ );
44
+ if (!res.error) {
45
+ toast.success('Template updated successfully');
46
+ } else {
47
+ toast.error(res.error);
48
+ }
49
+ } catch (error) {
50
+ console.error('Error updating field:', error);
51
+ }
52
+ }, 1000),
53
+ []
54
+ );
55
+
56
+ const groupCategoriesByType = (data) => {
57
+ const grouped = {};
58
+
59
+ data.forEach((category, categoryKey) => {
60
+ const categoryType =
61
+ category[rows.categoryKey.id]?.[rows.categoryKey.label] || null;
62
+
63
+ const categoryId = category[rows.categoryKey.id]?.id || null;
64
+
65
+ const groupKey = categoryId || 'no_category';
66
+
67
+ if (!grouped[groupKey]) {
68
+ grouped[groupKey] = {
69
+ id: categoryId || 'no_category',
70
+ category: categoryType || '',
71
+ entries: [],
72
+ };
73
+ }
74
+ grouped[groupKey].entries.push({
75
+ ...category,
76
+ keyCounter: categoryKey,
77
+ });
78
+ });
79
+
80
+ Object.values(grouped).forEach((group) => {
81
+ group.entries.sort((a, b) => a.sort_order - b.sort_order);
82
+ });
83
+
84
+ // Ensure 'no_category' group is at the top
85
+ const groupedArray = Object.values(grouped);
86
+ return groupedArray.sort((a, b) =>
87
+ a.id === 'no_category' ? -1 : b.id === 'no_category' ? 1 : 0
88
+ );
89
+ };
90
+
91
+ const handleFieldChange = (value, fieldId, keyCounter) => {
92
+ setLocalData((prev) => {
93
+ const updatedDetail = [...prev];
94
+ updatedDetail[keyCounter] = {
95
+ ...updatedDetail[keyCounter],
96
+ [fieldId]: value,
97
+ };
98
+
99
+ debouncedUpdate({ [rows.key]: updatedDetail });
100
+
101
+ return updatedDetail;
102
+ });
103
+ };
104
+
105
+ const handleSortChange = (groupId, entryId, direction) => {
106
+ setLocalData((prev) => {
107
+ const updatedDetail = [...prev];
108
+ const group = groupCategoriesByType(updatedDetail).find(
109
+ (group) => group.id === groupId
110
+ );
111
+
112
+ if (group) {
113
+ const index = group.entries.findIndex(
114
+ (entry) => entry.id === entryId
115
+ );
116
+
117
+ if (
118
+ (direction === 'up' && index > 0) ||
119
+ (direction === 'down' && index < group.entries.length - 1)
120
+ ) {
121
+ const swapIndex =
122
+ direction === 'up' ? index - 1 : index + 1;
123
+ [
124
+ group.entries[index].sort_order,
125
+ group.entries[swapIndex].sort_order,
126
+ ] = [
127
+ group.entries[swapIndex].sort_order,
128
+ group.entries[index].sort_order,
129
+ ];
130
+
131
+ const reordered = updatedDetail.map((item) => {
132
+ const entry = group.entries.find(
133
+ (e) => e.id === item.id
134
+ );
135
+ return entry
136
+ ? { ...item, sort_order: entry.sort_order }
137
+ : item;
138
+ });
139
+
140
+ debouncedUpdate({ [rows.key]: reordered });
141
+ return reordered;
142
+ }
143
+ }
144
+
145
+ return updatedDetail;
146
+ });
147
+ };
148
+
149
+ const handleDeleteRow = (entry) => {
150
+ confirmAlert({
151
+ title: 'Confirm to Delete',
152
+ message: 'Are you sure you want to delete this entry?',
153
+ buttons: [
154
+ {
155
+ label: 'Yes',
156
+ onClick: async () => {
157
+ setLocalData((prev) => {
158
+ const updatedDetail = prev.filter(
159
+ (item) => item.id !== entry.id
160
+ );
161
+ debouncedUpdate({ [rows.key]: updatedDetail });
162
+ return updatedDetail;
163
+ });
164
+ },
165
+ },
166
+ {
167
+ label: 'No',
168
+ },
169
+ ],
170
+ });
171
+ };
172
+
173
+ const renderScheduledTableField = (entry, column, keyCounter) => {
174
+ if (!entry || !column) return null;
175
+
176
+ switch (column.type) {
177
+ case 'currency':
178
+ case 'number':
179
+ return (
180
+ <input
181
+ type="text"
182
+ value={entry[column.id] || ''}
183
+ onChange={(e) =>
184
+ handleFieldChange(
185
+ e.target.value,
186
+ column.id,
187
+ keyCounter
188
+ )
189
+ }
190
+ style={{ textAlign: 'right' }}
191
+ />
192
+ );
193
+ case 'date':
194
+ return (
195
+ <input
196
+ type="date"
197
+ value={entry[column.id] || ''}
198
+ onChange={(e) =>
199
+ handleFieldChange(
200
+ e.target.value,
201
+ column.id,
202
+ keyCounter
203
+ )
204
+ }
205
+ style={{ textAlign: 'right' }}
206
+ />
207
+ );
208
+ case 'dropdown':
209
+ return (
210
+ <select
211
+ value={entry[column.id] || ''}
212
+ onChange={(e) =>
213
+ handleFieldChange(
214
+ e.target.value,
215
+ column.id,
216
+ keyCounter
217
+ )
218
+ }
219
+ >
220
+ {column.options.map((option) => (
221
+ <option key={option.id} value={option.id}>
222
+ {option.label}
223
+ </option>
224
+ ))}
225
+ </select>
226
+ );
227
+ case 'dropdown-ajax':
228
+ const options = preloadedOptions[column.id] || [];
229
+ return (
230
+ <select
231
+ value={entry[column.id] || ''}
232
+ onChange={(e) =>
233
+ handleFieldChange(
234
+ e.target.value,
235
+ column.id,
236
+ keyCounter
237
+ )
238
+ }
239
+ >
240
+ <option>Select an Option</option>
241
+ {options.map((option) => (
242
+ <option key={option.id} value={option.id}>
243
+ {option.label}
244
+ </option>
245
+ ))}
246
+ </select>
247
+ );
248
+ case 'multi-dropdown-ajax':
249
+ const multiOptions = preloadedOptions[column.id] || [];
250
+ return (
251
+ <MultiSelect
252
+ settings={{
253
+ id: column.id,
254
+ }}
255
+ className={styles.selectFullWidth}
256
+ multi={false}
257
+ onChange={(value) =>
258
+ handleFieldChange(value, column.id, keyCounter)
259
+ }
260
+ inputValue={entry[column.id] || ''}
261
+ options={multiOptions}
262
+ />
263
+ );
264
+ default:
265
+ return (
266
+ <input
267
+ type="text"
268
+ defaultValue={entry[column.id] || ''}
269
+ onChange={(e) =>
270
+ handleFieldChange(
271
+ e.target.value,
272
+ column.id,
273
+ keyCounter
274
+ )
275
+ }
276
+ />
277
+ );
278
+ }
279
+ };
280
+
281
+ useEffect(() => {
282
+ setGroupedCategories(groupCategoriesByType(localData));
283
+ }, [localData]);
284
+
285
+ return (
286
+ <div className={styles.gridtxt}>
287
+ <div className={styles.gridtxt__header}>
288
+ <span>{schedulingConfig.title || 'Estimation Schedule'}</span>
289
+ </div>
290
+ {groupedCategories.length > 0 ? (
291
+ <table className={styles.schedulingTable}>
292
+ <thead>
293
+ <tr>
294
+ {columns.map((column) => (
295
+ <th
296
+ key={column.id}
297
+ style={{ width: column.width || 'auto' }}
298
+ >
299
+ {column.label}
300
+ </th>
301
+ ))}
302
+ <th style={{ width: '5%' }}></th>
303
+ </tr>
304
+ </thead>
305
+ <tbody>
306
+ {groupedCategories.map((group) => (
307
+ <React.Fragment key={group.id}>
308
+ <tr className={styles.categoryRow}>
309
+ <td colSpan={columns.length + 1}>
310
+ {group.category}
311
+ </td>
312
+ </tr>
313
+ {group.entries.map((entry, idx) => (
314
+ <tr key={entry.id}>
315
+ {columns.map((column) => (
316
+ <td
317
+ key={column.id}
318
+ style={{
319
+ width:
320
+ column.width || 'auto',
321
+ textAlign:
322
+ column.type ===
323
+ 'currency'
324
+ ? 'right'
325
+ : 'left',
326
+ }}
327
+ >
328
+ {renderScheduledTableField(
329
+ entry,
330
+ column,
331
+ entry.keyCounter
332
+ )}
333
+ </td>
334
+ ))}
335
+ <td>
336
+ <div className={styles.tdactions}>
337
+ {idx > 0 && (
338
+ <ArrowUp
339
+ size={18}
340
+ onClick={() =>
341
+ handleSortChange(
342
+ group.id,
343
+ entry.id,
344
+ 'up'
345
+ )
346
+ }
347
+ style={{
348
+ cursor: 'pointer',
349
+ }}
350
+ />
351
+ )}
352
+ {idx <
353
+ group.entries.length -
354
+ 1 && (
355
+ <ArrowDown
356
+ size={18}
357
+ onClick={() =>
358
+ handleSortChange(
359
+ group.id,
360
+ entry.id,
361
+ 'down'
362
+ )
363
+ }
364
+ style={{
365
+ cursor: 'pointer',
366
+ }}
367
+ />
368
+ )}
369
+ <TrashCan
370
+ size={18}
371
+ onClick={() =>
372
+ handleDeleteRow(entry)
373
+ }
374
+ style={{
375
+ cursor: 'pointer',
376
+ }}
377
+ />
378
+ </div>
379
+ </td>
380
+ </tr>
381
+ ))}
382
+ </React.Fragment>
383
+ ))}
384
+ </tbody>
385
+ </table>
386
+ ) : (
387
+ <div className={styles.noDataMessage}>
388
+ No data available. Please check your filters or try
389
+ reloading the data.
390
+ </div>
391
+ )}
392
+
393
+ <div className={styles.polActions}>
394
+ <button
395
+ className={styles.btn}
396
+ onClick={() => {
397
+ modalOpen('create', 0);
398
+ }}
399
+ >
400
+ Add
401
+ </button>
402
+ </div>
403
+ </div>
404
+ );
405
+ };
406
+
407
+ export default GenericEditableTable;
@@ -37,43 +37,6 @@
37
37
  box-shadow: 0 4px 0 rgba(var(--primary-color-rgb), 0.05);
38
38
  }
39
39
 
40
- .crmtitle {
41
- border: none;
42
- min-height: auto;
43
- padding: 5px 20px;
44
- margin: 0;
45
- background: var(--primary-color);
46
- border-radius: var(--br);
47
- border: 1px solid rgba(var(--item-color-rgb), 1.15);
48
- position: relative;
49
- display: flex;
50
- align-items: center;
51
-
52
- h1 {
53
- color: var(--background-color);
54
- font-weight: 700;
55
- font-size: 1.15em;
56
- margin: 0;
57
- padding: 0.65rem 0;
58
- display: flex;
59
- align-items: center;
60
- gap: 0.25rem;
61
-
62
- a {
63
- text-decoration: none;
64
- color: var(--secondary-color);
65
- }
66
-
67
- svg {
68
- color: rgba(var(--tertiary-color-rgb), 0.5);
69
- position: relative;
70
- top: 0;
71
- margin: 0 0.5rem;
72
- max-width: 15px;
73
- }
74
- }
75
- }
76
-
77
40
  .gridtxt__header {
78
41
  width: 100%;
79
42
  display: block;
@@ -143,5 +106,9 @@
143
106
  background: rgba(#f4f4f4, 0.65);
144
107
  margin: 5px;
145
108
  border-radius: 5px;
109
+
110
+ p {
111
+ font-size: 0.85rem;
112
+ }
146
113
  }
147
114
  }
@@ -29,57 +29,6 @@
29
29
  box-shadow: 0 4px 0 rgba(var(--primary-color-rgb), 0.05);
30
30
  }
31
31
 
32
- .crmtitle {
33
- border: none;
34
- min-height: auto;
35
- padding: 10px 20px;
36
- margin: 0;
37
- background: var(--tertiary-color);
38
- border-radius: var(--br);
39
- border: 1px solid rgba(var(--item-color-rgb), 1.15);
40
- position: relative;
41
- }
42
-
43
- .crmtitle h1 {
44
- font-weight: 700;
45
- font-size: 1.45em;
46
- margin: 0;
47
- padding: 0.65rem 0;
48
- }
49
-
50
- .crmtitle h1 a {
51
- text-decoration: none;
52
- color: rgba(var(--secondary-color-rgb), 1.1);
53
- font-weight: 300;
54
- }
55
-
56
- .crmtitle h1 svg {
57
- color: rgba(var(--primary-color-rgb), 0.25);
58
- position: relative;
59
- top: 2px;
60
- margin: 0 0.25rem;
61
- }
62
-
63
- .crmtitleSimple {
64
- border: none;
65
- min-height: auto;
66
- padding: 0.65rem 1rem;
67
- margin-bottom: 0.15rem;
68
- background: var(--tertiary-color);
69
- border-radius: var(--br);
70
- box-sizing: border-box;
71
- display: flex;
72
- flex-wrap: nowrap;
73
- justify-content: space-between;
74
- align-items: center;
75
-
76
- h1 {
77
- font-size: 1.25rem;
78
- margin: 0;
79
- padding: 0;
80
- }
81
- }
82
-
83
32
  .titleInfo {
84
33
  position: absolute;
85
34
  right: 2em;