Compare commits

...

3 Commits

Author SHA1 Message Date
HotSwapp
4dbc452b65 items 2025-10-06 20:28:00 -05:00
HotSwapp
216adcc1f6 feat: Implement comprehensive admin panel with CSV import system
- Add ImportLog model for tracking import history and results
- Create admin.html template with file upload form and progress display
- Implement POST /admin/upload route for CSV file handling with validation
- Build CSV import engine with dispatcher routing by filename patterns:
  * ROLODEX*.csv → Client model import
  * PHONE*.csv → Phone model import with client linking
  * FILES*.csv → Case model import
  * LEDGER*.csv → Transaction model import
  * QDROS*.csv → Document model import
  * PAYMENTS*.csv → Payment model import
- Add POST /admin/import/{data_type} route for triggering imports
- Implement comprehensive validation, error handling, and progress tracking
- Support for CSV header validation, data type conversions, and duplicate handling
- Real-time progress tracking with ImportLog database model
- Responsive UI with Bootstrap components for upload and results display
- Enhanced navigation with admin panel link already in place
- Tested import functionality with validation and error handling

The admin panel enables bulk importing of legacy CSV data from the old-csv/ directory, making the system fully functional with real data.
2025-10-06 19:52:31 -05:00
HotSwapp
728d26ad17 feat(case): enable editing and close/reopen actions on case detail
- Add POST /case/{id}/update route for editing case fields (status, case_type, description, open_date, close_date)
- Add POST /case/{id}/close route to set status='closed' and close_date=current date
- Add POST /case/{id}/reopen route to set status='active' and clear close_date
- Update case.html template with edit form, success/error messaging, and action buttons
- Include comprehensive validation for dates and status values
- Add proper error handling with session-based error storage
- Preserve existing view content and styling consistency
2025-10-06 19:43:21 -05:00
16 changed files with 1632 additions and 33 deletions

21
.dockerignore Normal file
View 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
View File

@@ -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
View 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
View File

@@ -2,17 +2,17 @@
Refer to `del.plan.md` for context. Check off items as theyre completed. Refer to `del.plan.md` for context. Check off items as theyre 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 theyre 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.

File diff suppressed because it is too large Load Diff

View File

@@ -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}')>"

View File

@@ -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 %}

View File

@@ -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>

View File

@@ -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
View 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 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
2 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
View File

@@ -0,0 +1 @@
Id,Phone,Location
1 Id Phone Location

2
data-import/ROLODEX_TEST.csv Executable file
View 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
1 Id Prefix First Middle Last Suffix Title A1 A2 A3 City Abrev St Zip Email DOB SS# Legal_Status Group Memo
2 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

BIN
delphi.db

Binary file not shown.

22
docker-compose.yml Normal file
View 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