@visns-studio/visns-components 5.7.0 → 5.7.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.
- package/package.json +1 -4
- package/src/components/cms/DataGrid.jsx +2 -2
- package/src/components/crm/Autocomplete.jsx +161 -127
- package/src/components/crm/DataGrid.jsx +2 -2
- package/src/components/crm/Download.jsx +15 -4
- package/src/components/crm/Fetch.jsx +69 -16
- package/src/components/crm/Notification.jsx +237 -106
- package/src/components/crm/QuickAction.jsx +318 -91
- package/src/components/crm/generic/GenericDetail.jsx +43 -127
- package/src/components/crm/generic/NotificationList.jsx +76 -7
- package/src/components/crm/generic/styles/NotificationList.module.scss +90 -2
- package/src/components/crm/styles/DataGrid.module.scss +5 -0
- package/src/components/crm/styles/Notification.module.scss +313 -96
- package/src/components/crm/styles/QuickAction.module.scss +168 -46
- package/src/components/styles/global.css +5 -0
- package/src/utils/fetchUtil.js +240 -0
|
@@ -33,6 +33,7 @@ import { CopyToClipboard } from 'react-copy-to-clipboard';
|
|
|
33
33
|
import { saveAs } from 'file-saver';
|
|
34
34
|
import { toast } from 'react-toastify';
|
|
35
35
|
import imageCompression from 'browser-image-compression';
|
|
36
|
+
import fetchUtil from '../../../utils/fetchUtil';
|
|
36
37
|
import {
|
|
37
38
|
CircleCheck,
|
|
38
39
|
Copy,
|
|
@@ -153,36 +154,24 @@ function GenericDetail({
|
|
|
153
154
|
'Processing file... Please wait!'
|
|
154
155
|
);
|
|
155
156
|
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
.getAttribute('content');
|
|
159
|
-
const response = await fetch('/ajax/ocr/analyzeFile', {
|
|
157
|
+
const response = await fetchUtil({
|
|
158
|
+
url: '/ajax/ocr/analyzeFile',
|
|
160
159
|
method: 'POST',
|
|
161
|
-
|
|
162
|
-
body: formData,
|
|
160
|
+
data: formData,
|
|
163
161
|
});
|
|
164
162
|
|
|
165
|
-
|
|
166
|
-
const result = await response.json();
|
|
163
|
+
const result = response.data;
|
|
167
164
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
} else {
|
|
179
|
-
toast.update(loadingToast, {
|
|
180
|
-
render: 'Error processing file.',
|
|
181
|
-
type: 'error',
|
|
182
|
-
isLoading: false,
|
|
183
|
-
autoClose: 3000,
|
|
184
|
-
});
|
|
185
|
-
}
|
|
165
|
+
setData((prevState) => ({
|
|
166
|
+
...prevState,
|
|
167
|
+
[id]: result,
|
|
168
|
+
}));
|
|
169
|
+
toast.update(loadingToast, {
|
|
170
|
+
render: 'File processed successfully!',
|
|
171
|
+
type: 'success',
|
|
172
|
+
isLoading: false,
|
|
173
|
+
autoClose: 3000,
|
|
174
|
+
});
|
|
186
175
|
} catch (error) {
|
|
187
176
|
console.error(error);
|
|
188
177
|
toast.update(loadingToast, {
|
|
@@ -1141,100 +1130,20 @@ function GenericDetail({
|
|
|
1141
1130
|
file_url ||
|
|
1142
1131
|
`/ajax/files/downloadContent/${id}`;
|
|
1143
1132
|
|
|
1144
|
-
// Use
|
|
1145
|
-
|
|
1133
|
+
// Use fetchUtil to retrieve the file as a blob
|
|
1134
|
+
fetchUtil({
|
|
1135
|
+
url: urlToDownload,
|
|
1146
1136
|
method: 'GET',
|
|
1147
1137
|
headers: {
|
|
1148
1138
|
'Content-Type':
|
|
1149
1139
|
'application/octet-stream', // Specify content type as binary
|
|
1150
1140
|
},
|
|
1141
|
+
responseType: 'blob',
|
|
1151
1142
|
})
|
|
1152
|
-
.then(
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
const clonedResponse =
|
|
1157
|
-
response.clone();
|
|
1158
|
-
|
|
1159
|
-
try {
|
|
1160
|
-
// First try to parse as JSON
|
|
1161
|
-
const errorData =
|
|
1162
|
-
await clonedResponse.json();
|
|
1163
|
-
|
|
1164
|
-
if (
|
|
1165
|
-
errorData &&
|
|
1166
|
-
errorData.error
|
|
1167
|
-
) {
|
|
1168
|
-
throw new Error(
|
|
1169
|
-
errorData.error
|
|
1170
|
-
);
|
|
1171
|
-
} else if (
|
|
1172
|
-
errorData &&
|
|
1173
|
-
errorData.message
|
|
1174
|
-
) {
|
|
1175
|
-
throw new Error(
|
|
1176
|
-
errorData.message
|
|
1177
|
-
);
|
|
1178
|
-
} else if (
|
|
1179
|
-
errorData &&
|
|
1180
|
-
errorData.value
|
|
1181
|
-
) {
|
|
1182
|
-
throw new Error(
|
|
1183
|
-
errorData.value
|
|
1184
|
-
);
|
|
1185
|
-
} else {
|
|
1186
|
-
// JSON parsed but no known error fields
|
|
1187
|
-
throw new Error(
|
|
1188
|
-
`HTTP error! Status: ${response.status}`
|
|
1189
|
-
);
|
|
1190
|
-
}
|
|
1191
|
-
} catch (jsonError) {
|
|
1192
|
-
// If we can't parse as JSON, try to get text
|
|
1193
|
-
try {
|
|
1194
|
-
const textResponse =
|
|
1195
|
-
response.clone();
|
|
1196
|
-
const errorText =
|
|
1197
|
-
await textResponse.text();
|
|
1198
|
-
|
|
1199
|
-
// Check if the text looks like JSON but wasn't parsed correctly
|
|
1200
|
-
if (
|
|
1201
|
-
errorText.includes(
|
|
1202
|
-
'"error":'
|
|
1203
|
-
)
|
|
1204
|
-
) {
|
|
1205
|
-
// Try to extract the error message with regex
|
|
1206
|
-
const errorMatch =
|
|
1207
|
-
errorText.match(
|
|
1208
|
-
/"error":"([^"]+)"/i
|
|
1209
|
-
);
|
|
1210
|
-
if (
|
|
1211
|
-
errorMatch &&
|
|
1212
|
-
errorMatch[1]
|
|
1213
|
-
) {
|
|
1214
|
-
throw new Error(
|
|
1215
|
-
errorMatch[1]
|
|
1216
|
-
);
|
|
1217
|
-
}
|
|
1218
|
-
}
|
|
1219
|
-
|
|
1220
|
-
// If we got text but couldn't extract a specific error
|
|
1221
|
-
if (
|
|
1222
|
-
errorText &&
|
|
1223
|
-
errorText.length < 100
|
|
1224
|
-
) {
|
|
1225
|
-
throw new Error(
|
|
1226
|
-
errorText
|
|
1227
|
-
);
|
|
1228
|
-
}
|
|
1229
|
-
} catch (textError) {
|
|
1230
|
-
// If all else fails, fall back to status
|
|
1231
|
-
throw new Error(
|
|
1232
|
-
`HTTP error! Status: ${response.status}`
|
|
1233
|
-
);
|
|
1234
|
-
}
|
|
1235
|
-
}
|
|
1236
|
-
}
|
|
1237
|
-
return response.blob(); // Convert response to Blob
|
|
1143
|
+
.then((response) => {
|
|
1144
|
+
// fetchUtil already handles response.ok check and throws errors
|
|
1145
|
+
// Just return the blob data directly
|
|
1146
|
+
return response.data;
|
|
1238
1147
|
})
|
|
1239
1148
|
.then((blob) => {
|
|
1240
1149
|
// Validate that we have a valid blob with content
|
|
@@ -2815,20 +2724,27 @@ function GenericDetail({
|
|
|
2815
2724
|
open={modalShow}
|
|
2816
2725
|
onClose={modalClose}
|
|
2817
2726
|
closeOnDocumentClick={false}
|
|
2727
|
+
contentStyle={{ width: '80vw', maxWidth: '1200px' }}
|
|
2818
2728
|
>
|
|
2819
|
-
<
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2729
|
+
<div className="modalwrap top--modal modalWide">
|
|
2730
|
+
<div className="modal">
|
|
2731
|
+
<Form
|
|
2732
|
+
closeModal={modalClose}
|
|
2733
|
+
columnId={formId}
|
|
2734
|
+
fetchTable={handleReload}
|
|
2735
|
+
formSettings={formData}
|
|
2736
|
+
formType={formType}
|
|
2737
|
+
style={userProfile?.settings?.style || {}}
|
|
2738
|
+
updateForm={setFormData}
|
|
2739
|
+
userProfile={userProfile}
|
|
2740
|
+
paramValue={
|
|
2741
|
+
routeParams[urlParam]
|
|
2742
|
+
? routeParams[urlParam]
|
|
2743
|
+
: 0
|
|
2744
|
+
}
|
|
2745
|
+
/>
|
|
2746
|
+
</div>
|
|
2747
|
+
</div>
|
|
2832
2748
|
</Popup>
|
|
2833
2749
|
</>
|
|
2834
2750
|
);
|
|
@@ -1,8 +1,37 @@
|
|
|
1
|
-
import React from 'react';
|
|
1
|
+
import React, { useState, useCallback } from 'react';
|
|
2
|
+
import { useNavigate } from 'react-router-dom';
|
|
2
3
|
import Table from '../DataGrid';
|
|
4
|
+
import { Bell, Check, ArrowLeft } from 'akar-icons';
|
|
5
|
+
import { toast } from 'react-toastify';
|
|
6
|
+
import CustomFetch from '../Fetch';
|
|
3
7
|
import styles from './styles/NotificationList.module.scss';
|
|
4
8
|
|
|
5
9
|
const NotificationList = () => {
|
|
10
|
+
const navigate = useNavigate();
|
|
11
|
+
const [loading, setLoading] = useState(false);
|
|
12
|
+
|
|
13
|
+
const markAllAsRead = useCallback(() => {
|
|
14
|
+
setLoading(true);
|
|
15
|
+
|
|
16
|
+
CustomFetch(
|
|
17
|
+
'/ajax/user/notification/markallasread',
|
|
18
|
+
'POST',
|
|
19
|
+
{},
|
|
20
|
+
function () {
|
|
21
|
+
toast.success('All notifications marked as read');
|
|
22
|
+
setLoading(false);
|
|
23
|
+
// Refresh the table
|
|
24
|
+
window.location.reload();
|
|
25
|
+
},
|
|
26
|
+
function (error) {
|
|
27
|
+
setLoading(false);
|
|
28
|
+
toast.error(
|
|
29
|
+
String(error) || 'Failed to mark all notifications as read'
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
);
|
|
33
|
+
}, []);
|
|
34
|
+
|
|
6
35
|
const setting = {
|
|
7
36
|
ajaxSetting: {
|
|
8
37
|
url: '/ajax/user/notifications/table',
|
|
@@ -23,25 +52,49 @@ const NotificationList = () => {
|
|
|
23
52
|
{
|
|
24
53
|
id: 'data',
|
|
25
54
|
jsonData: 'label',
|
|
26
|
-
label: '
|
|
55
|
+
label: 'Message',
|
|
27
56
|
type: 'json',
|
|
57
|
+
flex: 1,
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
id: 'created_at',
|
|
61
|
+
label: 'Created At',
|
|
62
|
+
type: 'datetime',
|
|
63
|
+
minWidth: 180,
|
|
64
|
+
maxWidth: 180,
|
|
28
65
|
},
|
|
29
66
|
{
|
|
30
67
|
id: 'read_at',
|
|
31
68
|
label: 'Read At',
|
|
32
69
|
type: 'datetime',
|
|
33
|
-
minWidth:
|
|
34
|
-
maxWidth:
|
|
70
|
+
minWidth: 180,
|
|
71
|
+
maxWidth: 180,
|
|
35
72
|
},
|
|
36
73
|
],
|
|
37
74
|
settings: [],
|
|
38
75
|
};
|
|
76
|
+
|
|
39
77
|
return (
|
|
40
|
-
<div>
|
|
78
|
+
<div className={styles.notificationListContainer}>
|
|
41
79
|
<div className={styles.grid}>
|
|
42
80
|
<div className={styles.grid__row}>
|
|
43
|
-
<div
|
|
44
|
-
|
|
81
|
+
<div
|
|
82
|
+
className={`${styles.grid__full} ${styles.headerContainer}`}
|
|
83
|
+
>
|
|
84
|
+
<div className={styles.headerContent}>
|
|
85
|
+
<div className={styles.titleSection}>
|
|
86
|
+
<Bell size={24} className={styles.titleIcon} />
|
|
87
|
+
<h1>Notifications</h1>
|
|
88
|
+
</div>
|
|
89
|
+
<button
|
|
90
|
+
className={styles.markAllReadButton}
|
|
91
|
+
onClick={markAllAsRead}
|
|
92
|
+
disabled={loading}
|
|
93
|
+
>
|
|
94
|
+
<Check size={16} />
|
|
95
|
+
<span>Mark all as read</span>
|
|
96
|
+
</button>
|
|
97
|
+
</div>
|
|
45
98
|
</div>
|
|
46
99
|
</div>
|
|
47
100
|
</div>
|
|
@@ -53,10 +106,26 @@ const NotificationList = () => {
|
|
|
53
106
|
form={setting.form}
|
|
54
107
|
columns={setting.columns}
|
|
55
108
|
settings={setting.settings}
|
|
109
|
+
gridHeight={600}
|
|
56
110
|
/>
|
|
57
111
|
</div>
|
|
58
112
|
</div>
|
|
59
113
|
</div>
|
|
114
|
+
<div className={styles.grid}>
|
|
115
|
+
<div className={styles.grid__row}>
|
|
116
|
+
<div className={styles.grid__full}>
|
|
117
|
+
<div className={styles.backButtonContainer}>
|
|
118
|
+
<button
|
|
119
|
+
className={styles.backButton}
|
|
120
|
+
onClick={() => navigate('/')}
|
|
121
|
+
>
|
|
122
|
+
<ArrowLeft size={16} />
|
|
123
|
+
<span>Back to Dashboard</span>
|
|
124
|
+
</button>
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
60
129
|
</div>
|
|
61
130
|
);
|
|
62
131
|
};
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
.notificationListContainer {
|
|
2
|
+
width: 100%;
|
|
3
|
+
max-width: 1200px;
|
|
4
|
+
margin: 0 auto;
|
|
5
|
+
}
|
|
6
|
+
|
|
1
7
|
.grid {
|
|
2
8
|
width: 100%;
|
|
3
9
|
display: flex;
|
|
@@ -24,7 +30,89 @@
|
|
|
24
30
|
box-sizing: border-box;
|
|
25
31
|
overflow: visible;
|
|
26
32
|
position: relative;
|
|
27
|
-
padding:
|
|
33
|
+
padding: 16px;
|
|
28
34
|
margin: 8px 0;
|
|
29
|
-
box-shadow: 0
|
|
35
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.headerContainer {
|
|
39
|
+
padding: 16px 24px;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.headerContent {
|
|
43
|
+
display: flex;
|
|
44
|
+
align-items: center;
|
|
45
|
+
justify-content: space-between;
|
|
46
|
+
width: 100%;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.titleSection {
|
|
50
|
+
display: flex;
|
|
51
|
+
align-items: center;
|
|
52
|
+
gap: 12px;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.titleIcon {
|
|
56
|
+
color: var(--primary-color);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.titleSection h1 {
|
|
60
|
+
margin: 0;
|
|
61
|
+
font-size: 24px;
|
|
62
|
+
font-weight: 600;
|
|
63
|
+
color: var(--heading-color, #333);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.markAllReadButton {
|
|
67
|
+
display: flex;
|
|
68
|
+
align-items: center;
|
|
69
|
+
gap: 8px;
|
|
70
|
+
background-color: var(--primary-color);
|
|
71
|
+
color: white;
|
|
72
|
+
border: none;
|
|
73
|
+
border-radius: 6px;
|
|
74
|
+
padding: 10px 16px;
|
|
75
|
+
font-size: 14px;
|
|
76
|
+
font-weight: 500;
|
|
77
|
+
cursor: pointer;
|
|
78
|
+
transition: all 0.2s ease;
|
|
79
|
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.markAllReadButton:hover {
|
|
83
|
+
background-color: var(--primary-color-darker, #0056b3);
|
|
84
|
+
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.markAllReadButton:disabled {
|
|
88
|
+
opacity: 0.6;
|
|
89
|
+
cursor: not-allowed;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.backButtonContainer {
|
|
93
|
+
display: flex;
|
|
94
|
+
justify-content: center;
|
|
95
|
+
padding: 16px 0;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.backButton {
|
|
99
|
+
display: flex;
|
|
100
|
+
align-items: center;
|
|
101
|
+
gap: 8px;
|
|
102
|
+
background-color: var(--primary-color);
|
|
103
|
+
color: white;
|
|
104
|
+
border: none;
|
|
105
|
+
border-radius: 6px;
|
|
106
|
+
padding: 12px 24px;
|
|
107
|
+
font-size: 14px;
|
|
108
|
+
font-weight: 500;
|
|
109
|
+
cursor: pointer;
|
|
110
|
+
transition: all 0.2s ease;
|
|
111
|
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
112
|
+
min-width: 200px;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.backButton:hover {
|
|
116
|
+
background-color: var(--primary-color-darker, #0056b3);
|
|
117
|
+
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
|
|
30
118
|
}
|