@visns-studio/visns-components 5.10.11 → 5.11.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.
@@ -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;