diff --git a/web/static/css/style.css b/web/static/css/style.css
index ca7b2fe..8121ff5 100644
--- a/web/static/css/style.css
+++ b/web/static/css/style.css
@@ -405,16 +405,23 @@ body {
/* Action buttons in tables */
.action-buttons {
display: flex;
- gap: var(--space-2);
- flex-wrap: wrap;
+ gap: var(--space-1);
+ flex-wrap: nowrap;
+ align-items: center;
min-width: 200px;
}
+.action-buttons .btn {
+ padding: var(--space-1) var(--space-2);
+ font-size: var(--font-size-sm);
+ white-space: nowrap;
+}
+
/* Ensure actions column is wide enough */
.table th:last-child,
.table td:last-child {
- min-width: 220px;
- width: 220px;
+ min-width: 200px;
+ width: 200px;
}
/* Subscriptions Grid */
@@ -548,6 +555,11 @@ body {
padding: var(--space-4) var(--space-6) var(--space-6);
}
+/* Forms inside modals need proper padding */
+.modal form {
+ padding: var(--space-4) var(--space-6) 0;
+}
+
/* Forms */
.form-group {
margin-bottom: var(--space-5);
@@ -684,14 +696,19 @@ body {
}
.action-buttons {
- flex-direction: column;
+ gap: var(--space-1);
min-width: auto;
}
+ .action-buttons .btn {
+ padding: var(--space-1) var(--space-2);
+ font-size: var(--font-size-xs);
+ }
+
.table th:last-child,
.table td:last-child {
- min-width: auto;
- width: auto;
+ min-width: 180px;
+ width: 180px;
}
}
@@ -854,6 +871,339 @@ body {
.mt-4 { margin-top: var(--space-4); }
.hidden { display: none; }
+/* Bulk Import Styles */
+.modal-large .modal-content {
+ max-width: 800px;
+ width: 90vw;
+}
+
+.import-step {
+ margin-bottom: var(--space-6);
+}
+
+.import-step h4 {
+ color: var(--gray-800);
+ font-size: var(--font-size-lg);
+ font-weight: 600;
+ margin-bottom: var(--space-4);
+ display: flex;
+ align-items: center;
+ gap: var(--space-2);
+}
+
+.file-upload-area {
+ border: 2px dashed var(--gray-300);
+ border-radius: var(--border-radius);
+ padding: var(--space-8);
+ text-align: center;
+ transition: var(--transition);
+ cursor: pointer;
+ background: var(--gray-50);
+}
+
+.file-upload-area:hover {
+ border-color: var(--primary-color);
+ background: rgba(37, 99, 235, 0.05);
+}
+
+.file-upload-area.dragover {
+ border-color: var(--primary-color);
+ background: rgba(37, 99, 235, 0.05);
+}
+
+.file-upload-content i {
+ font-size: var(--font-size-3xl);
+ color: var(--gray-400);
+ margin-bottom: var(--space-3);
+}
+
+.file-upload-content p {
+ color: var(--gray-600);
+ font-weight: 500;
+}
+
+.file-info {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: var(--space-4);
+ background: var(--gray-50);
+ border-radius: var(--border-radius);
+ margin-top: var(--space-4);
+}
+
+.file-details {
+ display: flex;
+ align-items: center;
+ gap: var(--space-3);
+}
+
+.file-details i {
+ color: var(--success-color);
+ font-size: var(--font-size-lg);
+}
+
+.csv-format-help {
+ margin-top: var(--space-6);
+ padding: var(--space-4);
+ background: var(--gray-50);
+ border-radius: var(--border-radius);
+}
+
+.csv-format-help h5 {
+ color: var(--gray-800);
+ margin-bottom: var(--space-3);
+ display: flex;
+ align-items: center;
+ gap: var(--space-2);
+}
+
+.code-example {
+ background: var(--gray-800);
+ color: var(--gray-100);
+ padding: var(--space-4);
+ border-radius: var(--border-radius);
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
+ font-size: var(--font-size-sm);
+ margin: var(--space-3) 0;
+ overflow-x: auto;
+}
+
+.preview-stats {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
+ gap: var(--space-4);
+ margin-bottom: var(--space-6);
+}
+
+.stat-item {
+ text-align: center;
+ padding: var(--space-4);
+ background: var(--gray-50);
+ border-radius: var(--border-radius);
+}
+
+.stat-value {
+ display: block;
+ font-size: var(--font-size-2xl);
+ font-weight: 700;
+ color: var(--primary-color);
+ margin-bottom: var(--space-1);
+}
+
+.stat-label {
+ color: var(--gray-600);
+ font-size: var(--font-size-sm);
+ font-weight: 500;
+}
+
+.preview-table-container {
+ max-height: 300px;
+ overflow-y: auto;
+ border: 1px solid var(--gray-200);
+ border-radius: var(--border-radius);
+ margin-bottom: var(--space-4);
+}
+
+.preview-table-container .table {
+ margin-bottom: 0;
+}
+
+.preview-table-container .table th {
+ position: sticky;
+ top: 0;
+ background: var(--gray-50);
+ z-index: 1;
+}
+
+.error-list {
+ background: rgba(239, 68, 68, 0.1);
+ border: 1px solid rgba(239, 68, 68, 0.2);
+ border-radius: var(--border-radius);
+ padding: var(--space-4);
+}
+
+.error-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: var(--space-3);
+}
+
+.error-list h5 {
+ color: var(--danger-color);
+ margin-bottom: 0;
+ display: flex;
+ align-items: center;
+ gap: var(--space-2);
+}
+
+.error-list ul {
+ color: var(--danger-color);
+ margin-left: var(--space-5);
+}
+
+.error-summary {
+ color: var(--danger-color);
+ font-size: var(--font-size-sm);
+ margin-bottom: var(--space-3);
+}
+
+.error-table-container {
+ max-height: 300px;
+ overflow-y: auto;
+ border: 1px solid rgba(239, 68, 68, 0.2);
+ border-radius: var(--border-radius);
+ margin-top: var(--space-3);
+}
+
+.error-table-container .table {
+ margin-bottom: 0;
+ font-size: var(--font-size-sm);
+}
+
+.error-table-container .table th {
+ position: sticky;
+ top: 0;
+ background: rgba(239, 68, 68, 0.1);
+ color: var(--danger-color);
+ border-bottom: 2px solid rgba(239, 68, 68, 0.2);
+ z-index: 1;
+}
+
+.error-table-container .table td {
+ border-color: rgba(239, 68, 68, 0.1);
+ color: var(--danger-color);
+}
+
+.raw-data-cell {
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
+ font-size: var(--font-size-xs);
+ max-width: 200px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.raw-data-cell:hover {
+ overflow: visible;
+ white-space: normal;
+ word-break: break-all;
+}
+
+.list-selection {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
+ gap: var(--space-3);
+ margin-bottom: var(--space-4);
+}
+
+.list-checkbox {
+ display: flex;
+ align-items: center;
+ padding: var(--space-3);
+ background: var(--gray-50);
+ border-radius: var(--border-radius);
+ cursor: pointer;
+ transition: var(--transition);
+}
+
+.list-checkbox:hover {
+ background: var(--gray-100);
+}
+
+.list-checkbox input[type="checkbox"] {
+ margin-right: var(--space-3);
+ transform: scale(1.2);
+}
+
+.list-checkbox-label {
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-1);
+}
+
+.list-checkbox-name {
+ font-weight: 500;
+ color: var(--gray-800);
+}
+
+.list-checkbox-email {
+ font-size: var(--font-size-sm);
+ color: var(--gray-600);
+}
+
+.selection-info {
+ padding: var(--space-4);
+ background: rgba(59, 130, 246, 0.1);
+ border-radius: var(--border-radius);
+}
+
+.import-results {
+ text-align: center;
+}
+
+.result-stats {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
+ gap: var(--space-4);
+ margin-bottom: var(--space-6);
+}
+
+.result-stats .stat-item {
+ background: rgba(16, 185, 129, 0.1);
+ border: 1px solid rgba(16, 185, 129, 0.2);
+}
+
+.result-stats .stat-value {
+ color: var(--success-color);
+}
+
+.result-errors {
+ text-align: left;
+ background: rgba(239, 68, 68, 0.1);
+ border: 1px solid rgba(239, 68, 68, 0.2);
+ border-radius: var(--border-radius);
+ padding: var(--space-4);
+}
+
+.result-errors h5 {
+ color: var(--danger-color);
+ margin-bottom: var(--space-3);
+ display: flex;
+ align-items: center;
+ gap: var(--space-2);
+}
+
+.result-errors ul {
+ color: var(--danger-color);
+ margin-left: var(--space-5);
+}
+
+.button-group {
+ display: flex;
+ gap: var(--space-3);
+}
+
+/* Row status indicators for preview table */
+.row-status {
+ font-size: var(--font-size-xs);
+ padding: var(--space-1) var(--space-2);
+ border-radius: 12px;
+ font-weight: 500;
+ text-transform: uppercase;
+}
+
+.row-status.valid {
+ background: rgba(16, 185, 129, 0.1);
+ color: var(--success-color);
+}
+
+.row-status.error {
+ background: rgba(239, 68, 68, 0.1);
+ color: var(--danger-color);
+}
+
/* Animations */
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
diff --git a/web/static/js/api.js b/web/static/js/api.js
index bae5b62..3af37e3 100644
--- a/web/static/js/api.js
+++ b/web/static/js/api.js
@@ -184,6 +184,19 @@ class APIClient {
method: 'DELETE'
});
}
+
+ /**
+ * Bulk import members from CSV data
+ */
+ async bulkImportMembers(csvData, listIds) {
+ return this.request('/bulk-import', {
+ method: 'POST',
+ body: JSON.stringify({
+ csv_data: csvData,
+ list_ids: listIds
+ })
+ });
+ }
}
/**
diff --git a/web/static/js/app.js b/web/static/js/app.js
index 38d7bf8..652cf4e 100644
--- a/web/static/js/app.js
+++ b/web/static/js/app.js
@@ -45,6 +45,11 @@ class MailingListApp {
this.handleLogin();
}
});
+
+ // Bulk import button
+ document.getElementById('showBulkImportBtn').addEventListener('click', () => {
+ uiManager.showBulkImportModal();
+ });
}
/**
diff --git a/web/static/js/ui.js b/web/static/js/ui.js
index 37330a5..964a811 100644
--- a/web/static/js/ui.js
+++ b/web/static/js/ui.js
@@ -50,6 +50,9 @@ class UIManager {
this.showListModal();
});
+ // Initialize bulk import listeners
+ this.initializeBulkImportListeners();
+
document.getElementById('addMemberBtn').addEventListener('click', () => {
this.showMemberModal();
});
@@ -634,6 +637,546 @@ class UIManager {
return link;
}
+ /**
+ * Show bulk import modal
+ */
+ showBulkImportModal() {
+ this.resetBulkImportModal();
+ this.showModal(document.getElementById('bulkImportModal'));
+ }
+
+ /**
+ * Reset bulk import modal state
+ */
+ resetBulkImportModal() {
+ // Reset to step 1
+ document.querySelectorAll('.import-step').forEach(step => {
+ step.style.display = 'none';
+ });
+ document.getElementById('importStep1').style.display = 'block';
+
+ // Reset button states
+ document.getElementById('bulkImportBackBtn').style.display = 'none';
+ document.getElementById('bulkImportNextBtn').style.display = 'inline-block';
+ document.getElementById('bulkImportNextBtn').disabled = true;
+ document.getElementById('bulkImportBtn').style.display = 'none';
+ document.getElementById('bulkImportDoneBtn').style.display = 'none';
+
+ // Clear file input
+ document.getElementById('csvFileInput').value = '';
+ document.getElementById('fileInfo').style.display = 'none';
+
+ // Reset parsed data
+ this.csvData = null;
+ this.parsedRows = [];
+ this.currentStep = 1;
+ }
+
+ /**
+ * Initialize bulk import event listeners
+ */
+ initializeBulkImportListeners() {
+ const fileUploadArea = document.getElementById('fileUploadArea');
+ const csvFileInput = document.getElementById('csvFileInput');
+ const nextBtn = document.getElementById('bulkImportNextBtn');
+ const backBtn = document.getElementById('bulkImportBackBtn');
+ const importBtn = document.getElementById('bulkImportBtn');
+ const doneBtn = document.getElementById('bulkImportDoneBtn');
+ const removeFileBtn = document.getElementById('removeFileBtn');
+
+ // File upload area click
+ fileUploadArea.addEventListener('click', () => {
+ csvFileInput.click();
+ });
+
+ // File input change
+ csvFileInput.addEventListener('change', (e) => {
+ this.handleFileSelection(e.target.files[0]);
+ });
+
+ // Remove file button
+ removeFileBtn.addEventListener('click', () => {
+ this.removeSelectedFile();
+ });
+
+ // Drag and drop
+ fileUploadArea.addEventListener('dragover', (e) => {
+ e.preventDefault();
+ fileUploadArea.classList.add('dragover');
+ });
+
+ fileUploadArea.addEventListener('dragleave', () => {
+ fileUploadArea.classList.remove('dragover');
+ });
+
+ fileUploadArea.addEventListener('drop', (e) => {
+ e.preventDefault();
+ fileUploadArea.classList.remove('dragover');
+ const files = e.dataTransfer.files;
+ if (files.length > 0) {
+ this.handleFileSelection(files[0]);
+ }
+ });
+
+ // Navigation buttons
+ nextBtn.addEventListener('click', () => {
+ this.handleBulkImportNext();
+ });
+
+ backBtn.addEventListener('click', () => {
+ this.handleBulkImportBack();
+ });
+
+ importBtn.addEventListener('click', () => {
+ this.handleBulkImportSubmit();
+ });
+
+ doneBtn.addEventListener('click', () => {
+ this.closeModal(document.getElementById('bulkImportModal'));
+ // Reload data to show imported members
+ if (window.app) {
+ window.app.loadData();
+ }
+ });
+ }
+
+ /**
+ * Handle file selection
+ */
+ handleFileSelection(file) {
+ if (!file) return;
+
+ if (!file.name.toLowerCase().endsWith('.csv')) {
+ this.showNotification('Please select a CSV file', 'error');
+ return;
+ }
+
+ const reader = new FileReader();
+ reader.onload = (e) => {
+ this.csvData = e.target.result;
+ this.displayFileInfo(file);
+ this.parseCSVPreview();
+ document.getElementById('bulkImportNextBtn').disabled = false;
+ };
+ reader.readAsText(file);
+ }
+
+ /**
+ * Display file information
+ */
+ displayFileInfo(file) {
+ document.getElementById('fileName').textContent = file.name;
+ document.getElementById('fileSize').textContent = `(${this.formatFileSize(file.size)})`;
+ document.getElementById('fileInfo').style.display = 'flex';
+ }
+
+ /**
+ * Remove selected file
+ */
+ removeSelectedFile() {
+ document.getElementById('csvFileInput').value = '';
+ document.getElementById('fileInfo').style.display = 'none';
+ document.getElementById('bulkImportNextBtn').disabled = true;
+ this.csvData = null;
+ this.parsedRows = [];
+ }
+
+ /**
+ * Format file size in human readable format
+ */
+ formatFileSize(bytes) {
+ if (bytes === 0) return '0 B';
+ const k = 1024;
+ const sizes = ['B', 'KB', 'MB', 'GB'];
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
+ }
+
+ /**
+ * Parse CSV for preview
+ */
+ parseCSVPreview() {
+ if (!this.csvData) return;
+
+ const lines = this.csvData.trim().split('\n');
+ if (lines.length < 2) {
+ this.showNotification('CSV file must contain at least a header and one data row', 'error');
+ return;
+ }
+
+ // Parse header
+ const headers = this.parseCSVLine(lines[0]);
+ const nameIndex = headers.findIndex(h => h.toLowerCase().includes('name'));
+ const emailIndex = headers.findIndex(h => h.toLowerCase().includes('email'));
+
+ if (nameIndex === -1 || emailIndex === -1) {
+ this.showNotification('CSV must contain Name and Email columns', 'error');
+ return;
+ }
+
+ this.parsedRows = [];
+ this.csvHeaders = headers;
+ let validRows = 0;
+ let errorRows = 0;
+
+ // Parse data rows
+ for (let i = 1; i < lines.length; i++) {
+ const rawLine = lines[i].trim();
+ if (!rawLine) continue; // Skip empty lines
+
+ const values = this.parseCSVLine(rawLine);
+ const name = (values[nameIndex] || '').trim();
+ const email = (values[emailIndex] || '').trim();
+
+ // Determine error type and message
+ let error = null;
+ let isValid = true;
+
+ if (!email) {
+ error = 'Missing email address';
+ isValid = false;
+ } else if (!this.isValidEmail(email)) {
+ error = 'Invalid email format';
+ isValid = false;
+ }
+ // Note: Missing name is OK - we'll use the email as display name if needed
+
+ const row = {
+ index: i,
+ name,
+ email,
+ valid: isValid,
+ error,
+ rawLine,
+ parsedValues: values,
+ headers: headers
+ };
+
+ this.parsedRows.push(row);
+ if (isValid) validRows++;
+ else errorRows++;
+ }
+
+ // Update preview stats
+ document.getElementById('totalRowsCount').textContent = this.parsedRows.length;
+ document.getElementById('validRowsCount').textContent = validRows;
+ document.getElementById('errorRowsCount').textContent = errorRows;
+ }
+
+ /**
+ * Parse a CSV line handling quoted values
+ */
+ parseCSVLine(line) {
+ const result = [];
+ let current = '';
+ let inQuotes = false;
+
+ for (let i = 0; i < line.length; i++) {
+ const char = line[i];
+
+ if (char === '"') {
+ inQuotes = !inQuotes;
+ } else if (char === ',' && !inQuotes) {
+ result.push(current.trim());
+ current = '';
+ } else {
+ current += char;
+ }
+ }
+
+ result.push(current.trim());
+ return result;
+ }
+
+ /**
+ * Simple email validation
+ */
+ isValidEmail(email) {
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
+ return emailRegex.test(email);
+ }
+
+ /**
+ * Handle next button click
+ */
+ handleBulkImportNext() {
+ if (this.currentStep === 1) {
+ this.showStep2Preview();
+ } else if (this.currentStep === 2) {
+ this.showStep3ListSelection();
+ }
+ }
+
+ /**
+ * Handle back button click
+ */
+ handleBulkImportBack() {
+ if (this.currentStep === 2) {
+ this.showStep1Upload();
+ } else if (this.currentStep === 3) {
+ this.showStep2Preview();
+ }
+ }
+
+ /**
+ * Show step 1 (upload)
+ */
+ showStep1Upload() {
+ document.querySelectorAll('.import-step').forEach(step => {
+ step.style.display = 'none';
+ });
+ document.getElementById('importStep1').style.display = 'block';
+
+ document.getElementById('bulkImportBackBtn').style.display = 'none';
+ document.getElementById('bulkImportNextBtn').style.display = 'inline-block';
+ document.getElementById('bulkImportBtn').style.display = 'none';
+
+ this.currentStep = 1;
+ }
+
+ /**
+ * Show step 2 (preview)
+ */
+ showStep2Preview() {
+ document.querySelectorAll('.import-step').forEach(step => {
+ step.style.display = 'none';
+ });
+ document.getElementById('importStep2').style.display = 'block';
+
+ document.getElementById('bulkImportBackBtn').style.display = 'inline-block';
+ document.getElementById('bulkImportNextBtn').style.display = 'inline-block';
+ document.getElementById('bulkImportBtn').style.display = 'none';
+
+ this.renderPreviewTable();
+ this.currentStep = 2;
+ }
+
+ /**
+ * Show step 3 (list selection)
+ */
+ showStep3ListSelection() {
+ document.querySelectorAll('.import-step').forEach(step => {
+ step.style.display = 'none';
+ });
+ document.getElementById('importStep3').style.display = 'block';
+
+ document.getElementById('bulkImportBackBtn').style.display = 'inline-block';
+ document.getElementById('bulkImportNextBtn').style.display = 'none';
+ document.getElementById('bulkImportBtn').style.display = 'inline-block';
+
+ this.renderListSelection();
+ this.currentStep = 3;
+ }
+
+ /**
+ * Render preview table
+ */
+ renderPreviewTable() {
+ const tbody = document.getElementById('previewTableBody');
+ tbody.innerHTML = '';
+
+ // Show first 10 rows for preview
+ const previewRows = this.parsedRows.slice(0, 10);
+
+ previewRows.forEach(row => {
+ const tr = document.createElement('tr');
+ tr.innerHTML = `
+
${row.index}
+
${this.escapeHtml(row.name)}
+
${this.escapeHtml(row.email)}
+
+
+ ${row.valid ? 'Valid' : 'Error'}
+
+
+ `;
+ tbody.appendChild(tr);
+ });
+
+ // Show errors if any
+ const errors = this.parsedRows.filter(row => !row.valid);
+ if (errors.length > 0) {
+ document.getElementById('errorList').style.display = 'block';
+ this.renderErrorDisplay(errors);
+ } else {
+ document.getElementById('errorList').style.display = 'none';
+ }
+ }
+
+ /**
+ * Render error display with expandable details
+ */
+ renderErrorDisplay(errors) {
+ // Update error summary
+ const errorSummary = document.getElementById('errorSummary');
+ const errorTypes = {};
+
+ errors.forEach(error => {
+ const type = error.error;
+ errorTypes[type] = (errorTypes[type] || 0) + 1;
+ });
+
+ const summaryText = Object.entries(errorTypes)
+ .map(([type, count]) => `${count} ${type.toLowerCase()}`)
+ .join(', ');
+
+ errorSummary.innerHTML = `
+
${errors.length} rows have issues: ${summaryText}
+
These rows will be skipped during import. Click "Show All" to see details.
+ `;
+
+ // Render error table
+ const errorTableBody = document.getElementById('errorTableBody');
+ errorTableBody.innerHTML = '';
+
+ errors.forEach(row => {
+ const tr = document.createElement('tr');
+ tr.innerHTML = `
+
${row.index + 1}
+
${this.escapeHtml(row.error)}
+
${this.escapeHtml(row.name || '-')}
+
${this.escapeHtml(row.email || '-')}
+
${this.escapeHtml(row.rawLine)}
+ `;
+ errorTableBody.appendChild(tr);
+ });
+
+ // Setup toggle functionality
+ this.setupErrorToggle();
+ }
+
+ /**
+ * Setup error toggle functionality
+ */
+ setupErrorToggle() {
+ const toggleBtn = document.getElementById('toggleErrorsBtn');
+ const errorDetails = document.getElementById('errorDetails');
+ const toggleText = document.getElementById('errorToggleText');
+ const toggleIcon = document.getElementById('errorToggleIcon');
+
+ // Remove existing listener to prevent duplicates
+ const newToggleBtn = toggleBtn.cloneNode(true);
+ toggleBtn.parentNode.replaceChild(newToggleBtn, toggleBtn);
+
+ newToggleBtn.addEventListener('click', () => {
+ const isHidden = errorDetails.style.display === 'none';
+
+ if (isHidden) {
+ errorDetails.style.display = 'block';
+ toggleText.textContent = 'Hide Details';
+ toggleIcon.className = 'fas fa-chevron-up';
+ } else {
+ errorDetails.style.display = 'none';
+ toggleText.textContent = 'Show All';
+ toggleIcon.className = 'fas fa-chevron-down';
+ }
+ });
+ }
+
+ /**
+ * Render list selection checkboxes
+ */
+ renderListSelection() {
+ const container = document.getElementById('listSelection');
+ container.innerHTML = '';
+
+ if (!window.app || !window.app.lists) {
+ container.innerHTML = '
No mailing lists available
';
+ return;
+ }
+
+ window.app.lists.forEach(list => {
+ const div = document.createElement('div');
+ div.className = 'list-checkbox';
+ div.innerHTML = `
+
+
+ ${this.escapeHtml(list.list_name)}
+ ${this.escapeHtml(list.list_email)}
+
+ `;
+ container.appendChild(div);
+
+ // Make the whole div clickable
+ div.addEventListener('click', (e) => {
+ if (e.target.type !== 'checkbox') {
+ const checkbox = div.querySelector('input[type="checkbox"]');
+ checkbox.checked = !checkbox.checked;
+ }
+ });
+ });
+ }
+
+ /**
+ * Handle bulk import submission
+ */
+ async handleBulkImportSubmit() {
+ const selectedLists = Array.from(document.querySelectorAll('#listSelection input[type="checkbox"]:checked'))
+ .map(cb => parseInt(cb.value));
+
+ if (selectedLists.length === 0) {
+ this.showNotification('Please select at least one mailing list', 'error');
+ return;
+ }
+
+ try {
+ this.setLoading(true);
+
+ const result = await apiClient.bulkImportMembers(this.csvData, selectedLists);
+
+ this.showStep4Results(result);
+
+ } catch (error) {
+ this.handleError(error, 'Bulk import failed');
+ } finally {
+ this.setLoading(false);
+ }
+ }
+
+ /**
+ * Show step 4 (results)
+ */
+ showStep4Results(result) {
+ document.querySelectorAll('.import-step').forEach(step => {
+ step.style.display = 'none';
+ });
+ document.getElementById('importStep4').style.display = 'block';
+
+ document.getElementById('bulkImportBackBtn').style.display = 'none';
+ document.getElementById('bulkImportNextBtn').style.display = 'none';
+ document.getElementById('bulkImportBtn').style.display = 'none';
+ document.getElementById('bulkImportDoneBtn').style.display = 'inline-block';
+
+ // Update result stats
+ document.getElementById('processedCount').textContent = result.processed_rows;
+ document.getElementById('createdCount').textContent = result.created_members;
+ document.getElementById('updatedCount').textContent = result.updated_members;
+ document.getElementById('subscriptionsCount').textContent = result.subscriptions_added;
+
+ // Show errors if any
+ if (result.errors && result.errors.length > 0) {
+ document.getElementById('resultErrors').style.display = 'block';
+ const errorList = document.getElementById('resultErrorList');
+ errorList.innerHTML = '';
+
+ result.errors.forEach(error => {
+ const li = document.createElement('li');
+ li.textContent = error;
+ errorList.appendChild(li);
+ });
+ } else {
+ document.getElementById('resultErrors').style.display = 'none';
+ }
+
+ this.currentStep = 4;
+
+ // Show success notification
+ this.showNotification(
+ `Successfully imported ${result.processed_rows} members with ${result.subscriptions_added} subscriptions`,
+ 'success'
+ );
+ }
+
/**
* Escape HTML to prevent XSS
*/