remove old import

This commit is contained in:
HotSwapp
2025-08-14 21:27:34 -05:00
parent bfc04a6909
commit 679ab4446a
17 changed files with 2016 additions and 557 deletions

View File

@@ -141,10 +141,6 @@
<i class="fa-solid fa-print"></i>
<span>Printers</span>
</button>
<button class="flex items-center gap-2 px-6 py-4 text-sm font-medium border-b-2 border-transparent hover:border-primary-300 text-neutral-600 dark:text-neutral-400 hover:text-primary-600 dark:hover:text-primary-400 hover:bg-neutral-50 dark:hover:bg-neutral-700/50 transition-all duration-200" id="import-tab" data-tab-target="#import" type="button" role="tab">
<i class="fa-solid fa-file-import"></i>
<span>Import</span>
</button>
<button class="flex items-center gap-2 px-6 py-4 text-sm font-medium border-b-2 border-transparent hover:border-primary-300 text-neutral-600 dark:text-neutral-400 hover:text-primary-600 dark:hover:text-primary-400 hover:bg-neutral-50 dark:hover:bg-neutral-700/50 transition-all duration-200" id="issues-tab" data-tab-target="#issues" type="button" role="tab">
<i class="fa-solid fa-bug"></i>
<span>Issues</span>
@@ -621,139 +617,6 @@
</div>
</div>
<!-- Data Import Tab -->
<div id="import" role="tabpanel" class="hidden">
<div class="flex flex-wrap -mx-4">
<div class="w-full px-4">
<h4 class="mb-4 text-xl font-semibold"><i class="fa-solid fa-upload"></i> Data Import Management</h4>
<!-- Import Status Panel -->
<div class="bg-white dark:bg-neutral-800 border border-neutral-200 dark:border-neutral-700 rounded-lg shadow mb-4">
<div class="px-4 py-3 border-b border-neutral-200 dark:border-neutral-700 flex items-center justify-between">
<h5 class="m-0 font-semibold"><i class="fa-solid fa-circle-info"></i> Current Database Status</h5>
<button class="px-3 py-1.5 border border-info-600 text-info-700 dark:text-info-200 rounded text-sm hover:bg-info-50 dark:hover:bg-info-900/20" onclick="loadImportStatus()">
<i class="fa-solid fa-rotate-right"></i> Refresh
</button>
</div>
<div class="p-4">
<div id="importStatus">
<div class="text-center">
<div class="inline-block w-6 h-6 border-2 border-neutral-300 border-t-primary-600 rounded-full animate-spin"></div>
<p class="mt-2">Loading import status...</p>
</div>
</div>
</div>
</div>
<!-- CSV File Upload Panel -->
<div class="bg-white dark:bg-neutral-800 border border-neutral-200 dark:border-neutral-700 rounded-lg shadow mb-4">
<div class="px-4 py-3 border-b border-neutral-200 dark:border-neutral-700">
<h5 class="m-0 font-semibold"><i class="fa-regular fa-file-arrow-up"></i> Upload CSV Files</h5>
</div>
<div class="p-4">
<form id="adminImportForm" enctype="multipart/form-data">
<div class="flex flex-wrap -mx-3">
<div class="w-full md:w-1/3 px-3 mb-3">
<label for="adminFileType" class="block text-sm font-medium mb-1">Data Type *</label>
<select class="w-full px-3 py-2 bg-white dark:bg-neutral-800 border border-neutral-300 dark:border-neutral-600 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent" id="adminFileType" name="fileType" required>
<option value="">Select data type...</option>
</select>
<div class="text-sm text-neutral-500 dark:text-neutral-400 mt-1" id="adminFileTypeDescription"></div>
</div>
<div class="w-full md:w-1/2 px-3 mb-3">
<label for="adminCsvFile" class="block text-sm font-medium mb-1">CSV File *</label>
<input type="file" class="w-full px-3 py-2 bg-white dark:bg-neutral-800 border border-neutral-300 dark:border-neutral-600 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent" id="adminCsvFile" name="csvFile" accept=".csv" required>
<div class="text-sm text-neutral-500 dark:text-neutral-400 mt-1">Select the CSV file to import</div>
</div>
<div class="w-full md:w-1/6 px-3 mb-3">
<label class="block text-sm font-medium mb-1">&nbsp;</label>
<label class="inline-flex items-center gap-2">
<input class="rounded border-neutral-300 text-primary-600 focus:ring-primary-500" type="checkbox" id="adminReplaceExisting" name="replaceExisting">
<span>Replace existing data</span>
</label>
</div>
</div>
<div class="mt-3">
<div class="flex flex-wrap gap-2">
<button type="button" class="px-4 py-2 border border-neutral-300 dark:border-neutral-600 rounded hover:bg-neutral-50 dark:hover:bg-neutral-700" onclick="validateAdminFile()">
<i class="fa-regular fa-circle-check"></i> Validate File
</button>
<button type="submit" class="px-4 py-2 bg-primary-600 hover:bg-primary-700 text-white rounded" id="adminImportBtn">
<i class="fa-solid fa-upload"></i> Import Data
</button>
</div>
</div>
</form>
</div>
</div>
<!-- Validation Results Panel -->
<div class="bg-white dark:bg-neutral-800 border border-neutral-200 dark:border-neutral-700 rounded-lg shadow mb-4 hidden" id="adminValidationPanel">
<div class="px-4 py-3 border-b border-neutral-200 dark:border-neutral-700">
<h5 class="m-0 font-semibold"><i class="fa-solid fa-clipboard-check"></i> File Validation Results</h5>
</div>
<div class="p-4" id="adminValidationResults">
<!-- Validation results will be shown here -->
</div>
</div>
<!-- Import Progress Panel -->
<div class="bg-white dark:bg-neutral-800 border border-neutral-200 dark:border-neutral-700 rounded-lg shadow mb-4 hidden" id="adminProgressPanel">
<div class="px-4 py-3 border-b border-neutral-200 dark:border-neutral-700">
<h5 class="m-0 font-semibold"><i class="fa-solid fa-hourglass-half"></i> Import Progress</h5>
</div>
<div class="p-4">
<div class="w-full bg-neutral-100 dark:bg-neutral-700 rounded h-3 overflow-hidden mb-3">
<div class="h-3 bg-primary-600 transition-all" style="width: 0%" id="adminProgressBar"></div>
</div>
<div id="adminProgressStatus">Ready to import...</div>
</div>
</div>
<!-- Import Results Panel -->
<div class="bg-white dark:bg-neutral-800 border border-neutral-200 dark:border-neutral-700 rounded-lg shadow mb-4 hidden" id="adminResultsPanel">
<div class="px-4 py-3 border-b border-neutral-200 dark:border-neutral-700">
<h5 class="m-0 font-semibold"><i class="fa-solid fa-circle-check"></i> Import Results</h5>
</div>
<div class="p-4" id="adminImportResults">
<!-- Import results will be shown here -->
</div>
</div>
<!-- Data Management Panel -->
<div class="bg-white dark:bg-neutral-800 border border-neutral-200 dark:border-neutral-700 rounded-lg shadow">
<div class="px-4 py-3 border-b border-neutral-200 dark:border-neutral-700">
<h5 class="m-0 font-semibold"><i class="fa-solid fa-database"></i> Data Management</h5>
</div>
<div class="p-4">
<div class="flex flex-wrap -mx-4">
<div class="w-full md:w-1/2 px-4">
<h6 class="font-semibold">Clear Table Data</h6>
<p class="text-neutral-500 dark:text-neutral-400 text-sm">Remove all records from a specific table (cannot be undone)</p>
<div class="flex items-center gap-2">
<select class="w-full px-3 py-2 bg-white dark:bg-neutral-800 border border-neutral-300 dark:border-neutral-600 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent" id="adminClearTableType">
<option value="">Select table to clear...</option>
</select>
<button class="px-3 py-2 bg-danger-600 hover:bg-danger-700 text-white rounded" onclick="clearAdminTable()">
<i class="fa-solid fa-trash"></i> Clear Table
</button>
</div>
</div>
<div class="w-full md:w-1/2 px-4 mt-4 md:mt-0">
<h6 class="font-semibold">Quick Actions</h6>
<div class="grid gap-2">
<button class="px-3 py-2 border border-info-600 text-info-700 dark:text-info-200 rounded hover:bg-info-50 dark:hover:bg-info-900/20" onclick="viewImportLogs()">
<i class="fa-regular fa-file-lines"></i> View Import Logs
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Backup Tab -->
<div id="backup" role="tabpanel" class="hidden">
@@ -1302,9 +1165,6 @@ function onTabShown(tabName) {
if (tabName === 'issues') {
loadIssues();
loadIssueStats();
} else if (tabName === 'import') {
loadAvailableImportFiles();
loadImportStatus();
} else if (tabName === 'backup') {
loadBackups();
} else if (tabName === 'users') {
@@ -2533,380 +2393,6 @@ async function addResponse() {
}
}
// Import Management Functions
let availableImportFiles = {};
let importInProgress = false;
async function loadAvailableImportFiles() {
try {
const response = await window.http.wrappedFetch('/api/import/available-files');
if (!response.ok) throw new Error('Failed to load available files');
const data = await response.json();
availableImportFiles = data;
// Populate file type dropdowns
const fileTypeSelect = document.getElementById('adminFileType');
const clearTableSelect = document.getElementById('adminClearTableType');
fileTypeSelect.innerHTML = '<option value="">Select data type...</option>';
clearTableSelect.innerHTML = '<option value="">Select table to clear...</option>';
data.available_files.forEach(fileType => {
const description = data.descriptions[fileType] || fileType;
const option1 = document.createElement('option');
option1.value = fileType;
option1.textContent = `${fileType} - ${description}`;
fileTypeSelect.appendChild(option1);
const option2 = document.createElement('option');
option2.value = fileType;
option2.textContent = `${fileType} - ${description}`;
clearTableSelect.appendChild(option2);
});
// Setup form listener
document.getElementById('adminImportForm').addEventListener('submit', handleAdminImport);
// File type change listener
document.getElementById('adminFileType').addEventListener('change', updateAdminFileTypeDescription);
} catch (error) {
console.error('Error loading available files:', error);
showAlert('Error loading available file types: ' + error.message, 'error');
}
}
async function loadImportStatus() {
try {
const response = await window.http.wrappedFetch('/api/import/status');
if (!response.ok) throw new Error('Failed to load import status');
const status = await response.json();
displayImportStatus(status);
} catch (error) {
console.error('Error loading import status:', error);
document.getElementById('importStatus').innerHTML =
`<div class="bg-danger-50 text-danger-800 border-l-4 border-danger-500 p-3 rounded">Error loading import status: ${error.message}</div>`;
}
}
function displayImportStatus(status) {
const container = document.getElementById('importStatus');
let html = '<div class="grid grid-cols-1 md:grid-cols-3 gap-2">';
let totalRecords = 0;
Object.entries(status).forEach(([fileType, info], index) => {
totalRecords += info.record_count || 0;
const statusClass = info.error ? 'danger' : (info.record_count > 0 ? 'success' : 'secondary');
const statusIcon = info.error ? 'triangle-exclamation' : (info.record_count > 0 ? 'circle-check' : 'circle');
// grid handles wrapping; no manual row breaks needed
html += `
<div>
<div class="border rounded-lg p-2 ${statusClass === 'danger' ? 'border-danger-300 dark:border-danger-700' : statusClass === 'success' ? 'border-success-300 dark:border-success-700' : 'border-neutral-200 dark:border-neutral-700'}">
<div class="p-0.5">
<div class="flex justify-between items-center">
<div>
<small class="font-semibold">${fileType}</small><br>
<small class="text-neutral-500 dark:text-neutral-400">${info.table_name}</small>
</div>
<div class="text-right">
<i class="fa-solid fa-${statusIcon} ${statusClass === 'danger' ? 'text-danger-600' : statusClass === 'success' ? 'text-success-600' : 'text-neutral-400'}"></i><br>
<small class="font-semibold">${info.record_count || 0}</small>
</div>
</div>
${info.error ? `<div class="text-danger-600 dark:text-danger-400 text-xs mt-1">${info.error}</div>` : ''}
</div>
</div>
</div>
`;
});
html += '</div>';
html += `<div class="mt-3 text-center">
<strong>Total Records: ${totalRecords.toLocaleString()}</strong>
</div>`;
container.innerHTML = html;
}
function updateAdminFileTypeDescription() {
const fileType = document.getElementById('adminFileType').value;
const description = availableImportFiles.descriptions && availableImportFiles.descriptions[fileType];
document.getElementById('adminFileTypeDescription').textContent = description || '';
}
async function validateAdminFile() {
const fileType = document.getElementById('adminFileType').value;
const fileInput = document.getElementById('adminCsvFile');
if (!fileType || !fileInput.files[0]) {
showAlert('Please select both file type and CSV file', 'error');
return;
}
const formData = new FormData();
formData.append('file', fileInput.files[0]);
try {
showAdminProgress(true, 'Validating file...');
const response = await window.http.wrappedFetch(`/api/import/validate/${fileType}`, {
method: 'POST',
body: formData
});
if (!response.ok) {
throw await window.http.toError(response, 'Validation failed');
}
const result = await response.json();
displayAdminValidationResults(result);
} catch (error) {
console.error('Validation error:', error);
const message = window.http && typeof window.http.formatAlert === 'function'
? window.http.formatAlert(error, 'Validation failed')
: 'Validation failed: ' + (error && error.message ? error.message : String(error));
showAlert(message, 'error');
} finally {
showAdminProgress(false);
}
}
function displayAdminValidationResults(result) {
const panel = document.getElementById('adminValidationPanel');
const container = document.getElementById('adminValidationResults');
let html = '';
// Overall status
const statusClass = result.valid ? 'success' : 'danger';
const statusIcon = result.valid ? 'circle-check' : 'triangle-exclamation';
html += `
<div class="${statusClass === 'success' ? 'bg-success-50 text-success-700 border-success-500' : 'bg-danger-50 text-danger-700 border-danger-500'} border-l-4 p-3 rounded">
<i class="fa-solid fa-${statusIcon}"></i>
File validation ${result.valid ? 'passed' : 'failed'}
</div>
`;
// Headers validation
html += '<h6>Column Headers</h6>';
if (result.headers.missing.length > 0) {
html += `<div class="bg-warning-50 text-warning-800 border-l-4 border-warning-500 p-3 rounded">
<strong>Missing columns:</strong> ${result.headers.missing.join(', ')}
</div>`;
}
if (result.headers.extra.length > 0) {
html += `<div class="bg-info-50 text-info-800 border-l-4 border-info-500 p-3 rounded">
<strong>Extra columns:</strong> ${result.headers.extra.join(', ')}
</div>`;
}
if (result.headers.missing.length === 0 && result.headers.extra.length === 0) {
html += '<div class="bg-success-50 text-success-800 border-l-4 border-success-500 p-3 rounded">All expected columns found</div>';
}
// Sample data
if (result.sample_data && result.sample_data.length > 0) {
html += '<h6>Sample Data (First 10 rows)</h6>';
html += '<div class="overflow-x-auto">';
html += '<table class="min-w-full text-sm">';
html += '<thead><tr>';
Object.keys(result.sample_data[0]).forEach(header => {
html += `<th class="px-2 py-1 text-left border-b border-neutral-200 dark:border-neutral-700">${header}</th>`;
});
html += '</tr></thead><tbody>';
result.sample_data.forEach(row => {
html += '<tr class="odd:bg-neutral-50 dark:odd:bg-neutral-800/50">';
Object.values(row).forEach(value => {
html += `<td class="px-2 py-1">${value || ''}</td>`;
});
html += '</tr>';
});
html += '</tbody></table></div>';
}
// Validation errors
if (result.validation_errors && result.validation_errors.length > 0) {
html += '<h6>Data Issues Found</h6>';
html += '<div class="bg-warning-50 text-warning-800 border-l-4 border-warning-500 p-3 rounded">';
result.validation_errors.forEach(error => {
html += `<div>Row ${error.row}, Field "${error.field}": ${error.error}</div>`;
});
if (result.total_errors > result.validation_errors.length) {
html += `<div class="mt-2"><strong>... and ${result.total_errors - result.validation_errors.length} more errors</strong></div>`;
}
html += '</div>';
}
container.innerHTML = html;
panel.style.display = 'block';
}
async function handleAdminImport(event) {
event.preventDefault();
if (importInProgress) {
showAlert('Import already in progress', 'error');
return;
}
const fileType = document.getElementById('adminFileType').value;
const fileInput = document.getElementById('adminCsvFile');
const replaceExisting = document.getElementById('adminReplaceExisting').checked;
if (!fileType || !fileInput.files[0]) {
showAlert('Please select both file type and CSV file', 'error');
return;
}
importInProgress = true;
const formData = new FormData();
formData.append('file', fileInput.files[0]);
formData.append('replace_existing', replaceExisting);
try {
showAdminProgress(true, 'Importing data...');
const response = await window.http.wrappedFetch(`/api/import/upload/${fileType}`, {
method: 'POST',
body: formData
});
if (!response.ok) {
throw await window.http.toError(response, 'Import failed');
}
const result = await response.json();
displayAdminImportResults(result);
// Refresh status after successful import
await loadImportStatus();
// Reset form
document.getElementById('adminImportForm').reset();
} catch (error) {
console.error('Import error:', error);
const message = window.http && typeof window.http.formatAlert === 'function'
? window.http.formatAlert(error, 'Import failed')
: 'Import failed: ' + (error && error.message ? error.message : String(error));
showAlert(message, 'error');
} finally {
importInProgress = false;
showAdminProgress(false);
}
}
function displayAdminImportResults(result) {
const panel = document.getElementById('adminResultsPanel');
const container = document.getElementById('adminImportResults');
const successClass = result.errors && result.errors.length > 0 ? 'warning' : 'success';
let html = `
<div class="${successClass === 'warning' ? 'bg-warning-50 text-warning-800 border-warning-500' : 'bg-success-50 text-success-800 border-success-500'} border-l-4 p-3 rounded">
<h6 class="font-semibold flex items-center gap-2"><i class="fa-regular fa-circle-check"></i> Import Completed</h6>
<p class="mb-0">
<strong>File Type:</strong> ${result.file_type}<br>
<strong>Records Imported:</strong> ${result.imported_count}<br>
<strong>Errors:</strong> ${result.total_errors || 0}
</p>
</div>
`;
if (result.errors && result.errors.length > 0) {
html += '<h6>Import Errors</h6>';
html += '<div class="bg-danger-50 text-danger-800 border-l-4 border-danger-500 p-3 rounded">';
result.errors.forEach(error => {
html += `<div><strong>Row ${error.row}:</strong> ${error.error}</div>`;
});
if (result.total_errors > result.errors.length) {
html += `<div class="mt-2"><strong>... and ${result.total_errors - result.errors.length} more errors</strong></div>`;
}
html += '</div>';
}
// Add summary for printers
if (result.file_type === 'PRINTERS.csv') {
html += `
<div class="mt-2 p-2 bg-neutral-50 dark:bg-neutral-800/50 rounded border border-neutral-200 dark:border-neutral-700 text-sm">
<strong>Printers:</strong> ${result.created_count || 0} created, ${result.updated_count || 0} updated
</div>
`;
// Auto-refresh printers tab list
try { loadPrinters(); } catch (_) {}
}
container.innerHTML = html;
panel.style.display = 'block';
}
function showAdminProgress(show, message = '') {
const panel = document.getElementById('adminProgressPanel');
const status = document.getElementById('adminProgressStatus');
const bar = document.getElementById('adminProgressBar');
if (show) {
status.textContent = message;
bar.style.width = '100%';
bar.textContent = 'Processing...';
panel.style.display = 'block';
} else {
panel.style.display = 'none';
}
}
async function clearAdminTable() {
const fileType = document.getElementById('adminClearTableType').value;
if (!fileType) {
showAlert('Please select a table to clear', 'error');
return;
}
if (!confirm(`Are you sure you want to clear all data from ${fileType}? This action cannot be undone.`)) {
return;
}
try {
const response = await window.http.wrappedFetch(`/api/import/clear/${fileType}`, {
method: 'DELETE'
});
if (!response.ok) {
throw await window.http.toError(response, 'Clear operation failed');
}
const result = await response.json();
showAlert(`Successfully cleared ${result.deleted_count} records from ${result.table_name}`, 'success');
// Refresh status
await loadImportStatus();
} catch (error) {
console.error('Clear table error:', error);
const message = window.http && typeof window.http.formatAlert === 'function'
? window.http.formatAlert(error, 'Clear operation failed')
: 'Clear operation failed: ' + (error && error.message ? error.message : String(error));
showAlert(message, 'error');
}
}
function viewImportLogs() {
showAlert('Import logs functionality coming soon', 'info');
}
function searchUsers() {
const searchTerm = document.getElementById('user-search').value.toLowerCase();

View File

@@ -804,7 +804,7 @@ function clearUploadFileNo() {
async function loadTemplates() {
try {
console.log('🔍 DEBUG: loadTemplates() called');
// Loading templates...
const search = document.getElementById('templateSearch').value;
const category = document.getElementById('categoryFilter').value;
@@ -814,17 +814,17 @@ async function loadTemplates() {
// Ensure we have auth token for this API call
const token = localStorage.getItem('auth_token');
console.log('🔍 DEBUG: Token exists:', !!token, 'Length:', token?.length);
// Token validation
if (!token) {
console.log('🔍 DEBUG: No token found, redirecting to login');
// No token found, redirecting to login
window.location.href = '/login';
return;
}
console.log('🔍 DEBUG: Making API call to:', url);
// Making API call
const response = await window.http.wrappedFetch(url);
console.log('🔍 DEBUG: Response status:', response.status);
// Processing response
if (!response.ok) {
const errorData = await response.json().catch(() => ({ detail: 'Unknown error' }));
@@ -832,10 +832,10 @@ async function loadTemplates() {
}
const templates = await response.json();
console.log('🔍 DEBUG: Templates loaded:', templates.length, 'items');
// Templates loaded successfully
displayTemplates(templates);
} catch (error) {
console.error('🔍 DEBUG: Error in loadTemplates:', error);
console.error('Error in loadTemplates:', error);
try { logClientError({ message: 'Error loading templates', action: 'loadTemplates', error }); } catch (_) {}
showAlert('Error loading templates: ' + (error && error.message ? error.message : 'Unknown error'), 'danger');
}