Compare commits
3 Commits
2e49340663
...
4dbc452b65
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4dbc452b65 | ||
|
|
216adcc1f6 | ||
|
|
728d26ad17 |
21
.dockerignore
Normal file
21
.dockerignore
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*.so
|
||||||
|
*.egg
|
||||||
|
*.egg-info/
|
||||||
|
.venv/
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
build/
|
||||||
|
dist/
|
||||||
|
node_modules/
|
||||||
|
.DS_Store
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
delphi.db
|
||||||
|
cookies.txt
|
||||||
|
data-import/*
|
||||||
|
!data-import/.gitkeep
|
||||||
|
|
||||||
2
.env
2
.env
@@ -1,3 +1 @@
|
|||||||
# Delphi Database Environment Configuration
|
|
||||||
SECRET_KEY=your-secret-key-here-change-this-in-production
|
SECRET_KEY=your-secret-key-here-change-this-in-production
|
||||||
DATABASE_URL=sqlite:///./delphi.db
|
|
||||||
|
|||||||
30
Dockerfile
Normal file
30
Dockerfile
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
FROM python:3.12-slim
|
||||||
|
|
||||||
|
ENV PYTHONDONTWRITEBYTECODE=1
|
||||||
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Minimal tooling for healthcheck
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends curl \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
COPY requirements.txt ./
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
# Copy application
|
||||||
|
COPY app ./app
|
||||||
|
COPY static ./static
|
||||||
|
COPY delphi-logo.webp ./delphi-logo.webp
|
||||||
|
COPY old-csv ./old-csv
|
||||||
|
COPY old-database ./old-database
|
||||||
|
COPY data-import ./data-import
|
||||||
|
|
||||||
|
ENV DATABASE_URL=sqlite:///./delphi.db
|
||||||
|
|
||||||
|
EXPOSE 8000
|
||||||
|
|
||||||
|
HEALTHCHECK --interval=30s --timeout=3s --retries=3 CMD curl -fsS http://localhost:8000/health || exit 1
|
||||||
|
|
||||||
|
CMD ["uvicorn","app.main:app","--host","0.0.0.0","--port","8000"]
|
||||||
|
|
||||||
52
TODO.md
52
TODO.md
@@ -2,17 +2,17 @@
|
|||||||
|
|
||||||
Refer to `del.plan.md` for context. Check off items as they’re completed.
|
Refer to `del.plan.md` for context. Check off items as they’re completed.
|
||||||
|
|
||||||
- [ ] Create project directories and empty files per structure
|
- [x] Create project directories and empty files per structure
|
||||||
- [ ] Create requirements.txt with minimal deps
|
- [x] Create requirements.txt with minimal deps
|
||||||
- [ ] Copy delphi-logo.webp into static/logo/
|
- [x] Copy delphi-logo.webp into static/logo/
|
||||||
- [ ] Set up SQLAlchemy Base and engine/session helpers
|
- [x] Set up SQLAlchemy Base and engine/session helpers
|
||||||
- [x] Add User model with username and password_hash
|
- [x] Add User model with username and password_hash
|
||||||
- [ ] Add Client model (rolodex_id and core fields)
|
- [x] Add Client model (rolodex_id and core fields)
|
||||||
- [ ] Add Phone model with FK to Client
|
- [x] Add Phone model with FK to Client
|
||||||
- [ ] Add Case model (file_no unique, FK to Client)
|
- [x] Add Case model (file_no unique, FK to Client)
|
||||||
- [ ] Add Transaction model with FK to Case
|
- [x] Add Transaction model with FK to Case
|
||||||
- [ ] Add Document model with FK to Case
|
- [x] Add Document model with FK to Case
|
||||||
- [ ] Add Payment model with FK to Case
|
- [x] Add Payment model with FK to Case
|
||||||
- [x] Create tables and seed default admin user
|
- [x] Create tables and seed default admin user
|
||||||
- [x] Create FastAPI app with DB session dependency
|
- [x] Create FastAPI app with DB session dependency
|
||||||
- [x] Add SessionMiddleware with SECRET_KEY from env
|
- [x] Add SessionMiddleware with SECRET_KEY from env
|
||||||
@@ -20,20 +20,20 @@ Refer to `del.plan.md` for context. Check off items as they’re completed.
|
|||||||
- [x] Create base.html with Bootstrap 5 CDN and nav
|
- [x] Create base.html with Bootstrap 5 CDN and nav
|
||||||
- [x] Implement login form, POST handler, and logout
|
- [x] Implement login form, POST handler, and logout
|
||||||
- [x] Create login.html form
|
- [x] Create login.html form
|
||||||
- [ ] Implement dashboard route listing cases
|
- [x] Implement dashboard route listing cases
|
||||||
- [ ] Add simple search by file_no/name/keyword
|
- [x] Add simple search by file_no/name/keyword
|
||||||
- [ ] Create dashboard.html with table and search box
|
- [x] Create dashboard.html with table and search box
|
||||||
- [ ] Implement case view and edit POST
|
- [x] Implement case view and edit POST
|
||||||
- [ ] Create case.html with form and tabs
|
- [x] Create case.html with form and tabs
|
||||||
- [ ] Implement admin page with file upload
|
- [x] Implement admin page with file upload
|
||||||
- [ ] Create admin.html with upload form and results
|
- [x] Create admin.html with upload form and results
|
||||||
- [ ] Build CSV import core with dispatch by filename
|
- [x] Build CSV import core with dispatch by filename
|
||||||
- [ ] Importer for ROLODEX → Client
|
- [x] Importer for ROLODEX → Client
|
||||||
- [ ] Importer for PHONE → Phone
|
- [x] Importer for PHONE → Phone
|
||||||
- [ ] Importer for FILES → Case
|
- [x] Importer for FILES → Case
|
||||||
- [ ] Importer for LEDGER → Transaction
|
- [x] Importer for LEDGER → Transaction
|
||||||
- [ ] Importer for QDROS → Document
|
- [x] Importer for QDROS → Document
|
||||||
- [ ] Importer for PAYMENTS → Payment
|
- [x] Importer for PAYMENTS → Payment
|
||||||
- [ ] Wire admin POST to run selected importers
|
- [x] Wire admin POST to run selected importers
|
||||||
- [ ] Run app and test login/import/list/case-edit
|
- [ ] Run app and test login/import/list/case-edit
|
||||||
- [ ] Add minimal Dockerfile and compose for local run
|
- [x] Add minimal Dockerfile and compose for local run
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
1032
app/main.py
1032
app/main.py
File diff suppressed because it is too large
Load Diff
@@ -182,3 +182,29 @@ class Payment(Base):
|
|||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<Payment(id={self.id}, amount={self.amount})>"
|
return f"<Payment(id={self.id}, amount={self.amount})>"
|
||||||
|
|
||||||
|
|
||||||
|
class ImportLog(Base):
|
||||||
|
"""
|
||||||
|
ImportLog model for tracking CSV import operations.
|
||||||
|
|
||||||
|
Records the history and results of bulk data imports from legacy CSV files.
|
||||||
|
"""
|
||||||
|
__tablename__ = "import_logs"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
import_type = Column(String(50), nullable=False) # client, phone, case, transaction, document, payment
|
||||||
|
file_name = Column(String(255), nullable=False)
|
||||||
|
file_path = Column(String(500), nullable=False)
|
||||||
|
status = Column(String(20), default="pending") # pending, running, completed, failed
|
||||||
|
total_rows = Column(Integer, default=0)
|
||||||
|
processed_rows = Column(Integer, default=0)
|
||||||
|
success_count = Column(Integer, default=0)
|
||||||
|
error_count = Column(Integer, default=0)
|
||||||
|
error_details = Column(Text) # JSON string of error details
|
||||||
|
started_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||||
|
completed_at = Column(DateTime(timezone=True))
|
||||||
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<ImportLog(id={self.id}, type='{self.import_type}', status='{self.status}')>"
|
||||||
|
|||||||
@@ -1 +1,375 @@
|
|||||||
<!-- Admin CSV import interface -->
|
{% 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>Import Type</th>
|
||||||
|
<th>Size</th>
|
||||||
|
<th>Status</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for result in upload_results %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ result.filename }}</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">
|
||||||
|
<label class="form-label">Available Files:</label>
|
||||||
|
<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" 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();
|
||||||
|
|
||||||
|
// File selection helpers
|
||||||
|
document.querySelectorAll('input[type="checkbox"]').forEach(checkbox => {
|
||||||
|
checkbox.addEventListener('change', function() {
|
||||||
|
const form = this.closest('form');
|
||||||
|
const checkboxes = form.querySelectorAll('input[name="selected_files"]');
|
||||||
|
const submitBtn = form.querySelector('button[type="submit"]');
|
||||||
|
|
||||||
|
// Enable/disable submit button based on selection
|
||||||
|
const hasSelection = Array.from(checkboxes).some(cb => cb.checked);
|
||||||
|
submitBtn.disabled = !hasSelection;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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 %}
|
||||||
|
|||||||
@@ -20,6 +20,31 @@ Case {{ case.file_no if case else '' }} · Delphi Database
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{% if saved %}
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
||||||
|
<i class="bi bi-check-circle me-2"></i>
|
||||||
|
Case updated successfully!
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if errors %}
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||||
|
<i class="bi bi-exclamation-triangle me-2"></i>
|
||||||
|
<strong>Please fix the following errors:</strong>
|
||||||
|
<ul class="mb-0 mt-2">
|
||||||
|
{% for error in errors %}
|
||||||
|
<li>{{ error }}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% if case %}
|
{% if case %}
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
@@ -83,6 +108,78 @@ Case {{ case.file_no if case else '' }} · Delphi Database
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Edit Case Form -->
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
|
<h5 class="mb-0">Edit Case</h5>
|
||||||
|
<div>
|
||||||
|
{% if case.status == 'active' %}
|
||||||
|
<form method="post" action="/case/{{ case.id }}/close" class="d-inline me-2">
|
||||||
|
<button type="submit" class="btn btn-sm btn-outline-danger"
|
||||||
|
onclick="return confirm('Are you sure you want to close this case?')">
|
||||||
|
<i class="bi bi-x-circle me-1"></i>Close Case
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
{% if case.status == 'closed' %}
|
||||||
|
<form method="post" action="/case/{{ case.id }}/reopen" class="d-inline me-2">
|
||||||
|
<button type="submit" class="btn btn-sm btn-outline-success"
|
||||||
|
onclick="return confirm('Are you sure you want to reopen this case?')">
|
||||||
|
<i class="bi bi-check-circle me-1"></i>Reopen Case
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="post" action="/case/{{ case.id }}/update">
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="status" class="form-label">Status</label>
|
||||||
|
<select class="form-select" id="status" name="status">
|
||||||
|
<option value="active" {% if case.status == 'active' %}selected{% endif %}>Active</option>
|
||||||
|
<option value="closed" {% if case.status == 'closed' %}selected{% endif %}>Closed</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="case_type" class="form-label">Case Type</label>
|
||||||
|
<input type="text" class="form-control" id="case_type" name="case_type"
|
||||||
|
value="{{ case.case_type or '' }}">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12">
|
||||||
|
<label for="description" class="form-label">Description</label>
|
||||||
|
<textarea class="form-control" id="description" name="description" rows="3">{{ case.description or '' }}</textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="open_date" class="form-label">Open Date</label>
|
||||||
|
<input type="date" class="form-control" id="open_date" name="open_date"
|
||||||
|
value="{{ case.open_date.strftime('%Y-%m-%d') if case.open_date else '' }}">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="close_date" class="form-label">Close Date</label>
|
||||||
|
<input type="date" class="form-control" id="close_date" name="close_date"
|
||||||
|
value="{{ case.close_date.strftime('%Y-%m-%d') if case.close_date else '' }}">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<i class="bi bi-check-lg me-1"></i>Save Changes
|
||||||
|
</button>
|
||||||
|
<a href="/case/{{ case.id }}" class="btn btn-outline-secondary">Cancel</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="col-xl-4">
|
<div class="col-xl-4">
|
||||||
<div class="card h-100">
|
<div class="card h-100">
|
||||||
<div class="card-header">Transactions</div>
|
<div class="card-header">Transactions</div>
|
||||||
|
|||||||
@@ -2,4 +2,4 @@
|
|||||||
# https://curl.se/docs/http-cookies.html
|
# https://curl.se/docs/http-cookies.html
|
||||||
# This file was generated by libcurl! Edit at your own risk.
|
# This file was generated by libcurl! Edit at your own risk.
|
||||||
|
|
||||||
#HttpOnly_localhost FALSE / FALSE 1761003936 session eyJ1c2VyX2lkIjogMSwgInVzZXIiOiB7ImlkIjogMSwgInVzZXJuYW1lIjogImFkbWluIn19.aORUoA.KSAst9pcXJJJHTc1m-mY_jQ0P6A
|
#HttpOnly_localhost FALSE / FALSE 1761009191 session eyJ1c2VyX2lkIjogMSwgInVzZXIiOiB7ImlkIjogMSwgInVzZXJuYW1lIjogImFkbWluIn19.aORpJw.oMEiA8ZMjrlLoJlYpDsM_T5EMpk
|
||||||
|
|||||||
2
data-import/FILES_TEST.csv
Executable file
2
data-import/FILES_TEST.csv
Executable file
@@ -0,0 +1,2 @@
|
|||||||
|
File_No,Id,File_Type,Regarding,Opened,Closed,Empl_Num,Rate_Per_Hour,Status,Footer_Code,Opposing,Hours,Hours_P,Trust_Bal,Trust_Bal_P,Hourly_Fees,Hourly_Fees_P,Flat_Fees,Flat_Fees_P,Disbursements,Disbursements_P,Credit_Bal,Credit_Bal_P,Total_Charges,Total_Charges_P,Amount_Owing,Amount_Owing_P,Transferable,Memo
|
||||||
|
TEST-001,1,Family Law,Divorce case,2024-01-15,2024-06-15,EMP001,150.00,active,FC001,,10.5,0,1000.00,0,1575.00,0,0,0,0,0,0,0,0,0,0,0,0,Test case for development
|
||||||
|
1
data-import/PHONE_TEST.csv
Executable file
1
data-import/PHONE_TEST.csv
Executable file
@@ -0,0 +1 @@
|
|||||||
|
Id,Phone,Location
|
||||||
|
2
data-import/ROLODEX_TEST.csv
Executable file
2
data-import/ROLODEX_TEST.csv
Executable file
@@ -0,0 +1,2 @@
|
|||||||
|
Id,Prefix,First,Middle,Last,Suffix,Title,A1,A2,A3,City,Abrev,St,Zip,Email,DOB,SS#,Legal_Status,Group,Memo
|
||||||
|
1,,John,,Doe,,Attorney,123 Main St,,Apt 2B,New York,,NY,10001,john.doe@example.com,1970-01-01,123-45-6789,Active,Group A,Test client record
|
||||||
|
22
docker-compose.yml
Normal file
22
docker-compose.yml
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
version: "3.9"
|
||||||
|
|
||||||
|
services:
|
||||||
|
web:
|
||||||
|
build: .
|
||||||
|
container_name: delphicg-web
|
||||||
|
ports:
|
||||||
|
- "8000:8000"
|
||||||
|
environment:
|
||||||
|
- SECRET_KEY=${SECRET_KEY}
|
||||||
|
- DATABASE_URL=sqlite:///./delphi.db
|
||||||
|
volumes:
|
||||||
|
- ./data-import:/app/data-import
|
||||||
|
- ./delphi.db:/app/delphi.db
|
||||||
|
- ./old-csv:/app/old-csv:ro
|
||||||
|
- ./static/logo:/app/static/logo
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "curl -fsS http://localhost:8000/health || exit 1"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 3
|
||||||
|
|
||||||
Reference in New Issue
Block a user