diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..f0a8e50 --- /dev/null +++ b/.dockerignore @@ -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 + diff --git a/.env b/.env index 85236cb..78c69be 100644 --- a/.env +++ b/.env @@ -1,3 +1 @@ -# Delphi Database Environment Configuration SECRET_KEY=your-secret-key-here-change-this-in-production -DATABASE_URL=sqlite:///./delphi.db diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..be396da --- /dev/null +++ b/Dockerfile @@ -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"] + diff --git a/TODO.md b/TODO.md index b39b358..17b3849 100644 --- a/TODO.md +++ b/TODO.md @@ -2,17 +2,17 @@ Refer to `del.plan.md` for context. Check off items as they’re completed. -- [ ] Create project directories and empty files per structure -- [ ] Create requirements.txt with minimal deps -- [ ] Copy delphi-logo.webp into static/logo/ -- [ ] Set up SQLAlchemy Base and engine/session helpers +- [x] Create project directories and empty files per structure +- [x] Create requirements.txt with minimal deps +- [x] Copy delphi-logo.webp into static/logo/ +- [x] Set up SQLAlchemy Base and engine/session helpers - [x] Add User model with username and password_hash -- [ ] Add Client model (rolodex_id and core fields) -- [ ] Add Phone model with FK to Client -- [ ] Add Case model (file_no unique, FK to Client) -- [ ] Add Transaction model with FK to Case -- [ ] Add Document model with FK to Case -- [ ] Add Payment model with FK to Case +- [x] Add Client model (rolodex_id and core fields) +- [x] Add Phone model with FK to Client +- [x] Add Case model (file_no unique, FK to Client) +- [x] Add Transaction model with FK to Case +- [x] Add Document model with FK to Case +- [x] Add Payment model with FK to Case - [x] Create tables and seed default admin user - [x] Create FastAPI app with DB session dependency - [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] Implement login form, POST handler, and logout - [x] Create login.html form -- [ ] Implement dashboard route listing cases -- [ ] Add simple search by file_no/name/keyword -- [ ] Create dashboard.html with table and search box -- [ ] Implement case view and edit POST -- [ ] Create case.html with form and tabs -- [ ] Implement admin page with file upload -- [ ] Create admin.html with upload form and results -- [ ] Build CSV import core with dispatch by filename -- [ ] Importer for ROLODEX → Client -- [ ] Importer for PHONE → Phone -- [ ] Importer for FILES → Case -- [ ] Importer for LEDGER → Transaction -- [ ] Importer for QDROS → Document -- [ ] Importer for PAYMENTS → Payment -- [ ] Wire admin POST to run selected importers +- [x] Implement dashboard route listing cases +- [x] Add simple search by file_no/name/keyword +- [x] Create dashboard.html with table and search box +- [x] Implement case view and edit POST +- [x] Create case.html with form and tabs +- [x] Implement admin page with file upload +- [x] Create admin.html with upload form and results +- [x] Build CSV import core with dispatch by filename +- [x] Importer for ROLODEX → Client +- [x] Importer for PHONE → Phone +- [x] Importer for FILES → Case +- [x] Importer for LEDGER → Transaction +- [x] Importer for QDROS → Document +- [x] Importer for PAYMENTS → Payment +- [x] Wire admin POST to run selected importers - [ ] 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 diff --git a/app/__pycache__/main.cpython-313.pyc b/app/__pycache__/main.cpython-313.pyc index cf7d9d6..68e7316 100644 Binary files a/app/__pycache__/main.cpython-313.pyc and b/app/__pycache__/main.cpython-313.pyc differ diff --git a/app/main.py b/app/main.py index 570491e..8e6a9b6 100644 --- a/app/main.py +++ b/app/main.py @@ -196,10 +196,15 @@ def validate_csv_headers(headers: List[str], expected_fields: Dict[str, str]) -> if not matched: 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 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) 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 = { - 'rolodex_id': 'Client ID', - 'first_name': 'First Name', - 'middle_initial': 'Middle Initial', - 'last_name': 'Last Name', - 'company': 'Company/Organization', - 'address': 'Address Line 1', - 'city': 'City', - 'state': 'State', - 'zip_code': 'ZIP Code' + 'Id': 'Client ID', + 'Prefix': 'Name Prefix', + 'First': 'First Name', + 'Middle': 'Middle Initial', + 'Last': 'Last Name', + 'Suffix': 'Name Suffix', + 'Title': 'Company/Organization', + 'A1': 'Address Line 1', + '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: @@ -404,12 +420,35 @@ def import_files_data(db: Session, file_path: str) -> Dict[str, Any]: } expected_fields = { - 'file_no': 'File Number', - 'status': 'Status', - 'case_type': 'File Type', - 'description': 'Regarding', - 'open_date': 'Opened Date', - 'close_date': 'Closed Date' + 'File_No': 'File Number', + 'Status': 'Status', + 'File_Type': 'File Type', + 'Regarding': 'Regarding', + 'Opened': 'Opened 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: @@ -1180,11 +1219,6 @@ async def case_detail( async def case_update( request: Request, 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), ) -> RedirectResponse: """ @@ -1197,6 +1231,9 @@ async def case_update( if not user: return RedirectResponse(url="/login", status_code=302) + # Get form data + form = await request.form() + # Fetch the case case_obj = db.query(Case).filter(Case.id == case_id).first() if not case_obj: @@ -1208,6 +1245,7 @@ async def case_update( update_data = {} # Status validation + status = form.get("status") if status is not None: if status not in ["active", "closed"]: errors.append("Status must be 'active' or 'closed'") @@ -1215,13 +1253,16 @@ async def case_update( update_data["status"] = status # Case type and description (optional) + case_type = form.get("case_type") if case_type is not None: update_data["case_type"] = case_type.strip() if case_type.strip() else None + description = form.get("description") if description is not None: update_data["description"] = description.strip() if description.strip() else None # Date validation and parsing + open_date = form.get("open_date") if open_date is not None: if open_date.strip(): try: @@ -1231,6 +1272,7 @@ async def case_update( else: update_data["open_date"] = None + close_date = form.get("close_date") if close_date is not None: if close_date.strip(): try: diff --git a/cookies.txt b/cookies.txt index 1aa9e0f..f5452af 100644 --- a/cookies.txt +++ b/cookies.txt @@ -2,4 +2,4 @@ # https://curl.se/docs/http-cookies.html # 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 diff --git a/data-import/FILES_TEST.csv b/data-import/FILES_TEST.csv new file mode 100755 index 0000000..3c8524e --- /dev/null +++ b/data-import/FILES_TEST.csv @@ -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 diff --git a/data-import/PHONE_TEST.csv b/data-import/PHONE_TEST.csv new file mode 100755 index 0000000..c08a4be --- /dev/null +++ b/data-import/PHONE_TEST.csv @@ -0,0 +1 @@ +Id,Phone,Location \ No newline at end of file diff --git a/data-import/ROLODEX_TEST.csv b/data-import/ROLODEX_TEST.csv new file mode 100755 index 0000000..44c70bd --- /dev/null +++ b/data-import/ROLODEX_TEST.csv @@ -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 diff --git a/delphi.db b/delphi.db index 11b5fc3..8330bff 100644 Binary files a/delphi.db and b/delphi.db differ diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..3b57e71 --- /dev/null +++ b/docker-compose.yml @@ -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 +