@visns-studio/visns-components 5.15.10 → 5.15.11

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,372 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import {
3
+ X,
4
+ Download,
5
+ RefreshCw,
6
+ Plus,
7
+ Edit,
8
+ AlertTriangle,
9
+ CheckCircle,
10
+ Search,
11
+ Filter,
12
+ Eye,
13
+ EyeOff
14
+ } from 'lucide-react';
15
+ import styles from '../styles/DataPreviewModal.module.scss';
16
+
17
+ const DataPreviewModal = ({
18
+ isOpen,
19
+ onClose,
20
+ provider,
21
+ dataType,
22
+ onConfirmSync
23
+ }) => {
24
+ const [previewData, setPreviewData] = useState(null);
25
+ const [loading, setLoading] = useState(false);
26
+ const [currentPage, setCurrentPage] = useState(1);
27
+ const [searchTerm, setSearchTerm] = useState('');
28
+ const [statusFilter, setStatusFilter] = useState('all');
29
+ const [showDetails, setShowDetails] = useState({});
30
+ const recordsPerPage = 20;
31
+
32
+ useEffect(() => {
33
+ if (isOpen && provider && dataType) {
34
+ loadPreviewData();
35
+ }
36
+ }, [isOpen, provider, dataType]);
37
+
38
+ const loadPreviewData = async () => {
39
+ setLoading(true);
40
+ try {
41
+ const response = await fetch(`/integrations/oauth/${provider.name}/preview-comprehensive`, {
42
+ method: 'POST',
43
+ headers: {
44
+ 'Content-Type': 'application/json',
45
+ 'X-Requested-With': 'XMLHttpRequest',
46
+ 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content'),
47
+ },
48
+ credentials: 'same-origin',
49
+ body: JSON.stringify({
50
+ data_type: dataType,
51
+ include_all: true,
52
+ include_status: true
53
+ }),
54
+ });
55
+
56
+ if (!response.ok) {
57
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
58
+ }
59
+
60
+ const data = await response.json();
61
+ setPreviewData(data);
62
+ } catch (error) {
63
+ console.error('Preview failed:', error);
64
+ setPreviewData({
65
+ success: false,
66
+ error: error.message
67
+ });
68
+ } finally {
69
+ setLoading(false);
70
+ }
71
+ };
72
+
73
+ const toggleDetails = (recordId) => {
74
+ setShowDetails(prev => ({
75
+ ...prev,
76
+ [recordId]: !prev[recordId]
77
+ }));
78
+ };
79
+
80
+ const getStatusIcon = (status) => {
81
+ switch (status) {
82
+ case 'new':
83
+ return <Plus size={16} className={styles.statusIconNew} />;
84
+ case 'update':
85
+ return <Edit size={16} className={styles.statusIconUpdate} />;
86
+ case 'skip':
87
+ return <AlertTriangle size={16} className={styles.statusIconSkip} />;
88
+ default:
89
+ return <CheckCircle size={16} className={styles.statusIconDefault} />;
90
+ }
91
+ };
92
+
93
+ const getStatusBadge = (status) => {
94
+ const baseClass = styles.statusBadge;
95
+ switch (status) {
96
+ case 'new':
97
+ return `${baseClass} ${styles.statusNew}`;
98
+ case 'update':
99
+ return `${baseClass} ${styles.statusUpdate}`;
100
+ case 'skip':
101
+ return `${baseClass} ${styles.statusSkip}`;
102
+ default:
103
+ return `${baseClass} ${styles.statusDefault}`;
104
+ }
105
+ };
106
+
107
+ const filterRecords = (records) => {
108
+ if (!records) return [];
109
+
110
+ let filtered = records;
111
+
112
+ // Filter by search term
113
+ if (searchTerm) {
114
+ filtered = filtered.filter(record =>
115
+ JSON.stringify(record.data).toLowerCase().includes(searchTerm.toLowerCase()) ||
116
+ (record.existing_data && JSON.stringify(record.existing_data).toLowerCase().includes(searchTerm.toLowerCase()))
117
+ );
118
+ }
119
+
120
+ // Filter by status
121
+ if (statusFilter !== 'all') {
122
+ filtered = filtered.filter(record => record.sync_status === statusFilter);
123
+ }
124
+
125
+ return filtered;
126
+ };
127
+
128
+ const paginateRecords = (records) => {
129
+ const startIndex = (currentPage - 1) * recordsPerPage;
130
+ return records.slice(startIndex, startIndex + recordsPerPage);
131
+ };
132
+
133
+ const handleConfirmSync = () => {
134
+ if (previewData?.summary?.total_changes > 0) {
135
+ onConfirmSync(dataType, {
136
+ confirmed: true,
137
+ preview_id: previewData.preview_id
138
+ });
139
+ }
140
+ onClose();
141
+ };
142
+
143
+ if (!isOpen) return null;
144
+
145
+ const filteredRecords = filterRecords(previewData?.records || []);
146
+ const paginatedRecords = paginateRecords(filteredRecords);
147
+ const totalPages = Math.ceil(filteredRecords.length / recordsPerPage);
148
+
149
+ return (
150
+ <div className={styles.modalOverlay} onClick={onClose}>
151
+ <div className={styles.modalContainer} onClick={(e) => e.stopPropagation()}>
152
+ {/* Header */}
153
+ <div className={styles.modalHeader}>
154
+ <div className={styles.headerContent}>
155
+ <h2>
156
+ <Download size={20} />
157
+ Data Preview: {dataType}
158
+ </h2>
159
+ <p className={styles.subtitle}>
160
+ Review all records before importing from {provider.display_name}
161
+ </p>
162
+ </div>
163
+ <button onClick={onClose} className={styles.closeButton}>
164
+ <X size={20} />
165
+ </button>
166
+ </div>
167
+
168
+ {/* Content */}
169
+ <div className={styles.modalContent}>
170
+ {loading ? (
171
+ <div className={styles.loadingState}>
172
+ <RefreshCw size={32} className={styles.spinner} />
173
+ <p>Analyzing all records...</p>
174
+ </div>
175
+ ) : previewData?.success === false ? (
176
+ <div className={styles.errorState}>
177
+ <AlertTriangle size={32} />
178
+ <h3>Preview Failed</h3>
179
+ <p>{previewData.error}</p>
180
+ <button onClick={loadPreviewData} className={styles.retryButton}>
181
+ Try Again
182
+ </button>
183
+ </div>
184
+ ) : previewData ? (
185
+ <>
186
+ {/* Summary Stats */}
187
+ <div className={styles.summarySection}>
188
+ <div className={styles.summaryGrid}>
189
+ <div className={styles.summaryCard}>
190
+ <div className={styles.summaryIcon}>
191
+ <Plus size={20} />
192
+ </div>
193
+ <div className={styles.summaryContent}>
194
+ <span className={styles.summaryNumber}>
195
+ {previewData.summary?.new_records || 0}
196
+ </span>
197
+ <span className={styles.summaryLabel}>New Records</span>
198
+ </div>
199
+ </div>
200
+
201
+ <div className={styles.summaryCard}>
202
+ <div className={styles.summaryIcon}>
203
+ <Edit size={20} />
204
+ </div>
205
+ <div className={styles.summaryContent}>
206
+ <span className={styles.summaryNumber}>
207
+ {previewData.summary?.updates || 0}
208
+ </span>
209
+ <span className={styles.summaryLabel}>Updates</span>
210
+ </div>
211
+ </div>
212
+
213
+ <div className={styles.summaryCard}>
214
+ <div className={styles.summaryIcon}>
215
+ <AlertTriangle size={20} />
216
+ </div>
217
+ <div className={styles.summaryContent}>
218
+ <span className={styles.summaryNumber}>
219
+ {previewData.summary?.skipped || 0}
220
+ </span>
221
+ <span className={styles.summaryLabel}>Skipped</span>
222
+ </div>
223
+ </div>
224
+ </div>
225
+ </div>
226
+
227
+ {/* Filters and Search */}
228
+ <div className={styles.filtersSection}>
229
+ <div className={styles.searchBox}>
230
+ <Search size={16} />
231
+ <input
232
+ type="text"
233
+ placeholder="Search records..."
234
+ value={searchTerm}
235
+ onChange={(e) => setSearchTerm(e.target.value)}
236
+ />
237
+ </div>
238
+
239
+ <div className={styles.filterBox}>
240
+ <Filter size={16} />
241
+ <select
242
+ value={statusFilter}
243
+ onChange={(e) => setStatusFilter(e.target.value)}
244
+ >
245
+ <option value="all">All Records</option>
246
+ <option value="new">New Only</option>
247
+ <option value="update">Updates Only</option>
248
+ <option value="skip">Skipped Only</option>
249
+ </select>
250
+ </div>
251
+ </div>
252
+
253
+ {/* Records List */}
254
+ <div className={styles.recordsSection}>
255
+ {paginatedRecords.length === 0 ? (
256
+ <div className={styles.emptyState}>
257
+ <p>No records match your current filters</p>
258
+ </div>
259
+ ) : (
260
+ paginatedRecords.map((record, index) => (
261
+ <div key={record.id || index} className={styles.recordCard}>
262
+ <div className={styles.recordHeader}>
263
+ <div className={styles.recordInfo}>
264
+ <span className={getStatusBadge(record.sync_status)}>
265
+ {getStatusIcon(record.sync_status)}
266
+ {record.sync_status?.toUpperCase() || 'UNKNOWN'}
267
+ </span>
268
+ <span className={styles.recordTitle}>
269
+ {record.display_name || record.data?.name || record.data?.email || `Record ${index + 1}`}
270
+ </span>
271
+ </div>
272
+
273
+ <button
274
+ onClick={() => toggleDetails(record.id || index)}
275
+ className={styles.toggleButton}
276
+ >
277
+ {showDetails[record.id || index] ? (
278
+ <>Hide Details <EyeOff size={14} /></>
279
+ ) : (
280
+ <>Show Details <Eye size={14} /></>
281
+ )}
282
+ </button>
283
+ </div>
284
+
285
+ {record.sync_status === 'update' && record.changes && (
286
+ <div className={styles.changesPreview}>
287
+ <strong>Changes:</strong>
288
+ <ul>
289
+ {record.changes.map((change, i) => (
290
+ <li key={i}>
291
+ <span className={styles.fieldName}>{change.field}:</span>
292
+ <span className={styles.oldValue}>{change.old_value}</span>
293
+
294
+ <span className={styles.newValue}>{change.new_value}</span>
295
+ </li>
296
+ ))}
297
+ </ul>
298
+ </div>
299
+ )}
300
+
301
+ {showDetails[record.id || index] && (
302
+ <div className={styles.recordDetails}>
303
+ <div className={styles.dataSection}>
304
+ <h4>External Data (from {provider.display_name})</h4>
305
+ <pre className={styles.jsonData}>
306
+ {JSON.stringify(record.data, null, 2)}
307
+ </pre>
308
+ </div>
309
+
310
+ {record.existing_data && (
311
+ <div className={styles.dataSection}>
312
+ <h4>Current CRM Data</h4>
313
+ <pre className={styles.jsonData}>
314
+ {JSON.stringify(record.existing_data, null, 2)}
315
+ </pre>
316
+ </div>
317
+ )}
318
+ </div>
319
+ )}
320
+ </div>
321
+ ))
322
+ )}
323
+ </div>
324
+
325
+ {/* Pagination */}
326
+ {totalPages > 1 && (
327
+ <div className={styles.paginationSection}>
328
+ <button
329
+ onClick={() => setCurrentPage(prev => Math.max(1, prev - 1))}
330
+ disabled={currentPage === 1}
331
+ className={styles.paginationButton}
332
+ >
333
+ Previous
334
+ </button>
335
+
336
+ <span className={styles.paginationInfo}>
337
+ Page {currentPage} of {totalPages} ({filteredRecords.length} records)
338
+ </span>
339
+
340
+ <button
341
+ onClick={() => setCurrentPage(prev => Math.min(totalPages, prev + 1))}
342
+ disabled={currentPage === totalPages}
343
+ className={styles.paginationButton}
344
+ >
345
+ Next
346
+ </button>
347
+ </div>
348
+ )}
349
+ </>
350
+ ) : null}
351
+ </div>
352
+
353
+ {/* Footer */}
354
+ <div className={styles.modalFooter}>
355
+ <button onClick={onClose} className={styles.cancelButton}>
356
+ Cancel
357
+ </button>
358
+ <button
359
+ onClick={handleConfirmSync}
360
+ className={styles.confirmButton}
361
+ disabled={!previewData?.summary?.total_changes || loading}
362
+ >
363
+ <Download size={16} />
364
+ Import {previewData?.summary?.total_changes || 0} Changes
365
+ </button>
366
+ </div>
367
+ </div>
368
+ </div>
369
+ );
370
+ };
371
+
372
+ export default DataPreviewModal;