@visns-studio/visns-components 5.10.11 → 5.11.2
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/README.md +85 -0
- package/package.json +7 -7
- package/src/components/cms/DataGrid.jsx +8 -8
- package/src/components/cms/DropZone.jsx +1 -1
- package/src/components/cms/Form.jsx +1 -1
- package/src/components/cms/Gallery.jsx +1 -1
- package/src/components/cms/sorting/Item.jsx +1 -1
- package/src/components/crm/Autocomplete.jsx +3 -3
- package/src/components/crm/Breadcrumb.jsx +4 -4
- package/src/components/crm/BusinessCardOcr.jsx +681 -95
- package/src/components/crm/DataGrid.jsx +34 -32
- package/src/components/crm/Field.jsx +12 -12
- package/src/components/crm/Form.jsx +2 -2
- package/src/components/crm/Navigation.jsx +11 -11
- package/src/components/crm/Notification.jsx +2 -2
- package/src/components/crm/QuickAction.jsx +3 -3
- package/src/components/crm/SectionHeader.jsx +1 -1
- package/src/components/crm/SwitchAccount.jsx +4 -4
- package/src/components/crm/auth/ClientLogin.jsx +2 -2
- package/src/components/crm/auth/ClientOTPVerify.jsx +2 -2
- package/src/components/crm/auth/Login.jsx +3 -3
- package/src/components/crm/auth/Reset.jsx +2 -2
- package/src/components/crm/auth/TwoFactorAuth.jsx +2 -2
- package/src/components/crm/columns/ColumnRenderers.jsx +320 -259
- package/src/components/crm/controls/DataGridSearch.jsx +5 -5
- package/src/components/crm/generic/AlternativeActionModal.jsx +100 -0
- package/src/components/crm/generic/ContactSelectorModal.jsx +423 -0
- package/src/components/crm/generic/DatePickerDialog.jsx +302 -0
- package/src/components/crm/generic/DateRangeSelectorModal.jsx +181 -0
- package/src/components/crm/generic/GenericClientPortal.jsx +1 -1
- package/src/components/crm/generic/GenericDashboard.jsx +4 -4
- package/src/components/crm/generic/GenericDetail.jsx +6 -6
- package/src/components/crm/generic/GenericDynamic.jsx +3 -3
- package/src/components/crm/generic/GenericEditableTable.jsx +4 -4
- package/src/components/crm/generic/GenericFormBuilder.jsx +6 -6
- package/src/components/crm/generic/GenericGrid.jsx +2888 -0
- package/src/components/crm/generic/GenericIndex.jsx +5 -1255
- package/src/components/crm/generic/GenericReport.jsx +966 -646
- package/src/components/crm/generic/GenericReportForm.jsx +194 -0
- package/src/components/crm/generic/NotificationList.jsx +1 -1
- package/src/components/crm/generic/ReasonCollectorModal.jsx +194 -0
- package/src/components/crm/generic/SortableQuoteItems.jsx +3 -3
- package/src/components/crm/generic/shared/formatters.js +231 -0
- package/src/components/crm/generic/shared/gridUtils.js +194 -0
- package/src/components/crm/generic/styles/AlternativeActionModal.css +127 -0
- package/src/components/crm/generic/styles/ContactSelectorModal.css +367 -0
- package/src/components/crm/generic/styles/DatePickerDialog.css +84 -0
- package/src/components/crm/generic/styles/DateRangeSelectorModal.css +115 -0
- package/src/components/crm/generic/styles/GenericIndex.module.scss +382 -0
- package/src/components/crm/generic/styles/GenericReport.module.scss +96 -0
- package/src/components/crm/generic/styles/ReasonCollectorModal.css +217 -0
- package/src/components/crm/modals/GalleryModal.jsx +2 -2
- package/src/components/crm/sorting/Item.jsx +3 -3
- package/src/components/crm/styles/BusinessCardOcr.module.scss +197 -4
- package/src/index.js +4 -0
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import Swal from 'sweetalert2';
|
|
3
|
+
import './styles/SweetAlert.module.css';
|
|
4
|
+
import './styles/DatePickerDialog.css';
|
|
5
|
+
import moment from 'moment';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A utility function that shows a date picker confirmation dialog using SweetAlert2
|
|
9
|
+
*
|
|
10
|
+
* @param {Object} options - Configuration options
|
|
11
|
+
* @param {string} options.title - The title of the dialog (default: 'Select Date')
|
|
12
|
+
* @param {string} options.message - The message to display in the dialog
|
|
13
|
+
* @param {string} options.confirmLabel - Label for the confirm button (default: 'Confirm')
|
|
14
|
+
* @param {string} options.cancelLabel - Label for the cancel button (default: 'Cancel')
|
|
15
|
+
* @param {Function} options.onConfirm - Function to call when confirmed with selected date
|
|
16
|
+
* @param {Function} options.onCancel - Function to call when cancelled
|
|
17
|
+
* @param {string} options.defaultDate - Default date value (ISO string or 'now')
|
|
18
|
+
* @param {string} options.dateFormat - Date format for display (default: 'YYYY-MM-DD')
|
|
19
|
+
* @param {Object} options.data - Optional data object to pass context
|
|
20
|
+
* @param {string} options.timezone - Timezone to use ('auto', 'Australia/Perth', or specific timezone)
|
|
21
|
+
* @param {boolean} options.forcePerth - Force Australia/Perth timezone (default: false)
|
|
22
|
+
* @param {boolean} options.dateOnly - Return date string only for DATE fields (default: auto-detect)
|
|
23
|
+
* @returns {Promise} - A promise that resolves when the dialog is closed
|
|
24
|
+
*/
|
|
25
|
+
export const showDatePickerDialog = ({
|
|
26
|
+
title = 'Select Date',
|
|
27
|
+
message = 'Please select a date:',
|
|
28
|
+
confirmLabel = 'Confirm',
|
|
29
|
+
cancelLabel = 'Cancel',
|
|
30
|
+
onConfirm = () => {},
|
|
31
|
+
onCancel = () => {},
|
|
32
|
+
defaultDate = null,
|
|
33
|
+
dateFormat = 'YYYY-MM-DD',
|
|
34
|
+
data = null,
|
|
35
|
+
timezone = 'auto',
|
|
36
|
+
forcePerth = false,
|
|
37
|
+
dateOnly = 'auto',
|
|
38
|
+
}) => {
|
|
39
|
+
// Timezone detection and handling
|
|
40
|
+
const getTargetTimezone = () => {
|
|
41
|
+
if (forcePerth) {
|
|
42
|
+
return 'Australia/Perth';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (timezone === 'auto') {
|
|
46
|
+
// Try to detect browser timezone
|
|
47
|
+
try {
|
|
48
|
+
return Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
49
|
+
} catch (e) {
|
|
50
|
+
console.warn('Could not detect timezone, falling back to Australia/Perth');
|
|
51
|
+
return 'Australia/Perth';
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return timezone;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const targetTimezone = getTargetTimezone();
|
|
59
|
+
|
|
60
|
+
// Auto-detect if this should be a date-only field based on field name or explicit config
|
|
61
|
+
const shouldUseDateOnly = () => {
|
|
62
|
+
if (dateOnly === true || dateOnly === false) {
|
|
63
|
+
return dateOnly; // Explicit configuration
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Auto-detection based on field name patterns
|
|
67
|
+
const fieldName = data?.name || '';
|
|
68
|
+
const dateOnlyPatterns = [
|
|
69
|
+
/^date_/i, // date_sent, date_delivered, etc.
|
|
70
|
+
/^quarter_/i, // quarter_1, quarter_2, etc.
|
|
71
|
+
/_date$/i, // send_date, delivery_date, etc.
|
|
72
|
+
/^(start|end)_/i, // start_date, end_date, etc.
|
|
73
|
+
/^(birth|death)_/i, // birth_date, death_date, etc.
|
|
74
|
+
/^due_/i, // due_date, etc.
|
|
75
|
+
/^expiry/i, // expiry_date, etc.
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
const isDateField = dateOnlyPatterns.some(pattern => pattern.test(fieldName));
|
|
79
|
+
|
|
80
|
+
console.log(`Auto-detecting field type for "${fieldName}": ${isDateField ? 'DATE' : 'DATETIME'} field`);
|
|
81
|
+
|
|
82
|
+
return isDateField;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const useDateOnly = shouldUseDateOnly();
|
|
86
|
+
|
|
87
|
+
// Get timezone offset in hours for a given timezone
|
|
88
|
+
const getTimezoneOffsetHours = (tz) => {
|
|
89
|
+
try {
|
|
90
|
+
// For Australia/Perth, it's UTC+8
|
|
91
|
+
if (tz === 'Australia/Perth') {
|
|
92
|
+
return 8; // +8 hours
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// For other timezones, calculate the offset
|
|
96
|
+
const now = new Date();
|
|
97
|
+
const utc = new Date(now.getTime() + (now.getTimezoneOffset() * 60000));
|
|
98
|
+
const targetTime = new Date(utc.toLocaleString("en-US", {timeZone: tz}));
|
|
99
|
+
return (targetTime.getTime() - utc.getTime()) / (1000 * 60 * 60); // Convert to hours
|
|
100
|
+
} catch (e) {
|
|
101
|
+
console.warn('Could not calculate timezone offset, using +8 for Perth:', e);
|
|
102
|
+
return 8; // Default to Perth time
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
// Convert UTC date to target timezone for display
|
|
107
|
+
const convertToDisplayDate = (utcDate) => {
|
|
108
|
+
if (!utcDate) return moment().format(dateFormat);
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
// If it's a UTC ISO string, convert to target timezone
|
|
112
|
+
if (typeof utcDate === 'string' && (utcDate.includes('T') || utcDate.includes('Z'))) {
|
|
113
|
+
const offsetHours = getTimezoneOffsetHours(targetTimezone);
|
|
114
|
+
// Add the offset to convert FROM UTC TO local timezone
|
|
115
|
+
return moment.utc(utcDate).add(offsetHours, 'hours').format(dateFormat);
|
|
116
|
+
}
|
|
117
|
+
// If it's already a date string (YYYY-MM-DD), treat it as local date
|
|
118
|
+
return moment(utcDate).format(dateFormat);
|
|
119
|
+
} catch (e) {
|
|
120
|
+
console.warn('Error converting date, using current date:', e);
|
|
121
|
+
return moment().format(dateFormat);
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
// Convert selected date for API calls
|
|
126
|
+
const convertForAPI = (localDateString) => {
|
|
127
|
+
try {
|
|
128
|
+
// If useDateOnly is true, just return the date string as-is for DATE fields
|
|
129
|
+
if (useDateOnly) {
|
|
130
|
+
console.log(`Date-only field: returning ${localDateString} as-is for DATE field`);
|
|
131
|
+
return localDateString;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// For DATETIME fields, convert to UTC
|
|
135
|
+
const offsetHours = getTimezoneOffsetHours(targetTimezone);
|
|
136
|
+
|
|
137
|
+
// Create a UTC moment object from the date string, then treat it as if it were in the target timezone
|
|
138
|
+
// This means: "2025-06-18" becomes "2025-06-18T00:00:00 in target timezone"
|
|
139
|
+
const naiveMoment = moment.utc(localDateString, dateFormat);
|
|
140
|
+
|
|
141
|
+
// Now subtract the offset to get the actual UTC time
|
|
142
|
+
// If it's Perth (+8), we subtract 8 hours to get UTC
|
|
143
|
+
const utcMoment = naiveMoment.clone().subtract(offsetHours, 'hours');
|
|
144
|
+
|
|
145
|
+
console.log(`Converting: ${localDateString} midnight in ${targetTimezone}`);
|
|
146
|
+
console.log(`Naive UTC: ${naiveMoment.toISOString()}`);
|
|
147
|
+
console.log(`Offset: -${offsetHours} hours`);
|
|
148
|
+
console.log(`Final UTC: ${utcMoment.toISOString()}`);
|
|
149
|
+
|
|
150
|
+
return utcMoment.toISOString();
|
|
151
|
+
} catch (e) {
|
|
152
|
+
console.warn('Error converting date, using as-is:', e);
|
|
153
|
+
// Fallback: return the date string as-is
|
|
154
|
+
return localDateString;
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
// Get default date value with timezone handling
|
|
159
|
+
let initialDate = '';
|
|
160
|
+
if (defaultDate === 'now') {
|
|
161
|
+
initialDate = convertToDisplayDate(moment().toISOString());
|
|
162
|
+
} else if (defaultDate) {
|
|
163
|
+
initialDate = convertToDisplayDate(defaultDate);
|
|
164
|
+
} else {
|
|
165
|
+
initialDate = convertToDisplayDate(moment().toISOString());
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Enhanced message with item details if available
|
|
169
|
+
let enhancedMessage = message;
|
|
170
|
+
if (data) {
|
|
171
|
+
const identifiers = ['name', 'label', 'title', 'id'];
|
|
172
|
+
for (const id of identifiers) {
|
|
173
|
+
if (data[id] && typeof data[id] === 'string' && data[id].trim() !== '') {
|
|
174
|
+
if (!message.includes(data[id])) {
|
|
175
|
+
enhancedMessage = `${message} (${data[id]})`;
|
|
176
|
+
}
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Create HTML content with compact date input
|
|
183
|
+
const htmlContent = `
|
|
184
|
+
<div style="text-align: left; margin: 8px 0; max-width: 100%; overflow: hidden;">
|
|
185
|
+
<p style="margin-bottom: 8px; color: #374151; word-wrap: break-word; font-size: 14px;">${enhancedMessage}</p>
|
|
186
|
+
<div style="margin: 8px 0; width: 100%;">
|
|
187
|
+
<label for="swal-date-input" style="display: block; margin-bottom: 4px; font-weight: 500; color: #374151; font-size: 13px;">
|
|
188
|
+
Date <span style="font-size: 11px; color: #6b7280; font-weight: normal;">(${targetTimezone})</span>:
|
|
189
|
+
</label>
|
|
190
|
+
<input
|
|
191
|
+
id="swal-date-input"
|
|
192
|
+
type="date"
|
|
193
|
+
value="${initialDate}"
|
|
194
|
+
class="swal2-input"
|
|
195
|
+
style="
|
|
196
|
+
width: 100% !important;
|
|
197
|
+
max-width: 100% !important;
|
|
198
|
+
box-sizing: border-box !important;
|
|
199
|
+
padding: 8px 10px !important;
|
|
200
|
+
border: 1px solid #d1d5db !important;
|
|
201
|
+
border-radius: 4px !important;
|
|
202
|
+
font-size: 13px !important;
|
|
203
|
+
font-family: inherit !important;
|
|
204
|
+
background-color: #ffffff !important;
|
|
205
|
+
outline: none !important;
|
|
206
|
+
transition: border-color 0.2s ease !important;
|
|
207
|
+
margin: 0 !important;
|
|
208
|
+
display: block !important;
|
|
209
|
+
height: 34px !important;
|
|
210
|
+
"
|
|
211
|
+
onfocus="this.style.borderColor='#4f46e5'"
|
|
212
|
+
onblur="this.style.borderColor='#d1d5db'">
|
|
213
|
+
</div>
|
|
214
|
+
</div>
|
|
215
|
+
`;
|
|
216
|
+
|
|
217
|
+
// Configure SweetAlert2 options
|
|
218
|
+
const swalOptions = {
|
|
219
|
+
title: title,
|
|
220
|
+
html: htmlContent,
|
|
221
|
+
icon: 'question',
|
|
222
|
+
showCancelButton: true,
|
|
223
|
+
confirmButtonText: confirmLabel,
|
|
224
|
+
cancelButtonText: cancelLabel,
|
|
225
|
+
confirmButtonColor: 'var(--primary-color, #4f46e5)',
|
|
226
|
+
cancelButtonColor: '#6b7280',
|
|
227
|
+
allowOutsideClick: true,
|
|
228
|
+
allowEscapeKey: true,
|
|
229
|
+
reverseButtons: true,
|
|
230
|
+
focusCancel: false,
|
|
231
|
+
buttonsStyling: true,
|
|
232
|
+
width: 'auto',
|
|
233
|
+
padding: '0.75rem',
|
|
234
|
+
customClass: {
|
|
235
|
+
popup: 'date-picker-dialog',
|
|
236
|
+
htmlContainer: 'date-picker-content'
|
|
237
|
+
},
|
|
238
|
+
preConfirm: () => {
|
|
239
|
+
const dateInput = document.getElementById('swal-date-input');
|
|
240
|
+
const selectedDate = dateInput.value;
|
|
241
|
+
|
|
242
|
+
if (!selectedDate) {
|
|
243
|
+
Swal.showValidationMessage('Please select a date');
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Validate the date
|
|
248
|
+
if (!moment(selectedDate).isValid()) {
|
|
249
|
+
Swal.showValidationMessage('Please enter a valid date');
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return selectedDate;
|
|
254
|
+
},
|
|
255
|
+
didOpen: () => {
|
|
256
|
+
// Focus on the date input when dialog opens
|
|
257
|
+
const dateInput = document.getElementById('swal-date-input');
|
|
258
|
+
if (dateInput) {
|
|
259
|
+
// Additional CSS to ensure the input stays within bounds
|
|
260
|
+
dateInput.style.setProperty('width', '100%', 'important');
|
|
261
|
+
dateInput.style.setProperty('max-width', '100%', 'important');
|
|
262
|
+
dateInput.style.setProperty('box-sizing', 'border-box', 'important');
|
|
263
|
+
dateInput.style.setProperty('overflow', 'hidden', 'important');
|
|
264
|
+
|
|
265
|
+
// Focus the input
|
|
266
|
+
dateInput.focus();
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Apply additional container styling
|
|
270
|
+
const popup = document.querySelector('.date-picker-dialog');
|
|
271
|
+
if (popup) {
|
|
272
|
+
popup.style.setProperty('max-width', '90vw', 'important');
|
|
273
|
+
popup.style.setProperty('width', 'auto', 'important');
|
|
274
|
+
popup.style.setProperty('min-width', '320px', 'important');
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const htmlContainer = document.querySelector('.date-picker-content');
|
|
278
|
+
if (htmlContainer) {
|
|
279
|
+
htmlContainer.style.setProperty('overflow', 'hidden', 'important');
|
|
280
|
+
htmlContainer.style.setProperty('word-wrap', 'break-word', 'important');
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
// Show SweetAlert2 dialog
|
|
286
|
+
return Swal.fire(swalOptions).then((result) => {
|
|
287
|
+
if (result.isConfirmed && result.value) {
|
|
288
|
+
// Convert the selected date for API calls (either date string or UTC datetime)
|
|
289
|
+
const selectedDateForAPI = convertForAPI(result.value);
|
|
290
|
+
const selectedDateLocal = result.value;
|
|
291
|
+
|
|
292
|
+
// Log for debugging
|
|
293
|
+
console.log(`Date selected: ${selectedDateLocal} (${targetTimezone}) -> ${selectedDateForAPI} (${useDateOnly ? 'DATE' : 'UTC'})`);
|
|
294
|
+
|
|
295
|
+
onConfirm(selectedDateForAPI, selectedDateLocal, targetTimezone);
|
|
296
|
+
} else if (result.dismiss === Swal.DismissReason.cancel) {
|
|
297
|
+
onCancel();
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
export default showDatePickerDialog;
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import Swal from 'sweetalert2';
|
|
2
|
+
import moment from 'moment';
|
|
3
|
+
import './styles/SweetAlert.module.css';
|
|
4
|
+
import './styles/DateRangeSelectorModal.css';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Show a date range selector modal for selecting start and end dates
|
|
8
|
+
* @param {Object} options - Configuration options
|
|
9
|
+
* @param {string} options.title - Modal title
|
|
10
|
+
* @param {string} options.message - Modal message
|
|
11
|
+
* @param {string} options.confirmLabel - Confirm button label
|
|
12
|
+
* @param {string} options.cancelLabel - Cancel button label
|
|
13
|
+
* @param {string|null} options.defaultStartDate - Default start date value
|
|
14
|
+
* @param {string|null} options.defaultEndDate - Default end date value
|
|
15
|
+
* @param {Object} options.data - Additional data to pass to callbacks
|
|
16
|
+
* @param {string} options.timezone - Timezone for date handling
|
|
17
|
+
* @param {boolean} options.forcePerth - Force Australia/Perth timezone
|
|
18
|
+
* @param {boolean} options.dateOnly - Return date only (no time conversion)
|
|
19
|
+
* @param {Function} options.onConfirm - Callback when confirmed
|
|
20
|
+
* @param {Function} options.onCancel - Callback when cancelled
|
|
21
|
+
*/
|
|
22
|
+
export const showDateRangeSelectorModal = async ({
|
|
23
|
+
title = 'Select Date Range',
|
|
24
|
+
message = 'Please select start and end dates:',
|
|
25
|
+
confirmLabel = 'Next: Select Contacts',
|
|
26
|
+
cancelLabel = 'Cancel',
|
|
27
|
+
defaultStartDate = null,
|
|
28
|
+
defaultEndDate = null,
|
|
29
|
+
data = {},
|
|
30
|
+
timezone = 'auto',
|
|
31
|
+
forcePerth = false,
|
|
32
|
+
dateOnly = false,
|
|
33
|
+
onConfirm,
|
|
34
|
+
onCancel
|
|
35
|
+
}) => {
|
|
36
|
+
// Determine timezone
|
|
37
|
+
let userTimezone = 'UTC';
|
|
38
|
+
if (forcePerth) {
|
|
39
|
+
userTimezone = 'Australia/Perth';
|
|
40
|
+
} else if (timezone === 'auto') {
|
|
41
|
+
userTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
42
|
+
} else if (timezone && timezone !== 'auto') {
|
|
43
|
+
userTimezone = timezone;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
console.log('Date Range Selector - Timezone Configuration:', {
|
|
47
|
+
inputTimezone: timezone,
|
|
48
|
+
forcePerth,
|
|
49
|
+
resolvedTimezone: userTimezone,
|
|
50
|
+
dateOnly
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Convert UTC dates to local timezone for display
|
|
54
|
+
const convertUTCToLocal = (utcDateString) => {
|
|
55
|
+
if (!utcDateString || utcDateString === 'now') return '';
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
const utcMoment = moment.utc(utcDateString);
|
|
59
|
+
if (!utcMoment.isValid()) return '';
|
|
60
|
+
|
|
61
|
+
if (dateOnly) {
|
|
62
|
+
// For DATE fields, just return the date part
|
|
63
|
+
return utcMoment.format('YYYY-MM-DD');
|
|
64
|
+
} else {
|
|
65
|
+
// For DATETIME fields, convert to local timezone
|
|
66
|
+
const localMoment = utcMoment.clone().tz(userTimezone);
|
|
67
|
+
return localMoment.format('YYYY-MM-DD');
|
|
68
|
+
}
|
|
69
|
+
} catch (error) {
|
|
70
|
+
console.error('Error converting UTC to local:', error);
|
|
71
|
+
return '';
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
// Convert local date to UTC for API
|
|
76
|
+
const convertLocalToUTC = (localDateString) => {
|
|
77
|
+
if (!localDateString) return null;
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
if (dateOnly) {
|
|
81
|
+
// For DATE fields, return just the date string
|
|
82
|
+
return localDateString;
|
|
83
|
+
} else {
|
|
84
|
+
// For DATETIME fields, convert to UTC
|
|
85
|
+
const localMoment = moment.tz(localDateString, userTimezone);
|
|
86
|
+
return localMoment.utc().toISOString();
|
|
87
|
+
}
|
|
88
|
+
} catch (error) {
|
|
89
|
+
console.error('Error converting local to UTC:', error);
|
|
90
|
+
return localDateString;
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
// Prepare default values
|
|
95
|
+
const displayStartDate = convertUTCToLocal(defaultStartDate);
|
|
96
|
+
const displayEndDate = convertUTCToLocal(defaultEndDate);
|
|
97
|
+
|
|
98
|
+
const timezoneLabel = userTimezone === 'Australia/Perth' ? ' (Perth Time)' : userTimezone !== 'UTC' ? ` (${userTimezone})` : '';
|
|
99
|
+
|
|
100
|
+
const result = await Swal.fire({
|
|
101
|
+
title: title,
|
|
102
|
+
html: `
|
|
103
|
+
<div class="date-range-selector-modal">
|
|
104
|
+
<p class="date-range-message">${message}</p>
|
|
105
|
+
<div class="date-range-inputs">
|
|
106
|
+
<div class="date-input-group">
|
|
107
|
+
<label for="start-date-input" class="date-input-label">
|
|
108
|
+
Start Date${timezoneLabel}:
|
|
109
|
+
</label>
|
|
110
|
+
<input
|
|
111
|
+
type="date"
|
|
112
|
+
id="start-date-input"
|
|
113
|
+
class="date-range-input"
|
|
114
|
+
value="${displayStartDate}"
|
|
115
|
+
/>
|
|
116
|
+
</div>
|
|
117
|
+
<div class="date-input-group">
|
|
118
|
+
<label for="end-date-input" class="date-input-label">
|
|
119
|
+
End Date${timezoneLabel}:
|
|
120
|
+
</label>
|
|
121
|
+
<input
|
|
122
|
+
type="date"
|
|
123
|
+
id="end-date-input"
|
|
124
|
+
class="date-range-input"
|
|
125
|
+
value="${displayEndDate}"
|
|
126
|
+
/>
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
</div>
|
|
130
|
+
`,
|
|
131
|
+
showCancelButton: true,
|
|
132
|
+
confirmButtonText: confirmLabel,
|
|
133
|
+
cancelButtonText: cancelLabel,
|
|
134
|
+
customClass: {
|
|
135
|
+
container: 'date-range-swal-container',
|
|
136
|
+
popup: 'date-range-swal-popup',
|
|
137
|
+
content: 'date-range-swal-content',
|
|
138
|
+
confirmButton: 'date-range-swal-confirm',
|
|
139
|
+
cancelButton: 'date-range-swal-cancel'
|
|
140
|
+
},
|
|
141
|
+
preConfirm: () => {
|
|
142
|
+
const startDateInput = document.getElementById('start-date-input');
|
|
143
|
+
const endDateInput = document.getElementById('end-date-input');
|
|
144
|
+
const startDate = startDateInput.value;
|
|
145
|
+
const endDate = endDateInput.value;
|
|
146
|
+
|
|
147
|
+
if (!startDate) {
|
|
148
|
+
Swal.showValidationMessage('Please select a start date');
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (!endDate) {
|
|
153
|
+
Swal.showValidationMessage('Please select an end date');
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (moment(startDate).isAfter(moment(endDate))) {
|
|
158
|
+
Swal.showValidationMessage('Start date must be before end date');
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
console.log('Date Range Selected:', {
|
|
163
|
+
startDate: startDate,
|
|
164
|
+
endDate: endDate,
|
|
165
|
+
timezone: userTimezone,
|
|
166
|
+
dateOnly: dateOnly
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
startDate: convertLocalToUTC(startDate),
|
|
171
|
+
endDate: convertLocalToUTC(endDate)
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
if (result.isConfirmed && onConfirm) {
|
|
177
|
+
await onConfirm(result.value.startDate, result.value.endDate);
|
|
178
|
+
} else if (result.isDismissed && onCancel) {
|
|
179
|
+
onCancel();
|
|
180
|
+
}
|
|
181
|
+
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { Routes, Route, useNavigate } from 'react-router-dom';
|
|
3
3
|
import { toast } from 'react-toastify';
|
|
4
|
-
import { SignOut } from '
|
|
4
|
+
import { LogOut as SignOut } from 'lucide-react';
|
|
5
5
|
|
|
6
6
|
import CustomFetch from '../Fetch';
|
|
7
7
|
|
|
@@ -8,7 +8,7 @@ import Popup from 'reactjs-popup';
|
|
|
8
8
|
import dayjs from 'dayjs';
|
|
9
9
|
import parse from 'html-react-parser';
|
|
10
10
|
import { createSwapy } from 'swapy';
|
|
11
|
-
import {
|
|
11
|
+
import { X, Minus, Plus, Star, Trash2 } from 'lucide-react';
|
|
12
12
|
import { confirmDialog } from '../../utils/ConfirmDialog';
|
|
13
13
|
|
|
14
14
|
import CustomFetch from '../../crm/Fetch';
|
|
@@ -1320,7 +1320,7 @@ function GenericDashboard({ setting, userProfile, dynamicDashboard = false }) {
|
|
|
1320
1320
|
<button
|
|
1321
1321
|
onClick={() => confirmDelete(widget.id)}
|
|
1322
1322
|
>
|
|
1323
|
-
<
|
|
1323
|
+
<Trash2 strokeWidth={2} size={12} />
|
|
1324
1324
|
</button>
|
|
1325
1325
|
</div>
|
|
1326
1326
|
</div>
|
|
@@ -1370,7 +1370,7 @@ function GenericDashboard({ setting, userProfile, dynamicDashboard = false }) {
|
|
|
1370
1370
|
className="modal__close"
|
|
1371
1371
|
onClick={closeModal}
|
|
1372
1372
|
>
|
|
1373
|
-
<
|
|
1373
|
+
<X strokeWidth={1} size={24} />
|
|
1374
1374
|
</button>
|
|
1375
1375
|
</div>
|
|
1376
1376
|
<div className="modal__content">
|
|
@@ -1397,7 +1397,7 @@ function GenericDashboard({ setting, userProfile, dynamicDashboard = false }) {
|
|
|
1397
1397
|
className="modal__close"
|
|
1398
1398
|
onClick={() => setShowAddWidgetModal(false)}
|
|
1399
1399
|
>
|
|
1400
|
-
<
|
|
1400
|
+
<X strokeWidth={1} size={24} />
|
|
1401
1401
|
</button>
|
|
1402
1402
|
</div>
|
|
1403
1403
|
<div className="modal__content">
|
|
@@ -49,14 +49,14 @@ import { toast } from 'react-toastify';
|
|
|
49
49
|
import imageCompression from 'browser-image-compression';
|
|
50
50
|
import fetchUtil from '../../../utils/fetchUtil';
|
|
51
51
|
import {
|
|
52
|
-
CircleCheck,
|
|
52
|
+
CheckCircle as CircleCheck,
|
|
53
53
|
Copy,
|
|
54
|
-
EyeOpen,
|
|
55
|
-
EyeSlashed,
|
|
54
|
+
Eye as EyeOpen,
|
|
55
|
+
EyeOff as EyeSlashed,
|
|
56
56
|
File,
|
|
57
|
-
Pencil,
|
|
58
|
-
TrashCan,
|
|
59
|
-
} from '
|
|
57
|
+
Edit as Pencil,
|
|
58
|
+
Trash2 as TrashCan,
|
|
59
|
+
} from 'lucide-react';
|
|
60
60
|
import { confirmDialog } from '../../utils/ConfirmDialog';
|
|
61
61
|
|
|
62
62
|
import 'react-big-calendar/lib/css/react-big-calendar.css';
|
|
@@ -3,7 +3,7 @@ import moment from 'moment';
|
|
|
3
3
|
import { toast } from 'react-toastify';
|
|
4
4
|
import { saveAs } from 'file-saver';
|
|
5
5
|
import parse from 'html-react-parser';
|
|
6
|
-
import {
|
|
6
|
+
import { Delete, Download as DownloadIcon } from 'lucide-react';
|
|
7
7
|
import { confirmDialog } from '../../utils/ConfirmDialog';
|
|
8
8
|
|
|
9
9
|
import CustomFetch from '../Fetch';
|
|
@@ -1056,7 +1056,7 @@ const GenericDynamic = ({
|
|
|
1056
1056
|
)
|
|
1057
1057
|
}
|
|
1058
1058
|
>
|
|
1059
|
-
<
|
|
1059
|
+
<DownloadIcon
|
|
1060
1060
|
strokeWidth={
|
|
1061
1061
|
2
|
|
1062
1062
|
}
|
|
@@ -1077,7 +1077,7 @@ const GenericDynamic = ({
|
|
|
1077
1077
|
)
|
|
1078
1078
|
}
|
|
1079
1079
|
>
|
|
1080
|
-
<
|
|
1080
|
+
<Delete
|
|
1081
1081
|
strokeWidth={
|
|
1082
1082
|
2
|
|
1083
1083
|
}
|
|
@@ -2,7 +2,7 @@ import React, { useEffect, useMemo, useRef, useState } from 'react';
|
|
|
2
2
|
import ReactDOM from 'react-dom';
|
|
3
3
|
import debounce from 'lodash.debounce';
|
|
4
4
|
import { toast } from 'react-toastify';
|
|
5
|
-
import {
|
|
5
|
+
import { Trash2, ArrowUp, ArrowDown, Copy, Droplets } from 'lucide-react';
|
|
6
6
|
import { CompactPicker } from 'react-color';
|
|
7
7
|
import { confirmDialog } from '../../utils/ConfirmDialog';
|
|
8
8
|
|
|
@@ -65,7 +65,7 @@ const ColourPicker = ({ value, columnId, keyCounter, onChange }) => {
|
|
|
65
65
|
setColorPickerShow(!colorPickerShow);
|
|
66
66
|
}}
|
|
67
67
|
>
|
|
68
|
-
<
|
|
68
|
+
<Droplets size={20} style={{ color: 'black' }} />
|
|
69
69
|
</button>
|
|
70
70
|
|
|
71
71
|
{colorPickerShow &&
|
|
@@ -1059,7 +1059,7 @@ const GenericEditableTable = ({
|
|
|
1059
1059
|
}}
|
|
1060
1060
|
/>
|
|
1061
1061
|
)}
|
|
1062
|
-
<
|
|
1062
|
+
<Trash2
|
|
1063
1063
|
size={16}
|
|
1064
1064
|
onClick={() =>
|
|
1065
1065
|
handleDeleteRow(
|
|
@@ -1199,7 +1199,7 @@ const GenericEditableTable = ({
|
|
|
1199
1199
|
style={{ cursor: 'pointer' }}
|
|
1200
1200
|
/>
|
|
1201
1201
|
)}
|
|
1202
|
-
<
|
|
1202
|
+
<Trash2
|
|
1203
1203
|
size={16}
|
|
1204
1204
|
onClick={() =>
|
|
1205
1205
|
handleDeleteRow(entry)
|
|
@@ -28,12 +28,12 @@ import { v4 as uuidv4 } from 'uuid';
|
|
|
28
28
|
import { Editor } from '@tinymce/tinymce-react';
|
|
29
29
|
import { Tooltip } from 'react-tooltip';
|
|
30
30
|
import {
|
|
31
|
-
CirclePlus,
|
|
32
|
-
CircleX,
|
|
33
|
-
TrashCan,
|
|
34
|
-
Pencil,
|
|
35
|
-
ChevronVertical,
|
|
36
|
-
} from '
|
|
31
|
+
PlusCircle as CirclePlus,
|
|
32
|
+
X as CircleX,
|
|
33
|
+
Trash2 as TrashCan,
|
|
34
|
+
Edit as Pencil,
|
|
35
|
+
GripVertical as ChevronVertical,
|
|
36
|
+
} from 'lucide-react';
|
|
37
37
|
import { confirmDialog } from '../../utils/ConfirmDialog';
|
|
38
38
|
|
|
39
39
|
import 'react-toggle/style.css';
|