items
This commit is contained in:
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.
86
app/main.py
86
app/main.py
@@ -196,10 +196,15 @@ def validate_csv_headers(headers: List[str], expected_fields: Dict[str, str]) ->
|
|||||||
if not matched:
|
if not matched:
|
||||||
result['errors'].append(f"Unknown header: '{csv_header}'")
|
result['errors'].append(f"Unknown header: '{csv_header}'")
|
||||||
|
|
||||||
# Check for required fields
|
# Check for required fields (case-insensitive)
|
||||||
required_fields = ['id'] # Most imports need some form of ID
|
required_fields = ['id'] # Most imports need some form of ID
|
||||||
for required in required_fields:
|
for required in required_fields:
|
||||||
if required not in result['field_mapping']:
|
found = False
|
||||||
|
for mapped_field in result['field_mapping']:
|
||||||
|
if mapped_field.lower() == required.lower():
|
||||||
|
found = True
|
||||||
|
break
|
||||||
|
if not found:
|
||||||
result['missing_fields'].append(required)
|
result['missing_fields'].append(required)
|
||||||
|
|
||||||
if result['missing_fields'] or result['errors']:
|
if result['missing_fields'] or result['errors']:
|
||||||
@@ -263,15 +268,26 @@ def import_rolodex_data(db: Session, file_path: str) -> Dict[str, Any]:
|
|||||||
}
|
}
|
||||||
|
|
||||||
expected_fields = {
|
expected_fields = {
|
||||||
'rolodex_id': 'Client ID',
|
'Id': 'Client ID',
|
||||||
'first_name': 'First Name',
|
'Prefix': 'Name Prefix',
|
||||||
'middle_initial': 'Middle Initial',
|
'First': 'First Name',
|
||||||
'last_name': 'Last Name',
|
'Middle': 'Middle Initial',
|
||||||
'company': 'Company/Organization',
|
'Last': 'Last Name',
|
||||||
'address': 'Address Line 1',
|
'Suffix': 'Name Suffix',
|
||||||
'city': 'City',
|
'Title': 'Company/Organization',
|
||||||
'state': 'State',
|
'A1': 'Address Line 1',
|
||||||
'zip_code': 'ZIP Code'
|
'A2': 'Address Line 2',
|
||||||
|
'A3': 'Address Line 3',
|
||||||
|
'City': 'City',
|
||||||
|
'Abrev': 'State Abbreviation',
|
||||||
|
'St': 'State',
|
||||||
|
'Zip': 'ZIP Code',
|
||||||
|
'Email': 'Email Address',
|
||||||
|
'DOB': 'Date of Birth',
|
||||||
|
'SS#': 'Social Security Number',
|
||||||
|
'Legal_Status': 'Legal Status',
|
||||||
|
'Group': 'Group',
|
||||||
|
'Memo': 'Memo/Notes'
|
||||||
}
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -404,12 +420,35 @@ def import_files_data(db: Session, file_path: str) -> Dict[str, Any]:
|
|||||||
}
|
}
|
||||||
|
|
||||||
expected_fields = {
|
expected_fields = {
|
||||||
'file_no': 'File Number',
|
'File_No': 'File Number',
|
||||||
'status': 'Status',
|
'Status': 'Status',
|
||||||
'case_type': 'File Type',
|
'File_Type': 'File Type',
|
||||||
'description': 'Regarding',
|
'Regarding': 'Regarding',
|
||||||
'open_date': 'Opened Date',
|
'Opened': 'Opened Date',
|
||||||
'close_date': 'Closed Date'
|
'Closed': 'Closed Date',
|
||||||
|
'Id': 'Client ID',
|
||||||
|
'Empl_Num': 'Employee Number',
|
||||||
|
'Rate_Per_Hour': 'Rate Per Hour',
|
||||||
|
'Footer_Code': 'Footer Code',
|
||||||
|
'Opposing': 'Opposing Party',
|
||||||
|
'Hours': 'Hours',
|
||||||
|
'Hours_P': 'Hours (Previous)',
|
||||||
|
'Trust_Bal': 'Trust Balance',
|
||||||
|
'Trust_Bal_P': 'Trust Balance (Previous)',
|
||||||
|
'Hourly_Fees': 'Hourly Fees',
|
||||||
|
'Hourly_Fees_P': 'Hourly Fees (Previous)',
|
||||||
|
'Flat_Fees': 'Flat Fees',
|
||||||
|
'Flat_Fees_P': 'Flat Fees (Previous)',
|
||||||
|
'Disbursements': 'Disbursements',
|
||||||
|
'Disbursements_P': 'Disbursements (Previous)',
|
||||||
|
'Credit_Bal': 'Credit Balance',
|
||||||
|
'Credit_Bal_P': 'Credit Balance (Previous)',
|
||||||
|
'Total_Charges': 'Total Charges',
|
||||||
|
'Total_Charges_P': 'Total Charges (Previous)',
|
||||||
|
'Amount_Owing': 'Amount Owing',
|
||||||
|
'Amount_Owing_P': 'Amount Owing (Previous)',
|
||||||
|
'Transferable': 'Transferable',
|
||||||
|
'Memo': 'Memo'
|
||||||
}
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -1180,11 +1219,6 @@ async def case_detail(
|
|||||||
async def case_update(
|
async def case_update(
|
||||||
request: Request,
|
request: Request,
|
||||||
case_id: int,
|
case_id: int,
|
||||||
status: str = None,
|
|
||||||
case_type: str = None,
|
|
||||||
description: str = None,
|
|
||||||
open_date: str = None,
|
|
||||||
close_date: str = None,
|
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
) -> RedirectResponse:
|
) -> RedirectResponse:
|
||||||
"""
|
"""
|
||||||
@@ -1197,6 +1231,9 @@ async def case_update(
|
|||||||
if not user:
|
if not user:
|
||||||
return RedirectResponse(url="/login", status_code=302)
|
return RedirectResponse(url="/login", status_code=302)
|
||||||
|
|
||||||
|
# Get form data
|
||||||
|
form = await request.form()
|
||||||
|
|
||||||
# Fetch the case
|
# Fetch the case
|
||||||
case_obj = db.query(Case).filter(Case.id == case_id).first()
|
case_obj = db.query(Case).filter(Case.id == case_id).first()
|
||||||
if not case_obj:
|
if not case_obj:
|
||||||
@@ -1208,6 +1245,7 @@ async def case_update(
|
|||||||
update_data = {}
|
update_data = {}
|
||||||
|
|
||||||
# Status validation
|
# Status validation
|
||||||
|
status = form.get("status")
|
||||||
if status is not None:
|
if status is not None:
|
||||||
if status not in ["active", "closed"]:
|
if status not in ["active", "closed"]:
|
||||||
errors.append("Status must be 'active' or 'closed'")
|
errors.append("Status must be 'active' or 'closed'")
|
||||||
@@ -1215,13 +1253,16 @@ async def case_update(
|
|||||||
update_data["status"] = status
|
update_data["status"] = status
|
||||||
|
|
||||||
# Case type and description (optional)
|
# Case type and description (optional)
|
||||||
|
case_type = form.get("case_type")
|
||||||
if case_type is not None:
|
if case_type is not None:
|
||||||
update_data["case_type"] = case_type.strip() if case_type.strip() else None
|
update_data["case_type"] = case_type.strip() if case_type.strip() else None
|
||||||
|
|
||||||
|
description = form.get("description")
|
||||||
if description is not None:
|
if description is not None:
|
||||||
update_data["description"] = description.strip() if description.strip() else None
|
update_data["description"] = description.strip() if description.strip() else None
|
||||||
|
|
||||||
# Date validation and parsing
|
# Date validation and parsing
|
||||||
|
open_date = form.get("open_date")
|
||||||
if open_date is not None:
|
if open_date is not None:
|
||||||
if open_date.strip():
|
if open_date.strip():
|
||||||
try:
|
try:
|
||||||
@@ -1231,6 +1272,7 @@ async def case_update(
|
|||||||
else:
|
else:
|
||||||
update_data["open_date"] = None
|
update_data["open_date"] = None
|
||||||
|
|
||||||
|
close_date = form.get("close_date")
|
||||||
if close_date is not None:
|
if close_date is not None:
|
||||||
if close_date.strip():
|
if close_date.strip():
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -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