Skip to main content

max / makenotwork

8.5 KB · 212 lines History Blame Raw
1 (function() {
2 const fileInput = document.getElementById('import-file');
3 const previewEl = document.getElementById('csv-preview');
4 const previewTable = document.getElementById('preview-table');
5 const mappingEl = document.getElementById('column-mapping');
6 const startBtn = document.getElementById('start-import-btn');
7 const progressEl = document.getElementById('import-progress');
8 const resultEl = document.getElementById('import-result');
9
10 const csrfToken = document.getElementById('dashboard-import-inline-cfg').dataset.csrfToken;
11
12 let csvBase64 = '';
13 let csvHeaders = [];
14
15 fileInput.addEventListener('change', function(e) {
16 const file = e.target.files[0];
17 if (!file) return;
18
19 const reader = new FileReader();
20 reader.onload = function(ev) {
21 const text = ev.target.result;
22 csvBase64 = btoa(unescape(encodeURIComponent(text)));
23
24 const lines = text.split('\n').filter(l => l.trim());
25 if (lines.length < 2) {
26 alert('CSV must have a header row and at least one data row.');
27 return;
28 }
29
30 csvHeaders = parseCSVLine(lines[0]);
31
32 // Build preview table
33 let html = '<thead><tr>';
34 csvHeaders.forEach(h => { html += '<th>' + escapeHtml(h) + '</th>'; });
35 html += '</tr></thead><tbody>';
36 for (let i = 1; i < Math.min(lines.length, 4); i++) {
37 const cols = parseCSVLine(lines[i]);
38 html += '<tr>';
39 csvHeaders.forEach((_, ci) => {
40 html += '<td>' + escapeHtml(cols[ci] || '') + '</td>';
41 });
42 html += '</tr>';
43 }
44 html += '</tbody>';
45 previewTable.innerHTML = html;
46 previewEl.classList.remove('hidden');
47
48 // Populate mapping selects
49 const selects = mappingEl.querySelectorAll('select');
50 selects.forEach(sel => {
51 const current = sel.value;
52 sel.innerHTML = '<option value="">-- skip --</option>';
53 csvHeaders.forEach((h, i) => {
54 const opt = document.createElement('option');
55 opt.value = i;
56 opt.textContent = h;
57 sel.appendChild(opt);
58 });
59 // Auto-detect common column names
60 autoDetectColumn(sel, csvHeaders);
61 });
62 mappingEl.classList.remove('hidden');
63 };
64 reader.readAsText(file);
65 });
66
67 function autoDetectColumn(select, headers) {
68 const id = select.id;
69 const lower = headers.map(h => h.toLowerCase().trim());
70 const patterns = {
71 'map-email': ['email', 'e-mail', 'email_address', 'subscriber_email'],
72 'map-name': ['name', 'full_name', 'display_name', 'subscriber_name'],
73 'map-amount': ['amount', 'total', 'price', 'payment', 'lifetime_amount'],
74 'map-date': ['date', 'created_at', 'joined', 'start_date', 'signup_date'],
75 'map-item-title': ['item', 'product', 'title', 'item_title', 'product_name'],
76 'map-tier': ['tier', 'plan', 'level', 'membership'],
77 'map-status': ['status', 'state', 'active']
78 };
79 const p = patterns[id] || [];
80 for (let i = 0; i < lower.length; i++) {
81 if (p.includes(lower[i])) {
82 select.value = i;
83 return;
84 }
85 }
86 }
87
88 startBtn.addEventListener('click', function() {
89 const projectId = document.getElementById('import-project').value;
90 if (!csvBase64) {
91 alert('Please select a CSV file first.');
92 return;
93 }
94
95 const mapping = {};
96 const mapEmail = document.getElementById('map-email').value;
97 const mapName = document.getElementById('map-name').value;
98 const mapAmount = document.getElementById('map-amount').value;
99 const mapDate = document.getElementById('map-date').value;
100 const mapItem = document.getElementById('map-item-title').value;
101 const mapTier = document.getElementById('map-tier').value;
102 const mapStatus = document.getElementById('map-status').value;
103
104 if (mapEmail) mapping.email = parseInt(mapEmail);
105 if (mapName) mapping.name = parseInt(mapName);
106 if (mapAmount) mapping.amount = parseInt(mapAmount);
107 if (mapDate) mapping.date = parseInt(mapDate);
108 if (mapItem) mapping.item_title = parseInt(mapItem);
109 if (mapTier) mapping.tier = parseInt(mapTier);
110 if (mapStatus) mapping.status = parseInt(mapStatus);
111
112 if (!mapping.email && !mapping.amount) {
113 alert('Please map at least an email or amount column.');
114 return;
115 }
116
117 startBtn.disabled = true;
118 startBtn.textContent = 'Starting...';
119
120 fetch('/api/users/me/import', {
121 method: 'POST',
122 headers: {
123 'Content-Type': 'application/json',
124 'X-CSRF-Token': csrfToken
125 },
126 body: JSON.stringify({
127 project_id: projectId,
128 source: 'generic_csv',
129 csv_data: csvBase64,
130 column_mapping: mapping
131 })
132 })
133 .then(r => r.json())
134 .then(data => {
135 if (data.error) {
136 resultEl.className = 'import-result error';
137 resultEl.textContent = data.error;
138 startBtn.disabled = false;
139 startBtn.textContent = 'Start Import';
140 return;
141 }
142 progressEl.classList.remove('hidden');
143 pollProgress(data.job_id);
144 })
145 .catch(err => {
146 resultEl.className = 'import-result error';
147 resultEl.textContent = 'Failed to start import: ' + err.message;
148 startBtn.disabled = false;
149 startBtn.textContent = 'Start Import';
150 });
151 });
152
153 function pollProgress(jobId) {
154 const interval = setInterval(() => {
155 fetch('/api/users/me/import/' + jobId)
156 .then(r => r.json())
157 .then(data => {
158 const pct = data.total_rows > 0
159 ? Math.round((data.processed_rows / data.total_rows) * 100)
160 : 0;
161 document.getElementById('progress-bar').style.width = pct + '%';
162 document.getElementById('stat-processed').textContent = data.processed_rows;
163 document.getElementById('stat-created').textContent = data.created_rows;
164 document.getElementById('stat-skipped').textContent = data.skipped_rows;
165
166 if (data.status === 'completed' || data.status === 'failed') {
167 clearInterval(interval);
168 progressEl.classList.add('hidden');
169
170 resultEl.classList.remove('hidden');
171 if (data.status === 'completed') {
172 resultEl.className = 'import-result success';
173 resultEl.innerHTML = '<strong>Import complete.</strong> ' +
174 data.created_rows + ' created, ' +
175 data.skipped_rows + ' skipped.';
176 } else {
177 resultEl.className = 'import-result error';
178 resultEl.innerHTML = '<strong>Import failed.</strong>';
179 }
180 if (data.error_log) {
181 resultEl.innerHTML += '<div class="error-log">' +
182 escapeHtml(data.error_log) + '</div>';
183 }
184 startBtn.disabled = false;
185 startBtn.textContent = 'Start Import';
186 }
187 });
188 }, 2000);
189 }
190
191 function parseCSVLine(line) {
192 const result = [];
193 let current = '';
194 let inQuotes = false;
195 for (let i = 0; i < line.length; i++) {
196 const ch = line[i];
197 if (inQuotes) {
198 if (ch === '"' && line[i+1] === '"') { current += '"'; i++; }
199 else if (ch === '"') { inQuotes = false; }
200 else { current += ch; }
201 } else {
202 if (ch === '"') { inQuotes = true; }
203 else if (ch === ',') { result.push(current.trim()); current = ''; }
204 else { current += ch; }
205 }
206 }
207 result.push(current.trim());
208 return result;
209 }
210
211 })();
212