(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;
}
})();