- 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
930 lines
48 KiB
HTML
930 lines
48 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">
|
||
<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 %}
|