(function() { const fileInput = document.getElementById('import-file'); const previewEl = document.getElementById('csv-preview'); const previewTable = document.getElementById('preview-table'); const mappingEl = document.getElementById('column-mapping'); const startBtn = document.getElementById('start-import-btn'); const progressEl = document.getElementById('import-progress'); const resultEl = document.getElementById('import-result'); const csrfToken = document.getElementById('dashboard-import-inline-cfg').dataset.csrfToken; let csvBase64 = ''; let csvHeaders = []; fileInput.addEventListener('change', function(e) { const file = e.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = function(ev) { const text = ev.target.result; csvBase64 = btoa(unescape(encodeURIComponent(text))); const lines = text.split('\n').filter(l => l.trim()); if (lines.length < 2) { alert('CSV must have a header row and at least one data row.'); return; } csvHeaders = parseCSVLine(lines[0]); // Build preview table let html = ''; csvHeaders.forEach(h => { html += '' + escapeHtml(h) + ''; }); html += ''; for (let i = 1; i < Math.min(lines.length, 4); i++) { const cols = parseCSVLine(lines[i]); html += ''; csvHeaders.forEach((_, ci) => { html += '' + escapeHtml(cols[ci] || '') + ''; }); html += ''; } html += ''; previewTable.innerHTML = html; previewEl.classList.remove('hidden'); // Populate mapping selects const selects = mappingEl.querySelectorAll('select'); selects.forEach(sel => { const current = sel.value; sel.innerHTML = ''; csvHeaders.forEach((h, i) => { const opt = document.createElement('option'); opt.value = i; opt.textContent = h; sel.appendChild(opt); }); // Auto-detect common column names autoDetectColumn(sel, csvHeaders); }); mappingEl.classList.remove('hidden'); }; reader.readAsText(file); }); function autoDetectColumn(select, headers) { const id = select.id; const lower = headers.map(h => h.toLowerCase().trim()); const patterns = { 'map-email': ['email', 'e-mail', 'email_address', 'subscriber_email'], 'map-name': ['name', 'full_name', 'display_name', 'subscriber_name'], 'map-amount': ['amount', 'total', 'price', 'payment', 'lifetime_amount'], 'map-date': ['date', 'created_at', 'joined', 'start_date', 'signup_date'], 'map-item-title': ['item', 'product', 'title', 'item_title', 'product_name'], 'map-tier': ['tier', 'plan', 'level', 'membership'], 'map-status': ['status', 'state', 'active'] }; const p = patterns[id] || []; for (let i = 0; i < lower.length; i++) { if (p.includes(lower[i])) { select.value = i; return; } } } startBtn.addEventListener('click', function() { const projectId = document.getElementById('import-project').value; if (!csvBase64) { alert('Please select a CSV file first.'); return; } const mapping = {}; const mapEmail = document.getElementById('map-email').value; const mapName = document.getElementById('map-name').value; const mapAmount = document.getElementById('map-amount').value; const mapDate = document.getElementById('map-date').value; const mapItem = document.getElementById('map-item-title').value; const mapTier = document.getElementById('map-tier').value; const mapStatus = document.getElementById('map-status').value; if (mapEmail) mapping.email = parseInt(mapEmail); if (mapName) mapping.name = parseInt(mapName); if (mapAmount) mapping.amount = parseInt(mapAmount); if (mapDate) mapping.date = parseInt(mapDate); if (mapItem) mapping.item_title = parseInt(mapItem); if (mapTier) mapping.tier = parseInt(mapTier); if (mapStatus) mapping.status = parseInt(mapStatus); if (!mapping.email && !mapping.amount) { alert('Please map at least an email or amount column.'); return; } startBtn.disabled = true; startBtn.textContent = 'Starting...'; fetch('/api/users/me/import', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrfToken }, body: JSON.stringify({ project_id: projectId, source: 'generic_csv', csv_data: csvBase64, column_mapping: mapping }) }) .then(r => r.json()) .then(data => { if (data.error) { resultEl.className = 'import-result error'; resultEl.textContent = data.error; startBtn.disabled = false; startBtn.textContent = 'Start Import'; return; } progressEl.classList.remove('hidden'); pollProgress(data.job_id); }) .catch(err => { resultEl.className = 'import-result error'; resultEl.textContent = 'Failed to start import: ' + err.message; startBtn.disabled = false; startBtn.textContent = 'Start Import'; }); }); function pollProgress(jobId) { const interval = setInterval(() => { fetch('/api/users/me/import/' + jobId) .then(r => r.json()) .then(data => { const pct = data.total_rows > 0 ? Math.round((data.processed_rows / data.total_rows) * 100) : 0; document.getElementById('progress-bar').style.width = pct + '%'; document.getElementById('stat-processed').textContent = data.processed_rows; document.getElementById('stat-created').textContent = data.created_rows; document.getElementById('stat-skipped').textContent = data.skipped_rows; if (data.status === 'completed' || data.status === 'failed') { clearInterval(interval); progressEl.classList.add('hidden'); resultEl.classList.remove('hidden'); if (data.status === 'completed') { resultEl.className = 'import-result success'; resultEl.innerHTML = 'Import complete. ' + data.created_rows + ' created, ' + data.skipped_rows + ' skipped.'; } else { resultEl.className = 'import-result error'; resultEl.innerHTML = 'Import failed.'; } if (data.error_log) { resultEl.innerHTML += '
' + escapeHtml(data.error_log) + '
'; } startBtn.disabled = false; startBtn.textContent = 'Start Import'; } }); }, 2000); } function parseCSVLine(line) { const result = []; let current = ''; let inQuotes = false; for (let i = 0; i < line.length; i++) { const ch = line[i]; if (inQuotes) { if (ch === '"' && line[i+1] === '"') { current += '"'; i++; } else if (ch === '"') { inQuotes = false; } else { current += ch; } } else { if (ch === '"') { inQuotes = true; } else if (ch === ',') { result.push(current.trim()); current = ''; } else { current += ch; } } } result.push(current.trim()); return result; } })();