- Added 'Stored Filename' column to Upload Results table showing the actual filename used for storage - Added 'Select All' button for each import type section to quickly select/deselect all files - Improved JavaScript to handle select all/deselect all functionality with proper button state management - Enhanced UI to clearly distinguish between original and stored filenames
430 lines
21 KiB
HTML
430 lines
21 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}Admin Panel - Delphi Database{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="container-fluid">
|
|
<div class="row">
|
|
<div class="col-12">
|
|
<h1 class="mb-4">
|
|
<i class="bi bi-gear me-2"></i>Admin Panel
|
|
</h1>
|
|
|
|
<!-- Alert Messages -->
|
|
{% if error %}
|
|
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
|
<i class="bi bi-exclamation-triangle me-2"></i>{{ error }}
|
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% if show_upload_results %}
|
|
<div class="alert alert-info alert-dismissible fade show" role="alert">
|
|
<i class="bi bi-info-circle me-2"></i>
|
|
Files uploaded successfully. Review the results below and select files to import.
|
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% if show_import_results %}
|
|
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
|
<i class="bi bi-check-circle me-2"></i>
|
|
Import completed. Check the results below for details.
|
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Upload Section -->
|
|
<div class="card mb-4">
|
|
<div class="card-header bg-primary text-white">
|
|
<h5 class="mb-0">
|
|
<i class="bi bi-upload me-2"></i>File Upload
|
|
</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<form action="/admin/upload" method="post" enctype="multipart/form-data">
|
|
<div class="mb-3">
|
|
<label for="files" class="form-label">
|
|
<i class="bi bi-file-earmark-spreadsheet me-2"></i>Select CSV Files
|
|
</label>
|
|
<input type="file" class="form-control" id="files" name="files" multiple accept=".csv">
|
|
<div class="form-text">
|
|
Supported formats: ROLODEX*.csv, PHONE*.csv, FILES*.csv, LEDGER*.csv, QDROS*.csv, PAYMENTS*.csv
|
|
</div>
|
|
</div>
|
|
<button type="submit" class="btn btn-primary">
|
|
<i class="bi bi-cloud-upload me-2"></i>Upload Files
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Upload Results -->
|
|
{% if upload_results %}
|
|
<div class="card mb-4">
|
|
<div class="card-header bg-success text-white">
|
|
<h5 class="mb-0">
|
|
<i class="bi bi-check-circle me-2"></i>Upload Results
|
|
</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="row">
|
|
<div class="col-md-8">
|
|
<div class="table-responsive">
|
|
<table class="table table-sm">
|
|
<thead>
|
|
<tr>
|
|
<th>Original Filename</th>
|
|
<th>Stored Filename</th>
|
|
<th>Import Type</th>
|
|
<th>Size</th>
|
|
<th>Status</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for result in upload_results %}
|
|
<tr>
|
|
<td>
|
|
<strong>{{ result.filename }}</strong>
|
|
<br>
|
|
<small class="text-muted">Original name</small>
|
|
</td>
|
|
<td>
|
|
<code class="small">{{ result.stored_filename }}</code>
|
|
<br>
|
|
<small class="text-muted">Stored as</small>
|
|
</td>
|
|
<td>
|
|
<span class="badge bg-primary">{{ result.import_type }}</span>
|
|
</td>
|
|
<td>{{ result.size }} bytes</td>
|
|
<td><i class="bi bi-check-circle text-success"></i> Uploaded</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<div class="alert alert-success">
|
|
<h6><i class="bi bi-info-circle me-2"></i>Ready for Import</h6>
|
|
<p class="mb-0">Files have been uploaded and validated. Use the import section below to process the data.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Upload Errors -->
|
|
{% if upload_errors %}
|
|
<div class="card mb-4">
|
|
<div class="card-header bg-danger text-white">
|
|
<h5 class="mb-0">
|
|
<i class="bi bi-exclamation-triangle me-2"></i>Upload Errors
|
|
</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<ul class="list-group list-group-flush">
|
|
{% for error in upload_errors %}
|
|
<li class="list-group-item text-danger">
|
|
<i class="bi bi-x-circle me-2"></i>{{ error }}
|
|
</li>
|
|
{% endfor %}
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Import Section -->
|
|
<div class="card mb-4">
|
|
<div class="card-header bg-warning">
|
|
<h5 class="mb-0">
|
|
<i class="bi bi-arrow-down-circle me-2"></i>Data Import
|
|
</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
{% if files_by_type %}
|
|
<div class="row">
|
|
{% for import_type, files in files_by_type.items() %}
|
|
<div class="col-md-6 mb-4">
|
|
<div class="card h-100">
|
|
<div class="card-header">
|
|
<h6 class="mb-0">
|
|
<i class="bi bi-database me-2"></i>{{ import_type.title() }} Data
|
|
<span class="badge bg-secondary ms-2">{{ files|length }}</span>
|
|
</h6>
|
|
</div>
|
|
<div class="card-body">
|
|
<form action="/admin/import/{{ import_type }}" method="post">
|
|
<div class="mb-3">
|
|
<div class="d-flex justify-content-between align-items-center mb-2">
|
|
<label class="form-label mb-0">Available Files:</label>
|
|
<button type="button" class="btn btn-outline-primary btn-sm select-all-btn"
|
|
data-import-type="{{ import_type }}">
|
|
<i class="bi bi-check-all me-1"></i>Select All
|
|
</button>
|
|
</div>
|
|
<div class="list-group">
|
|
{% for file in files %}
|
|
<label class="list-group-item d-flex justify-content-between align-items-center">
|
|
<div>
|
|
<input class="form-check-input me-2 file-checkbox" type="checkbox"
|
|
name="selected_files" value="{{ file.filename }}" id="{{ file.filename }}">
|
|
<small class="text-muted">{{ file.filename }}</small>
|
|
<br>
|
|
<small class="text-muted">{{ file.size }} bytes • {{ file.modified.strftime('%Y-%m-%d %H:%M') }}</small>
|
|
</div>
|
|
</label>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
<button type="submit" class="btn btn-success btn-sm">
|
|
<i class="bi bi-download me-2"></i>Import {{ import_type.title() }} Data
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
{% else %}
|
|
<div class="alert alert-info">
|
|
<i class="bi bi-info-circle me-2"></i>No CSV files available for import. Upload files first.
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Import Results -->
|
|
{% if import_results %}
|
|
<div class="card mb-4">
|
|
<div class="card-header bg-info text-white">
|
|
<h5 class="mb-0">
|
|
<i class="bi bi-graph-up me-2"></i>Import Results
|
|
</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="row mb-3">
|
|
<div class="col-md-3">
|
|
<div class="card bg-success text-white">
|
|
<div class="card-body text-center">
|
|
<h3 class="mb-0">{{ total_success }}</h3>
|
|
<small>Successful</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="card bg-danger text-white">
|
|
<div class="card-body text-center">
|
|
<h3 class="mb-0">{{ total_errors }}</h3>
|
|
<small>Errors</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="card bg-primary text-white">
|
|
<div class="card-body text-center">
|
|
<h3 class="mb-0">{{ import_results|length }}</h3>
|
|
<small>Files</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="card bg-warning text-white">
|
|
<div class="card-body text-center">
|
|
<h3 class="mb-0">{{ total_success + total_errors }}</h3>
|
|
<small>Total Records</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="table-responsive">
|
|
<table class="table table-striped">
|
|
<thead>
|
|
<tr>
|
|
<th>Filename</th>
|
|
<th>Status</th>
|
|
<th>Total Rows</th>
|
|
<th>Success</th>
|
|
<th>Errors</th>
|
|
<th>Details</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for result in import_results %}
|
|
<tr>
|
|
<td>{{ result.filename }}</td>
|
|
<td>
|
|
{% if result.status == 'success' %}
|
|
<span class="badge bg-success">Success</span>
|
|
{% else %}
|
|
<span class="badge bg-danger">Error</span>
|
|
{% endif %}
|
|
</td>
|
|
<td>{{ result.total_rows }}</td>
|
|
<td class="text-success">{{ result.success_count }}</td>
|
|
<td class="text-danger">{{ result.error_count }}</td>
|
|
<td>
|
|
{% if result.errors %}
|
|
<button class="btn btn-sm btn-outline-danger" type="button"
|
|
data-bs-toggle="collapse" data-bs-target="#errors-{{ loop.index }}">
|
|
View Errors ({{ result.errors|length }})
|
|
</button>
|
|
<div class="collapse mt-2" id="errors-{{ loop.index }}">
|
|
<div class="card card-body">
|
|
<ul class="list-unstyled mb-0">
|
|
{% for error in result.errors %}
|
|
<li class="text-danger small">
|
|
<i class="bi bi-x-circle me-1"></i>{{ error }}
|
|
</li>
|
|
{% endfor %}
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
{% else %}
|
|
<span class="text-muted">No errors</span>
|
|
{% endif %}
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Recent Import History -->
|
|
{% if recent_imports %}
|
|
<div class="card">
|
|
<div class="card-header bg-secondary text-white">
|
|
<h5 class="mb-0">
|
|
<i class="bi bi-clock-history me-2"></i>Recent Import History
|
|
</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="table-responsive">
|
|
<table class="table table-hover">
|
|
<thead>
|
|
<tr>
|
|
<th>Date/Time</th>
|
|
<th>Type</th>
|
|
<th>File</th>
|
|
<th>Status</th>
|
|
<th>Records</th>
|
|
<th>Success</th>
|
|
<th>Errors</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for import_log in recent_imports %}
|
|
<tr>
|
|
<td>{{ import_log.created_at.strftime('%Y-%m-%d %H:%M') }}</td>
|
|
<td>
|
|
<span class="badge bg-primary">{{ import_log.import_type }}</span>
|
|
</td>
|
|
<td>{{ import_log.file_name }}</td>
|
|
<td>
|
|
{% if import_log.status == 'completed' %}
|
|
<span class="badge bg-success">Completed</span>
|
|
{% elif import_log.status == 'failed' %}
|
|
<span class="badge bg-danger">Failed</span>
|
|
{% elif import_log.status == 'running' %}
|
|
<span class="badge bg-warning">Running</span>
|
|
{% else %}
|
|
<span class="badge bg-secondary">{{ import_log.status }}</span>
|
|
{% endif %}
|
|
</td>
|
|
<td>{{ import_log.total_rows }}</td>
|
|
<td class="text-success">{{ import_log.success_count }}</td>
|
|
<td class="text-danger">{{ import_log.error_count }}</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
// Auto-refresh import status for running imports
|
|
function refreshRunningImports() {
|
|
const runningImports = document.querySelectorAll('span.badge.bg-warning');
|
|
if (runningImports.length > 0) {
|
|
// In a real application, you might implement WebSocket or polling here
|
|
setTimeout(refreshRunningImports, 5000); // Check every 5 seconds
|
|
}
|
|
}
|
|
|
|
// Start refresh cycle if there are running imports
|
|
refreshRunningImports();
|
|
|
|
// Select All functionality
|
|
document.querySelectorAll('.select-all-btn').forEach(button => {
|
|
button.addEventListener('click', function() {
|
|
const importType = this.getAttribute('data-import-type');
|
|
const form = this.closest('form');
|
|
const checkboxes = form.querySelectorAll('.file-checkbox');
|
|
const submitBtn = form.querySelector('button[type="submit"]');
|
|
|
|
// Toggle all checkboxes in this form
|
|
const allChecked = Array.from(checkboxes).every(cb => cb.checked);
|
|
checkboxes.forEach(checkbox => {
|
|
checkbox.checked = !allChecked;
|
|
});
|
|
|
|
// Update button text
|
|
this.innerHTML = allChecked ?
|
|
'<i class="bi bi-check-all me-1"></i>Select All' :
|
|
'<i class="bi bi-dash-square me-1"></i>Deselect All';
|
|
|
|
// Update submit button state
|
|
const hasSelection = Array.from(checkboxes).some(cb => cb.checked);
|
|
submitBtn.disabled = !hasSelection;
|
|
});
|
|
});
|
|
|
|
// File selection helpers
|
|
document.querySelectorAll('.file-checkbox').forEach(checkbox => {
|
|
checkbox.addEventListener('change', function() {
|
|
const form = this.closest('form');
|
|
const checkboxes = form.querySelectorAll('.file-checkbox');
|
|
const submitBtn = form.querySelector('button[type="submit"]');
|
|
const selectAllBtn = form.querySelector('.select-all-btn');
|
|
|
|
// Enable/disable submit button based on selection
|
|
const hasSelection = Array.from(checkboxes).some(cb => cb.checked);
|
|
submitBtn.disabled = !hasSelection;
|
|
|
|
// Update select all button state
|
|
const allChecked = Array.from(checkboxes).every(cb => cb.checked);
|
|
const noneChecked = Array.from(checkboxes).every(cb => !cb.checked);
|
|
|
|
if (allChecked) {
|
|
selectAllBtn.innerHTML = '<i class="bi bi-dash-square me-1"></i>Deselect All';
|
|
} else if (noneChecked) {
|
|
selectAllBtn.innerHTML = '<i class="bi bi-check-all me-1"></i>Select All';
|
|
} else {
|
|
selectAllBtn.innerHTML = '<i class="bi bi-check-square me-1"></i>Select All';
|
|
}
|
|
});
|
|
});
|
|
|
|
// Initialize submit buttons as disabled
|
|
document.querySelectorAll('form[action*="/admin/import/"]').forEach(form => {
|
|
const submitBtn = form.querySelector('button[type="submit"]');
|
|
if (submitBtn) {
|
|
submitBtn.disabled = true;
|
|
}
|
|
});
|
|
});
|
|
</script>
|
|
{% endblock %}
|