Files
delphi-database-v2/app/templates/admin.html
HotSwapp ac98bded69 Add detailed skip tracking for phone imports
- Track skipped_no_phone and skipped_no_id separately
- Display skip information in admin UI with warning icon
- Clarify that empty phone numbers cannot be imported (PK constraint)
- Update documentation to explain expected skip behavior
- Example: 143 rows without phone numbers is correct, not an error

When importing PHONE.csv with empty phone numbers:
- Rows are properly skipped (cannot have NULL in primary key)
- User sees: '⚠️ Skipped: 143 rows without phone number'
- This is expected behavior, not a bug
2025-10-13 08:46:53 -05:00

930 lines
48 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% 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">
<strong>Supported formats:</strong> ROLODEX, PHONE, FILES, LEDGER, PAYMENTS, DEPOSITS, QDROS, PENSIONS, PLANINFO,
TRNSTYPE, TRNSLKUP, FOOTERS, FILESTAT, EMPLOYEE, GRUPLKUP, FILETYPE, and all related tables (*.csv)
</div>
</div>
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" id="auto_import" name="auto_import" checked>
<label class="form-check-label" for="auto_import">
<strong>Auto-import after upload (follows Import Order Guide)</strong>
<br>
<small class="text-muted">Will stop on the first file that reports any row errors.</small>
</label>
</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 %}
<!-- Auto Import Results -->
{% if auto_import_results %}
<div class="card mb-4">
<div class="card-header bg-info text-white">
<h5 class="mb-0">
<i class="bi bi-lightning-charge me-2"></i>Auto Import Results
</h5>
</div>
<div class="card-body">
{% if auto_import_results.stopped %}
<div class="alert alert-warning">
<i class="bi bi-exclamation-triangle me-2"></i>
Stopped after {{ auto_import_results.files|length }} file(s) due to errors in <code>{{ auto_import_results.stopped_on }}</code>.
</div>
{% endif %}
<div class="table-responsive">
<table class="table table-sm table-bordered">
<thead>
<tr>
<th>Filename</th>
<th>Type</th>
<th>Status</th>
<th>Total</th>
<th>Success</th>
<th>Errors</th>
<th>Error Details</th>
</tr>
</thead>
<tbody>
{% for item in auto_import_results.files %}
<tr>
<td>{{ item.filename }}</td>
<td><span class="badge bg-secondary">{{ item.import_type }}</span></td>
<td>
{% if item.status == 'success' %}
<span class="badge bg-success">Completed</span>
{% else %}
<span class="badge bg-danger">Failed</span>
{% endif %}
</td>
<td>{{ item.total_rows }}</td>
<td class="text-success">{{ item.success_count }}</td>
<td class="text-danger">{{ item.error_count }}</td>
<td>
{% if item.errors %}
<details>
<summary class="text-danger">View Errors ({{ item.errors|length }})</summary>
<ul class="mt-2 mb-0">
{% for err in item.errors %}
<li><small>{{ err }}</small></li>
{% endfor %}
</ul>
</details>
{% elif item.skip_info %}
<small class="text-warning">⚠️ Skipped: {{ item.skip_info }}</small>
{% else %}
<span class="text-muted">None</span>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% if auto_import_results.skipped_unknowns and auto_import_results.skipped_unknowns|length > 0 %}
<div class="alert alert-info mt-3">
<i class="bi bi-info-circle me-2"></i>
{{ auto_import_results.skipped_unknowns|length }} unknown file(s) were skipped. Map them in the Data Import section.
</div>
{% endif %}
</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 %}
<!-- Database Status -->
{% if table_counts %}
<div class="card mb-4">
<div class="card-header bg-secondary text-white">
<h5 class="mb-0">
<i class="bi bi-database me-2"></i>Database Status - Imported Data
</h5>
</div>
<div class="card-body">
<p class="mb-3">View record counts for all tables to track import progress:</p>
<div class="row">
<!-- Reference Tables -->
<div class="col-md-3 mb-3">
<h6 class="text-primary"><i class="bi bi-bookmark me-2"></i>Reference Tables</h6>
<div class="table-responsive">
<table class="table table-sm table-hover">
<thead>
<tr>
<th>Table</th>
<th class="text-end">Records</th>
</tr>
</thead>
<tbody>
{% for table_name, count in table_counts.reference.items() %}
<tr class="{{ 'table-success' if count > 0 else 'table-light' }}">
<td>
<small>{{ table_name }}</small>
{% if count > 0 %}
<i class="bi bi-check-circle-fill text-success ms-1"></i>
{% endif %}
</td>
<td class="text-end">
<span class="badge {{ 'bg-success' if count > 0 else 'bg-secondary' }}">
{{ "{:,}".format(count) }}
</span>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<!-- Core Data Tables -->
<div class="col-md-3 mb-3">
<h6 class="text-success"><i class="bi bi-folder me-2"></i>Core Data Tables</h6>
<div class="table-responsive">
<table class="table table-sm table-hover">
<thead>
<tr>
<th>Table</th>
<th class="text-end">Records</th>
</tr>
</thead>
<tbody>
{% for table_name, count in table_counts.core.items() %}
<tr class="{{ 'table-success' if count > 0 else 'table-light' }}">
<td>
<small>{{ table_name }}</small>
{% if count > 0 %}
<i class="bi bi-check-circle-fill text-success ms-1"></i>
{% endif %}
</td>
<td class="text-end">
<span class="badge {{ 'bg-success' if count > 0 else 'bg-secondary' }}">
{{ "{:,}".format(count) }}
</span>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<!-- Specialized Tables -->
<div class="col-md-3 mb-3">
<h6 class="text-info"><i class="bi bi-file-earmark-medical me-2"></i>Specialized Tables</h6>
<div class="table-responsive">
<table class="table table-sm table-hover">
<thead>
<tr>
<th>Table</th>
<th class="text-end">Records</th>
</tr>
</thead>
<tbody>
{% for table_name, count in table_counts.specialized.items() %}
<tr class="{{ 'table-success' if count > 0 else 'table-light' }}">
<td>
<small>{{ table_name }}</small>
{% if count > 0 %}
<i class="bi bi-check-circle-fill text-success ms-1"></i>
{% endif %}
</td>
<td class="text-end">
<span class="badge {{ 'bg-success' if count > 0 else 'bg-secondary' }}">
{{ "{:,}".format(count) }}
</span>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<!-- Modern Models -->
<div class="col-md-3 mb-3">
<h6 class="text-warning"><i class="bi bi-stars me-2"></i>Modern Models</h6>
<div class="table-responsive">
<table class="table table-sm table-hover">
<thead>
<tr>
<th>Table</th>
<th class="text-end">Records</th>
</tr>
</thead>
<tbody>
{% for table_name, count in table_counts.modern.items() %}
<tr class="{{ 'table-warning' if count > 0 else 'table-light' }}">
<td>
<small>{{ table_name }}</small>
{% if count > 0 %}
<i class="bi bi-check-circle-fill text-warning ms-1"></i>
{% endif %}
</td>
<td class="text-end">
<span class="badge {{ 'bg-warning text-dark' if count > 0 else 'bg-secondary' }}">
{{ "{:,}".format(count) }}
</span>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<div class="row mt-3">
<div class="col-12">
<div class="alert alert-info mb-0">
<i class="bi bi-info-circle me-2"></i>
<strong>Legend:</strong>
<span class="badge bg-success ms-2">Green</span> = Has data imported |
<span class="badge bg-secondary ms-2">Gray</span> = No data yet |
<i class="bi bi-check-circle-fill text-success ms-3 me-1"></i> = Table populated
</div>
</div>
</div>
</div>
</div>
{% endif %}
<!-- Import Order Guide -->
<div class="card mb-4">
<div class="card-header bg-info text-white">
<h5 class="mb-0">
<i class="bi bi-list-ol me-2"></i>Import Order Guide
</h5>
</div>
<div class="card-body">
<p class="mb-3">For best results, import tables in this recommended order:</p>
<div class="row">
<div class="col-md-6">
<h6 class="text-primary"><i class="bi bi-1-circle me-2"></i>Reference Tables (Import First)</h6>
<ul class="list-unstyled ms-3">
<li><i class="bi bi-arrow-right me-2"></i>TRNSTYPE</li>
<li><i class="bi bi-arrow-right me-2"></i>TRNSLKUP</li>
<li><i class="bi bi-arrow-right me-2"></i>FOOTERS</li>
<li><i class="bi bi-arrow-right me-2"></i>FILESTAT</li>
<li><i class="bi bi-arrow-right me-2"></i>EMPLOYEE</li>
<li><i class="bi bi-arrow-right me-2"></i>GRUPLKUP</li>
<li><i class="bi bi-arrow-right me-2"></i>FILETYPE</li>
<li><i class="bi bi-arrow-right me-2"></i>FVARLKUP, RVARLKUP</li>
</ul>
</div>
<div class="col-md-6">
<h6 class="text-success"><i class="bi bi-2-circle me-2"></i>Core Data Tables</h6>
<ul class="list-unstyled ms-3">
<li><i class="bi bi-arrow-right me-2"></i>ROLODEX</li>
<li><i class="bi bi-arrow-right me-2"></i>PHONE, ROLEX_V</li>
<li><i class="bi bi-arrow-right me-2"></i>FILES (+ FILES_R, FILES_V, FILENOTS)</li>
<li><i class="bi bi-arrow-right me-2"></i>LEDGER</li>
<li><i class="bi bi-arrow-right me-2"></i>DEPOSITS, PAYMENTS</li>
<li><i class="bi bi-arrow-right me-2"></i>PLANINFO</li>
<li><i class="bi bi-arrow-right me-2"></i>QDROS, PENSIONS (+ related tables)</li>
</ul>
</div>
</div>
<div class="alert alert-warning mt-3 mb-0">
<i class="bi bi-exclamation-triangle me-2"></i>
<strong>Important:</strong> Reference tables must be imported before core data to avoid foreign key errors.
</div>
</div>
</div>
<!-- Sync to Modern Models -->
<div class="card mb-4">
<div class="card-header bg-success text-white">
<h5 class="mb-0">
<i class="bi bi-arrow-repeat me-2"></i>Sync to Modern Models
</h5>
</div>
<div class="card-body">
<p>After importing legacy CSV data, sync it to the simplified modern application models (Client, Phone, Case, Transaction, Payment, Document).</p>
<form action="/admin/sync" method="post" id="syncForm">
<div class="mb-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="clearExisting" name="clear_existing" value="true">
<label class="form-check-label" for="clearExisting">
<strong>Clear existing modern data before sync</strong>
<br>
<small class="text-muted">Warning: This will delete all current Client, Phone, Case, Transaction, Payment, and Document records!</small>
</label>
</div>
</div>
<button type="button" class="btn btn-success" onclick="confirmSync()">
<i class="bi bi-arrow-repeat me-2"></i>Start Sync Process
</button>
</form>
</div>
</div>
<!-- Sync Results -->
{% if show_sync_results and sync_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>Sync Results
</h5>
</div>
<div class="card-body">
<div class="row mb-3">
<div class="col-md-3">
<div class="card bg-light">
<div class="card-body text-center">
<h3 class="mb-0 text-success">{{ total_synced or 0 }}</h3>
<small class="text-muted">Records Synced</small>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card bg-light">
<div class="card-body text-center">
<h3 class="mb-0 text-warning">{{ total_skipped or 0 }}</h3>
<small class="text-muted">Records Skipped</small>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card bg-light">
<div class="card-body text-center">
<h3 class="mb-0 text-danger">{{ total_sync_errors or 0 }}</h3>
<small class="text-muted">Errors</small>
</div>
</div>
</div>
</div>
<h6 class="mb-3">Detailed Results by Table:</h6>
<div class="table-responsive">
<table class="table table-sm table-bordered">
<thead>
<tr>
<th>Modern Table</th>
<th>Synced</th>
<th>Skipped</th>
<th>Errors</th>
</tr>
</thead>
<tbody>
{% for table_name, result in sync_results.items() %}
<tr>
<td><strong>{{ table_name.title() }}</strong></td>
<td class="text-success">{{ result.success }}</td>
<td class="text-warning">{{ result.skipped }}</td>
<td class="text-danger">{{ result.errors|length }}</td>
</tr>
{% if result.errors %}
<tr>
<td colspan="4">
<details>
<summary class="text-danger">View Errors ({{ result.errors|length }})</summary>
<ul class="mt-2 mb-0">
{% for error in result.errors[:10] %}
<li><small>{{ error }}</small></li>
{% endfor %}
{% if result.errors|length > 10 %}
<li><small><em>... and {{ result.errors|length - 10 }} more errors</em></small></li>
{% endif %}
</ul>
</details>
</td>
</tr>
{% endif %}
{% endfor %}
</tbody>
</table>
</div>
</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">
{% if import_type == 'unknown' and valid_import_types %}
<div class="mb-3 d-flex align-items-end gap-2">
<div>
<label class="form-label mb-1">Map selected to:</label>
<select class="form-select form-select-sm" id="mapTypeSelect-{{ loop.index }}">
{% for t in valid_import_types %}
<option value="{{ t }}">{{ t.title().replace('_', ' ') }}</option>
{% endfor %}
</select>
</div>
<button type="button" class="btn btn-sm btn-warning" onclick="mapSelectedFiles(this, '{{ import_type }}')">
<i class="bi bi-tags"></i> Map Selected
</button>
</div>
{% endif %}
<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 class="flex-grow-1">
<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>
<button type="button" class="btn btn-sm btn-outline-danger delete-file-btn"
data-filename="{{ file.filename }}"
onclick="deleteFile('{{ file.filename }}', event)">
<i class="bi bi-trash"></i>
</button>
</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;
}
});
});
// Sync confirmation function
function confirmSync() {
const clearCheckbox = document.getElementById('clearExisting');
const clearExisting = clearCheckbox.checked;
let message = "Are you sure you want to sync legacy data to modern models?";
if (clearExisting) {
message += "\n\n⚠ WARNING: This will DELETE all existing Client, Phone, Case, Transaction, Payment, and Document records before syncing!";
}
if (confirm(message)) {
document.getElementById('syncForm').submit();
}
}
// Delete file function
async function deleteFile(filename, event) {
// Prevent label click from triggering checkbox
event.preventDefault();
event.stopPropagation();
if (!confirm(`Are you sure you want to delete "${filename}"?\n\nThis action cannot be undone.`)) {
return;
}
try {
const response = await fetch(`/admin/delete-file/${encodeURIComponent(filename)}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
}
});
if (response.ok) {
// Reload the page to refresh the file list
window.location.reload();
} else {
const error = await response.json();
alert(`Error deleting file: ${error.detail || 'Unknown error'}`);
}
} catch (error) {
console.error('Error deleting file:', error);
alert(`Error deleting file: ${error.message}`);
}
}
// Map selected unknown files to a chosen import type
async function mapSelectedFiles(buttonEl, importType) {
// Find the surrounding card and form
const cardBody = buttonEl.closest('.card-body');
const form = cardBody.querySelector('form');
const selectEl = cardBody.querySelector('select.form-select');
if (!form || !selectEl) return;
// Collect selected filenames
const checked = Array.from(form.querySelectorAll('.file-checkbox:checked'))
.map(cb => cb.value);
if (checked.length === 0) {
alert('Select at least one file to map.');
return;
}
const targetType = selectEl.value;
if (!targetType) {
alert('Choose a target type.');
return;
}
buttonEl.disabled = true;
try {
const resp = await fetch('/admin/map-files', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ target_type: targetType, filenames: checked })
});
if (!resp.ok) {
const err = await resp.json().catch(() => ({}));
throw new Error(err.detail || 'Mapping failed');
}
// Refresh UI
window.location.reload();
} catch (e) {
console.error(e);
alert(`Mapping failed: ${e.message}`);
} finally {
buttonEl.disabled = false;
}
}
</script>
{% endblock %}