763 lines
33 KiB
JavaScript
763 lines
33 KiB
JavaScript
/**
|
|
* Admin Import JavaScript
|
|
* Handles CSV file import functionality
|
|
*/
|
|
|
|
class ImportManager {
|
|
constructor() {
|
|
this.supportedTables = [];
|
|
this.batchFileCount = 0;
|
|
this.currentImportId = null;
|
|
this.pollInterval = null;
|
|
this.bulkFiles = [];
|
|
|
|
this.init();
|
|
}
|
|
|
|
async init() {
|
|
await this.loadSupportedTables();
|
|
await this.loadImportStatus();
|
|
this.setupEventListeners();
|
|
}
|
|
|
|
async loadSupportedTables() {
|
|
try {
|
|
console.log('Loading supported tables...');
|
|
const response = await window.http.wrappedFetch('/api/admin/import/tables');
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
this.supportedTables = data.tables || [];
|
|
console.log('Supported tables loaded:', this.supportedTables);
|
|
} else {
|
|
console.error('Failed to load supported tables, status:', response.status);
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to load supported tables:', error);
|
|
window.alerts.error('Failed to load supported tables');
|
|
}
|
|
}
|
|
|
|
setupEventListeners() {
|
|
// Single import form
|
|
document.getElementById('importForm').addEventListener('submit', (e) => {
|
|
e.preventDefault();
|
|
this.handleSingleImport();
|
|
});
|
|
|
|
// Validate button
|
|
document.getElementById('validateBtn').addEventListener('click', () => {
|
|
this.validateHeaders();
|
|
});
|
|
|
|
// Table selection change
|
|
document.getElementById('tableSelect').addEventListener('change', (e) => {
|
|
this.onTableChange(e.target.value);
|
|
});
|
|
|
|
// Bulk file upload
|
|
document.getElementById('bulkFiles').addEventListener('change', (e) => {
|
|
this.handleBulkFileSelection(e);
|
|
});
|
|
|
|
document.getElementById('autoMapBtn').addEventListener('click', () => {
|
|
this.autoMapTables();
|
|
});
|
|
|
|
document.getElementById('clearAllBtn').addEventListener('click', () => {
|
|
this.clearAllFiles();
|
|
});
|
|
|
|
document.getElementById('bulkUploadBtn').addEventListener('click', () => {
|
|
this.handleBulkUpload();
|
|
});
|
|
|
|
// Status refresh button
|
|
document.getElementById('refreshStatusBtn').addEventListener('click', () => {
|
|
this.loadImportStatus();
|
|
});
|
|
}
|
|
|
|
async onTableChange(tableName) {
|
|
const schemaInfo = document.getElementById('schemaInfo');
|
|
const schemaDetails = document.getElementById('schemaDetails');
|
|
|
|
if (!tableName) {
|
|
schemaInfo.classList.add('hidden');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
console.log('Loading schema for table:', tableName);
|
|
const response = await window.http.wrappedFetch(`/api/admin/import/tables/${tableName}/schema`);
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
const schema = data.schema;
|
|
console.log('Schema loaded for', tableName, ':', schema);
|
|
|
|
let html = '<div class="grid grid-cols-1 md:grid-cols-2 gap-4">';
|
|
html += '<div><h4 class="font-semibold mb-2">Required Fields:</h4>';
|
|
html += '<ul class="list-disc list-inside space-y-1">';
|
|
schema.required_fields.forEach(field => {
|
|
html += `<li><code class="bg-blue-100 px-1 rounded">${field}</code></li>`;
|
|
});
|
|
html += '</ul></div>';
|
|
|
|
html += '<div><h4 class="font-semibold mb-2">All Available Fields:</h4>';
|
|
html += '<div class="max-h-32 overflow-y-auto">';
|
|
html += '<div class="grid grid-cols-2 gap-1 text-xs">';
|
|
Object.keys(schema.field_mapping).forEach(field => {
|
|
html += `<code class="bg-gray-100 px-1 rounded">${field}</code>`;
|
|
});
|
|
html += '</div></div></div></div>';
|
|
|
|
schemaDetails.innerHTML = html;
|
|
schemaInfo.classList.remove('hidden');
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to load schema:', error);
|
|
}
|
|
}
|
|
|
|
async validateHeaders() {
|
|
const tableSelect = document.getElementById('tableSelect');
|
|
const fileInput = document.getElementById('csvFile');
|
|
|
|
console.log('Starting header validation...');
|
|
|
|
if (!tableSelect.value) {
|
|
console.warn('No table selected for validation');
|
|
window.alerts.error('Please select a table type');
|
|
return;
|
|
}
|
|
|
|
if (!fileInput.files[0]) {
|
|
console.warn('No file selected for validation');
|
|
window.alerts.error('Please select a CSV file');
|
|
return;
|
|
}
|
|
|
|
console.log('Validating headers for table:', tableSelect.value, 'file:', fileInput.files[0].name);
|
|
|
|
const formData = new FormData();
|
|
formData.append('table_name', tableSelect.value);
|
|
formData.append('file', fileInput.files[0]);
|
|
|
|
try {
|
|
const response = await window.http.wrappedFetch('/api/admin/import/validate', {
|
|
method: 'POST',
|
|
body: formData
|
|
});
|
|
|
|
console.log('Validation response status:', response.status);
|
|
|
|
if (response.ok) {
|
|
const result = await response.json();
|
|
console.log('Validation result:', result);
|
|
if (result.success) {
|
|
window.alerts.success('CSV headers validated successfully!');
|
|
} else {
|
|
const errors = result.validation_result.errors.join('\\n');
|
|
console.error('Validation errors:', result.validation_result.errors);
|
|
window.alerts.error(`Validation failed:\\n${errors}`);
|
|
}
|
|
} else {
|
|
const error = await response.json();
|
|
console.error('Validation failed with error:', error);
|
|
window.alerts.error(`Validation failed: ${error.detail}`);
|
|
}
|
|
} catch (error) {
|
|
console.error('Validation error:', error);
|
|
window.alerts.error('Failed to validate CSV headers');
|
|
}
|
|
}
|
|
|
|
async handleSingleImport() {
|
|
const tableSelect = document.getElementById('tableSelect');
|
|
const fileInput = document.getElementById('csvFile');
|
|
|
|
console.log('Starting single import...');
|
|
|
|
if (!tableSelect.value) {
|
|
console.warn('No table selected for import');
|
|
window.alerts.error('Please select a table type');
|
|
return;
|
|
}
|
|
|
|
if (!fileInput.files[0]) {
|
|
console.warn('No file selected for import');
|
|
window.alerts.error('Please select a CSV file');
|
|
return;
|
|
}
|
|
|
|
console.log('Importing to table:', tableSelect.value, 'file:', fileInput.files[0].name);
|
|
|
|
const formData = new FormData();
|
|
formData.append('table_name', tableSelect.value);
|
|
formData.append('file', fileInput.files[0]);
|
|
|
|
// Show progress
|
|
this.showProgress();
|
|
|
|
try {
|
|
const response = await window.http.wrappedFetch('/api/admin/import/csv', {
|
|
method: 'POST',
|
|
body: formData
|
|
});
|
|
|
|
console.log('Import response status:', response.status);
|
|
|
|
if (response.ok) {
|
|
const result = await response.json();
|
|
console.log('Import started successfully:', result);
|
|
this.currentImportId = result.import_id;
|
|
this.updateProgress(`Import started for ${result.table_name} (ID: ${result.import_id})`, 'info');
|
|
this.startPolling();
|
|
} else {
|
|
const error = await response.json();
|
|
console.error('Import failed:', error);
|
|
this.updateProgress(`Import failed: ${error.detail}`, 'error');
|
|
}
|
|
} catch (error) {
|
|
console.error('Import error:', error);
|
|
this.updateProgress('Import failed: Network error', 'error');
|
|
}
|
|
}
|
|
|
|
|
|
showProgress() {
|
|
document.getElementById('importProgress').classList.remove('hidden');
|
|
document.getElementById('importResults').classList.add('hidden');
|
|
}
|
|
|
|
updateProgress(message, type = 'info') {
|
|
const progressDetails = document.getElementById('progressDetails');
|
|
const timestamp = new Date().toLocaleTimeString();
|
|
|
|
let colorClass = 'text-blue-600';
|
|
if (type === 'error') colorClass = 'text-red-600';
|
|
if (type === 'success') colorClass = 'text-green-600';
|
|
if (type === 'warning') colorClass = 'text-yellow-600';
|
|
|
|
progressDetails.innerHTML += `
|
|
<div class="flex items-center space-x-2 mb-2">
|
|
<span class="text-gray-500 text-sm">${timestamp}</span>
|
|
<span class="${colorClass}">${message}</span>
|
|
</div>
|
|
`;
|
|
|
|
// Scroll to bottom
|
|
progressDetails.scrollTop = progressDetails.scrollHeight;
|
|
}
|
|
|
|
startPolling() {
|
|
if (this.pollInterval) {
|
|
clearInterval(this.pollInterval);
|
|
}
|
|
|
|
this.pollInterval = setInterval(async () => {
|
|
await this.checkImportStatus();
|
|
}, 2000); // Poll every 2 seconds
|
|
}
|
|
|
|
startBatchPolling() {
|
|
if (this.pollInterval) {
|
|
clearInterval(this.pollInterval);
|
|
}
|
|
|
|
this.pollInterval = setInterval(async () => {
|
|
await this.checkBatchStatus();
|
|
}, 2000); // Poll every 2 seconds
|
|
}
|
|
|
|
async checkImportStatus() {
|
|
if (!this.currentImportId) return;
|
|
|
|
try {
|
|
const response = await window.http.wrappedFetch(`/api/admin/import/status/${this.currentImportId}`);
|
|
if (response.ok) {
|
|
const status = await response.json();
|
|
|
|
if (status.status === 'COMPLETED') {
|
|
clearInterval(this.pollInterval);
|
|
this.updateProgress('Import completed successfully!', 'success');
|
|
this.showResults(status.result);
|
|
// Refresh status after successful import
|
|
setTimeout(() => {
|
|
this.loadImportStatus();
|
|
}, 1000);
|
|
} else if (status.status === 'FAILED') {
|
|
clearInterval(this.pollInterval);
|
|
this.updateProgress(`Import failed: ${status.error || 'Unknown error'}`, 'error');
|
|
if (status.result) {
|
|
this.showResults(status.result);
|
|
}
|
|
} else {
|
|
this.updateProgress(`Import status: ${status.status}`, 'info');
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Status check error:', error);
|
|
}
|
|
}
|
|
|
|
async checkBatchStatus() {
|
|
if (!this.currentImportIds || !Array.isArray(this.currentImportIds)) return;
|
|
|
|
let allCompleted = true;
|
|
let anyFailed = false;
|
|
|
|
for (const importId of this.currentImportIds) {
|
|
try {
|
|
const response = await window.http.wrappedFetch(`/api/admin/import/status/${importId}`);
|
|
if (response.ok) {
|
|
const status = await response.json();
|
|
|
|
if (status.status === 'PROCESSING') {
|
|
allCompleted = false;
|
|
} else if (status.status === 'FAILED') {
|
|
anyFailed = true;
|
|
this.updateProgress(`${status.table_name} import failed: ${status.error || 'Unknown error'}`, 'error');
|
|
} else if (status.status === 'COMPLETED') {
|
|
this.updateProgress(`${status.table_name} import completed`, 'success');
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Batch status check error:', error);
|
|
}
|
|
}
|
|
|
|
if (allCompleted) {
|
|
clearInterval(this.pollInterval);
|
|
const message = anyFailed ? 'Batch import completed with some failures' : 'Batch import completed successfully!';
|
|
const type = anyFailed ? 'warning' : 'success';
|
|
this.updateProgress(message, type);
|
|
|
|
// Refresh the import status after batch completion
|
|
setTimeout(() => {
|
|
this.loadImportStatus();
|
|
}, 1000);
|
|
}
|
|
}
|
|
|
|
showResults(result) {
|
|
const resultsContent = document.getElementById('resultsContent');
|
|
const resultsDiv = document.getElementById('importResults');
|
|
|
|
let html = '<div class="space-y-4">';
|
|
|
|
// Summary
|
|
html += `
|
|
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
|
|
<div class="bg-blue-50 p-3 rounded">
|
|
<div class="text-2xl font-bold text-blue-600">${result.total_rows}</div>
|
|
<div class="text-sm text-blue-800">Total Rows</div>
|
|
</div>
|
|
<div class="bg-green-50 p-3 rounded">
|
|
<div class="text-2xl font-bold text-green-600">${result.imported_rows}</div>
|
|
<div class="text-sm text-green-800">Imported</div>
|
|
</div>
|
|
<div class="bg-yellow-50 p-3 rounded">
|
|
<div class="text-2xl font-bold text-yellow-600">${result.skipped_rows}</div>
|
|
<div class="text-sm text-yellow-800">Skipped</div>
|
|
</div>
|
|
<div class="bg-red-50 p-3 rounded">
|
|
<div class="text-2xl font-bold text-red-600">${result.error_rows}</div>
|
|
<div class="text-sm text-red-800">Errors</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
// Errors
|
|
if (result.errors && result.errors.length > 0) {
|
|
html += '<div class="bg-red-50 border border-red-200 rounded p-4">';
|
|
html += '<h4 class="font-semibold text-red-800 mb-2">Errors:</h4>';
|
|
html += '<div class="text-sm text-red-700 space-y-1 max-h-40 overflow-y-auto">';
|
|
result.errors.forEach(error => {
|
|
html += `<div>${this.escapeHtml(error)}</div>`;
|
|
});
|
|
html += '</div></div>';
|
|
}
|
|
|
|
// Warnings
|
|
if (result.warnings && result.warnings.length > 0) {
|
|
html += '<div class="bg-yellow-50 border border-yellow-200 rounded p-4">';
|
|
html += '<h4 class="font-semibold text-yellow-800 mb-2">Warnings:</h4>';
|
|
html += '<div class="text-sm text-yellow-700 space-y-1 max-h-40 overflow-y-auto">';
|
|
result.warnings.forEach(warning => {
|
|
html += `<div>${this.escapeHtml(warning)}</div>`;
|
|
});
|
|
html += '</div></div>';
|
|
}
|
|
|
|
html += '</div>';
|
|
|
|
resultsContent.innerHTML = html;
|
|
resultsDiv.classList.remove('hidden');
|
|
}
|
|
|
|
handleBulkFileSelection(event) {
|
|
const files = Array.from(event.target.files);
|
|
console.log('Selected files:', files);
|
|
|
|
this.bulkFiles = files.map(file => {
|
|
const suggestion = this.suggestTableType(file.name);
|
|
return {
|
|
file: file,
|
|
name: file.name,
|
|
size: file.size,
|
|
suggested_table: suggestion || 'skip' // Default to skip for unknown files
|
|
};
|
|
});
|
|
|
|
this.renderBulkFiles();
|
|
this.updateBulkControls();
|
|
}
|
|
|
|
suggestTableType(filename) {
|
|
const name = filename.toUpperCase().replace('.CSV', '');
|
|
const mapping = {
|
|
// Core supported tables
|
|
'ROLODEX': 'rolodex',
|
|
'ROLEX_V': 'rolodex', // ROLEX_V variant
|
|
'PHONE': 'phone',
|
|
'FILES': 'files',
|
|
'FILES_R': 'files',
|
|
'FILES_V': 'files',
|
|
'LEDGER': 'ledger',
|
|
'QDROS': 'qdros',
|
|
// Lookup and reference tables
|
|
'GRUPLKUP': 'gruplkup',
|
|
'PLANINFO': 'planinfo',
|
|
'RVARLKUP': 'rvarlkup',
|
|
'FVARLKUP': 'fvarlkup',
|
|
'TRNSLKUP': 'trnslkup',
|
|
'EMPLOYEE': 'employee',
|
|
'FILETYPE': 'filetype',
|
|
'TRNSTYPE': 'trnstype',
|
|
'TRNSACTN': 'trnsactn',
|
|
'FILENOTS': 'filenots',
|
|
'SETUP': 'setup',
|
|
'PENSIONS': 'pensions',
|
|
'PAYMENTS': 'payments',
|
|
'DEPOSITS': 'deposits',
|
|
// Form tables
|
|
'NUMBERAL': 'numberal',
|
|
'INX_LKUP': 'inx_lkup',
|
|
'FORM_LST': 'form_lst',
|
|
'FORM_INX': 'form_inx',
|
|
'LIFETABL': 'lifetabl',
|
|
// Pension tables
|
|
'MARRIAGE': 'marriage',
|
|
'DEATH': 'death',
|
|
'SEPARATE': 'separate',
|
|
'SCHEDULE': 'schedule'
|
|
};
|
|
return mapping[name] || '';
|
|
}
|
|
|
|
renderBulkFiles() {
|
|
const filesDisplay = document.getElementById('bulkFilesDisplay');
|
|
const filesList = document.getElementById('bulkFilesList');
|
|
const fileCount = document.getElementById('bulkFileCount');
|
|
|
|
fileCount.textContent = this.bulkFiles.length;
|
|
|
|
if (this.bulkFiles.length === 0) {
|
|
filesDisplay.classList.add('hidden');
|
|
return;
|
|
}
|
|
|
|
filesDisplay.classList.remove('hidden');
|
|
|
|
let html = '';
|
|
this.bulkFiles.forEach((fileObj, index) => {
|
|
const sizeKB = Math.round(fileObj.size / 1024);
|
|
|
|
html += `
|
|
<div class="flex items-center justify-between p-3 bg-white dark:bg-neutral-700 rounded-lg border border-neutral-200 dark:border-neutral-600">
|
|
<div class="flex items-center space-x-3">
|
|
<i class="fa-solid fa-file-csv text-primary-600"></i>
|
|
<div>
|
|
<div class="font-medium text-neutral-900 dark:text-neutral-100">
|
|
${fileObj.name}
|
|
</div>
|
|
<p class="text-sm text-neutral-500 dark:text-neutral-400">${sizeKB} KB</p>
|
|
</div>
|
|
</div>
|
|
<div class="flex items-center space-x-2">
|
|
<select onchange="importManager.updateBulkFileTableMapping(${index}, this.value)"
|
|
class="px-3 py-1 border border-neutral-300 dark:border-neutral-600 rounded-md text-sm bg-white dark:bg-neutral-700 text-neutral-900 dark:text-neutral-100">
|
|
<option value="">-- Select Table --</option>
|
|
<optgroup label="Core Tables">
|
|
<option value="rolodex" ${fileObj.suggested_table === 'rolodex' ? 'selected' : ''}>ROLODEX (Contacts)</option>
|
|
<option value="phone" ${fileObj.suggested_table === 'phone' ? 'selected' : ''}>PHONE (Phone Numbers)</option>
|
|
<option value="files" ${fileObj.suggested_table === 'files' ? 'selected' : ''}>FILES (Case Files)</option>
|
|
<option value="ledger" ${fileObj.suggested_table === 'ledger' ? 'selected' : ''}>LEDGER (Financial)</option>
|
|
<option value="qdros" ${fileObj.suggested_table === 'qdros' ? 'selected' : ''}>QDROS (Documents)</option>
|
|
</optgroup>
|
|
<optgroup label="Lookup Tables">
|
|
<option value="gruplkup" ${fileObj.suggested_table === 'gruplkup' ? 'selected' : ''}>GRUPLKUP</option>
|
|
<option value="employee" ${fileObj.suggested_table === 'employee' ? 'selected' : ''}>EMPLOYEE</option>
|
|
<option value="filetype" ${fileObj.suggested_table === 'filetype' ? 'selected' : ''}>FILETYPE</option>
|
|
<option value="trnstype" ${fileObj.suggested_table === 'trnstype' ? 'selected' : ''}>TRNSTYPE</option>
|
|
<option value="trnslkup" ${fileObj.suggested_table === 'trnslkup' ? 'selected' : ''}>TRNSLKUP</option>
|
|
<option value="rvarlkup" ${fileObj.suggested_table === 'rvarlkup' ? 'selected' : ''}>RVARLKUP</option>
|
|
<option value="fvarlkup" ${fileObj.suggested_table === 'fvarlkup' ? 'selected' : ''}>FVARLKUP</option>
|
|
</optgroup>
|
|
<optgroup label="Financial Tables">
|
|
<option value="payments" ${fileObj.suggested_table === 'payments' ? 'selected' : ''}>PAYMENTS</option>
|
|
<option value="deposits" ${fileObj.suggested_table === 'deposits' ? 'selected' : ''}>DEPOSITS</option>
|
|
<option value="trnsactn" ${fileObj.suggested_table === 'trnsactn' ? 'selected' : ''}>TRNSACTN</option>
|
|
</optgroup>
|
|
<optgroup label="Forms & Documents">
|
|
<option value="numberal" ${fileObj.suggested_table === 'numberal' ? 'selected' : ''}>NUMBERAL</option>
|
|
<option value="inx_lkup" ${fileObj.suggested_table === 'inx_lkup' ? 'selected' : ''}>INX_LKUP</option>
|
|
<option value="form_lst" ${fileObj.suggested_table === 'form_lst' ? 'selected' : ''}>FORM_LST</option>
|
|
<option value="form_inx" ${fileObj.suggested_table === 'form_inx' ? 'selected' : ''}>FORM_INX</option>
|
|
<option value="lifetabl" ${fileObj.suggested_table === 'lifetabl' ? 'selected' : ''}>LIFETABL</option>
|
|
</optgroup>
|
|
<optgroup label="Pension Tables">
|
|
<option value="pensions" ${fileObj.suggested_table === 'pensions' ? 'selected' : ''}>PENSIONS</option>
|
|
<option value="marriage" ${fileObj.suggested_table === 'marriage' ? 'selected' : ''}>MARRIAGE</option>
|
|
<option value="death" ${fileObj.suggested_table === 'death' ? 'selected' : ''}>DEATH</option>
|
|
<option value="separate" ${fileObj.suggested_table === 'separate' ? 'selected' : ''}>SEPARATE</option>
|
|
<option value="schedule" ${fileObj.suggested_table === 'schedule' ? 'selected' : ''}>SCHEDULE</option>
|
|
</optgroup>
|
|
<optgroup label="Configuration">
|
|
<option value="setup" ${fileObj.suggested_table === 'setup' ? 'selected' : ''}>SETUP</option>
|
|
<option value="planinfo" ${fileObj.suggested_table === 'planinfo' ? 'selected' : ''}>PLANINFO</option>
|
|
<option value="filenots" ${fileObj.suggested_table === 'filenots' ? 'selected' : ''}>FILENOTS</option>
|
|
</optgroup>
|
|
<optgroup label="Other">
|
|
<option value="skip" ${fileObj.suggested_table === 'skip' ? 'selected' : ''}>⚠️ SKIP (Don't Import)</option>
|
|
</optgroup>
|
|
</select>
|
|
<button onclick="importManager.removeBulkFile(${index})"
|
|
class="px-2 py-1 bg-red-600 text-white rounded-md hover:bg-red-700 text-sm">
|
|
<i class="fa-solid fa-times"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
});
|
|
|
|
filesList.innerHTML = html;
|
|
}
|
|
|
|
updateBulkFileTableMapping(index, tableType) {
|
|
if (this.bulkFiles[index]) {
|
|
this.bulkFiles[index].suggested_table = tableType;
|
|
}
|
|
this.updateBulkControls();
|
|
}
|
|
|
|
removeBulkFile(index) {
|
|
this.bulkFiles.splice(index, 1);
|
|
this.renderBulkFiles();
|
|
this.updateBulkControls();
|
|
}
|
|
|
|
autoMapTables() {
|
|
this.bulkFiles.forEach(fileObj => {
|
|
if (!fileObj.suggested_table || fileObj.suggested_table === '') {
|
|
const suggestion = this.suggestTableType(fileObj.name);
|
|
// If no mapping found, default to 'skip' for unknown files
|
|
fileObj.suggested_table = suggestion || 'skip';
|
|
}
|
|
});
|
|
this.renderBulkFiles();
|
|
this.updateBulkControls();
|
|
}
|
|
|
|
clearAllFiles() {
|
|
this.bulkFiles = [];
|
|
document.getElementById('bulkFiles').value = '';
|
|
this.renderBulkFiles();
|
|
this.updateBulkControls();
|
|
}
|
|
|
|
updateBulkControls() {
|
|
const autoMapBtn = document.getElementById('autoMapBtn');
|
|
const clearAllBtn = document.getElementById('clearAllBtn');
|
|
const uploadBtn = document.getElementById('bulkUploadBtn');
|
|
|
|
const hasFiles = this.bulkFiles.length > 0;
|
|
const allMapped = this.bulkFiles.every(f => f.suggested_table && f.suggested_table !== '');
|
|
|
|
autoMapBtn.disabled = !hasFiles;
|
|
clearAllBtn.disabled = !hasFiles;
|
|
uploadBtn.disabled = !hasFiles || !allMapped;
|
|
|
|
if (hasFiles) {
|
|
const mappedCount = this.bulkFiles.filter(f => f.suggested_table && f.suggested_table !== '').length;
|
|
const importCount = this.bulkFiles.filter(f => f.suggested_table && f.suggested_table !== 'skip').length;
|
|
uploadBtn.innerHTML = `
|
|
<i class="fa-solid fa-cloud-upload"></i>
|
|
<span>Upload & Import ${importCount} Files (${mappedCount} mapped)</span>
|
|
`;
|
|
} else {
|
|
uploadBtn.innerHTML = `
|
|
<i class="fa-solid fa-cloud-upload"></i>
|
|
<span>Upload & Import All</span>
|
|
`;
|
|
}
|
|
}
|
|
|
|
async handleBulkUpload() {
|
|
if (this.bulkFiles.length === 0) {
|
|
window.alerts.error('Please select files to upload');
|
|
return;
|
|
}
|
|
|
|
// Validate all files have table mappings
|
|
const unmappedFiles = this.bulkFiles.filter(f => !f.suggested_table || f.suggested_table === '');
|
|
if (unmappedFiles.length > 0) {
|
|
window.alerts.error(`Please select table types for: ${unmappedFiles.map(f => f.name).join(', ')}`);
|
|
return;
|
|
}
|
|
|
|
// Filter out files marked as 'skip'
|
|
const filesToImport = this.bulkFiles.filter(f => f.suggested_table !== 'skip');
|
|
|
|
if (filesToImport.length === 0) {
|
|
window.alerts.error('No files selected for import (all marked as skip)');
|
|
return;
|
|
}
|
|
|
|
console.log('Starting bulk upload:', filesToImport);
|
|
|
|
// Show progress
|
|
this.showProgress();
|
|
|
|
try {
|
|
// Use the existing batch endpoint that handles FormData
|
|
const formData = new FormData();
|
|
|
|
// Add only files that are not marked as 'skip'
|
|
filesToImport.forEach(fileObj => {
|
|
formData.append('files', fileObj.file);
|
|
});
|
|
|
|
// Add corresponding table names
|
|
filesToImport.forEach(fileObj => {
|
|
formData.append('table_names', fileObj.suggested_table);
|
|
});
|
|
|
|
const response = await window.http.wrappedFetch('/api/admin/import/batch', {
|
|
method: 'POST',
|
|
body: formData
|
|
});
|
|
|
|
if (response.ok) {
|
|
const result = await response.json();
|
|
this.currentImportIds = result.import_ids;
|
|
this.updateProgress(`Bulk upload started for ${result.total_files} files`, 'info');
|
|
this.startBatchPolling();
|
|
} else {
|
|
const error = await response.json();
|
|
this.updateProgress(`Bulk upload failed: ${error.detail}`, 'error');
|
|
}
|
|
} catch (error) {
|
|
console.error('Bulk upload error:', error);
|
|
this.updateProgress('Bulk upload failed: Network error', 'error');
|
|
}
|
|
}
|
|
|
|
async loadImportStatus() {
|
|
try {
|
|
console.log('Loading import status...');
|
|
const response = await window.http.wrappedFetch('/api/admin/import/status');
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
this.displayImportStatus(data);
|
|
console.log('Import status loaded:', data);
|
|
} else {
|
|
console.error('Failed to load import status, status:', response.status);
|
|
window.alerts.error('Failed to load import status');
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to load import status:', error);
|
|
window.alerts.error('Failed to load import status');
|
|
}
|
|
}
|
|
|
|
displayImportStatus(data) {
|
|
const summary = data.summary;
|
|
const categories = data.categories;
|
|
|
|
// Update summary stats
|
|
document.getElementById('totalTables').textContent = summary.total_tables;
|
|
document.getElementById('importedTables').textContent = summary.imported_tables;
|
|
document.getElementById('emptyTables').textContent = summary.empty_tables;
|
|
document.getElementById('missingTables').textContent = summary.missing_tables;
|
|
document.getElementById('totalRows').textContent = summary.total_rows.toLocaleString();
|
|
document.getElementById('completionPercentage').textContent = `${summary.completion_percentage}%`;
|
|
|
|
// Update progress bar
|
|
const progressBar = document.getElementById('progressBar');
|
|
progressBar.style.width = `${summary.completion_percentage}%`;
|
|
|
|
// Display categories
|
|
const categoryContainer = document.getElementById('statusByCategory');
|
|
categoryContainer.innerHTML = '';
|
|
|
|
Object.keys(categories).forEach(categoryName => {
|
|
const tables = categories[categoryName];
|
|
const categoryDiv = document.createElement('div');
|
|
categoryDiv.className = 'bg-neutral-50 dark:bg-neutral-900 rounded-lg p-4';
|
|
|
|
const importedCount = tables.filter(t => t.imported).length;
|
|
const totalCount = tables.length;
|
|
|
|
categoryDiv.innerHTML = `
|
|
<h3 class="font-semibold text-neutral-900 dark:text-neutral-100 mb-3 flex items-center justify-between">
|
|
<span>${categoryName} Tables</span>
|
|
<span class="text-sm font-normal text-neutral-600 dark:text-neutral-400">${importedCount}/${totalCount} imported</span>
|
|
</h3>
|
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
|
|
${tables.map(table => `
|
|
<div class="flex items-center justify-between p-3 bg-white dark:bg-neutral-800 rounded-lg border border-neutral-200 dark:border-neutral-600">
|
|
<div class="flex items-center space-x-3">
|
|
<div class="flex-shrink-0">
|
|
${table.imported ?
|
|
'<i class="fa-solid fa-check-circle text-green-600"></i>' :
|
|
table.exists ?
|
|
'<i class="fa-solid fa-exclamation-circle text-yellow-600"></i>' :
|
|
'<i class="fa-solid fa-times-circle text-red-600"></i>'
|
|
}
|
|
</div>
|
|
<div>
|
|
<div class="font-medium text-neutral-900 dark:text-neutral-100">${table.display_name}</div>
|
|
<div class="text-sm text-neutral-500 dark:text-neutral-400">
|
|
${table.imported ?
|
|
`${table.row_count.toLocaleString()} rows` :
|
|
table.exists ?
|
|
'Empty table' :
|
|
'Not created'
|
|
}
|
|
</div>
|
|
<div class="text-xs text-neutral-400 dark:text-neutral-500">
|
|
Expected: ${table.expected_files.join(', ')}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`).join('')}
|
|
</div>
|
|
`;
|
|
|
|
categoryContainer.appendChild(categoryDiv);
|
|
});
|
|
}
|
|
|
|
escapeHtml(text) {
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
}
|
|
|
|
// Initialize when DOM is loaded
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
window.importManager = new ImportManager();
|
|
}); |