From 2e2380552e9f2a0b14e34660546485bdd71a6e2e Mon Sep 17 00:00:00 2001 From: HotSwapp <47397945+HotSwapp@users.noreply.github.com> Date: Mon, 13 Oct 2025 14:04:35 -0500 Subject: [PATCH] Customer 360: extended Client fields, auto-migrate, updated Rolodex CRUD/templates, QDRO routes/views, importer mapping QDRO links appear in rolodex_view.html case rows and case.html header when QDRO data exists, matching legacy flows. --- app/database.py | 34 + app/main.py | 149 +- app/models.py | 11 + app/templates/case.html | 5 + app/templates/qdro.html | 75 + app/templates/rolodex_edit.html | 40 + app/templates/rolodex_view.html | 55 +- cookies.txt | 2 +- ...h_99872675-ea63-400e-974c-05b171b2f649.csv | 5239 ++ ...e_f5f08030-2f1d-42db-a09c-4fe191e33848.csv | 1436 + ...s_0bb74540-b18b-4caa-a379-bde4bd4ad098.csv | 1 + ...e_4698880b-d1d3-4fb4-a1b9-fe1bae7ff1fd.csv | 503 + ...e_df2a3838-fdf0-4d7d-86cf-07a8d858b0e4.csv | 503 + ...e_732429ec-8721-47f4-af62-5cf2c37b1150.csv | 418 + ...s_1ff67863-52c4-41dc-9125-dfc0a0a4b2c6.csv | 16660 +++++ ...s_abddd9da-4039-4c38-8145-a82538b991d4.csv | 16660 +++++ ...s_db2192d1-b57d-4daf-9f5e-3660adba6ada.csv | 26575 +++++++ ...p_0648b941-7c0d-41ad-ad7a-4f641bd3416c.csv | 88 + ...e_b12eb625-9e65-4bfe-a145-5dd81b7e1f80.csv | 6 + ...n_1843cdf7-59c6-4e98-8f40-6f5c561b680c.csv | 1202 + ...n_5288cb8b-ee88-459d-a2b4-c7fd428b9bd0.csv | 293 + ...n_668a8220-88ff-4183-ab54-16f7f850c342.csv | 1043 + ...n_70ecdf03-7e21-4ba8-8e8d-a7ec5ce58402.csv | 308 + ...n_868c3f7f-b3d7-48bc-90b5-459193a19854.csv | 102 + ...n_be7abf5a-cf9a-4c91-aa0f-48a223ca54a9.csv | 1202 + ...n_be7b9412-b559-4816-bd01-3077a5b9ee11.csv | 102 + ...n_f6d90518-e279-4b19-a2cd-8f17ab46be00.csv | 1 + delphi.db | Bin 21090304 -> 37187584 bytes phone_book.csv | 60509 ++++++++++++++++ phone_book_address.csv | 60509 ++++++++++++++++ rolodex.html | 332 + sync_result.html | 578 + 32 files changed, 194632 insertions(+), 9 deletions(-) create mode 100644 app/templates/qdro.html create mode 100644 data-import/pension_death_99872675-ea63-400e-974c-05b171b2f649.csv create mode 100644 data-import/pension_marriage_f5f08030-2f1d-42db-a09c-4fe191e33848.csv create mode 100644 data-import/pension_results_0bb74540-b18b-4caa-a379-bde4bd4ad098.csv create mode 100644 data-import/pension_schedule_4698880b-d1d3-4fb4-a1b9-fe1bae7ff1fd.csv create mode 100644 data-import/pension_schedule_df2a3838-fdf0-4d7d-86cf-07a8d858b0e4.csv create mode 100644 data-import/pension_separate_732429ec-8721-47f4-af62-5cf2c37b1150.csv create mode 100644 data-import/pensions_1ff67863-52c4-41dc-9125-dfc0a0a4b2c6.csv create mode 100644 data-import/pensions_abddd9da-4039-4c38-8145-a82538b991d4.csv create mode 100644 data-import/qdros_db2192d1-b57d-4daf-9f5e-3660adba6ada.csv create mode 100644 data-import/trnslkup_0648b941-7c0d-41ad-ad7a-4f641bd3416c.csv create mode 100644 data-import/trnstype_b12eb625-9e65-4bfe-a145-5dd81b7e1f80.csv create mode 100644 data-import/unknown_1843cdf7-59c6-4e98-8f40-6f5c561b680c.csv create mode 100644 data-import/unknown_5288cb8b-ee88-459d-a2b4-c7fd428b9bd0.csv create mode 100644 data-import/unknown_668a8220-88ff-4183-ab54-16f7f850c342.csv create mode 100644 data-import/unknown_70ecdf03-7e21-4ba8-8e8d-a7ec5ce58402.csv create mode 100644 data-import/unknown_868c3f7f-b3d7-48bc-90b5-459193a19854.csv create mode 100644 data-import/unknown_be7abf5a-cf9a-4c91-aa0f-48a223ca54a9.csv create mode 100644 data-import/unknown_be7b9412-b559-4816-bd01-3077a5b9ee11.csv create mode 100644 data-import/unknown_f6d90518-e279-4b19-a2cd-8f17ab46be00.csv create mode 100644 phone_book.csv create mode 100644 phone_book_address.csv create mode 100644 rolodex.html create mode 100644 sync_result.html diff --git a/app/database.py b/app/database.py index 61d4d42..e71fb2a 100644 --- a/app/database.py +++ b/app/database.py @@ -105,6 +105,40 @@ def create_tables() -> None: except Exception: pass + # Lightweight migration: ensure new client columns exist (SQLite safe) + try: + inspector = inspect(engine) + client_cols = {col['name'] for col in inspector.get_columns('clients')} + client_required_sql = { + 'prefix': 'ALTER TABLE clients ADD COLUMN prefix VARCHAR(20)', + 'middle_name': 'ALTER TABLE clients ADD COLUMN middle_name VARCHAR(50)', + 'suffix': 'ALTER TABLE clients ADD COLUMN suffix VARCHAR(20)', + 'title': 'ALTER TABLE clients ADD COLUMN title VARCHAR(100)', + 'group': 'ALTER TABLE clients ADD COLUMN "group" VARCHAR(50)', + 'email': 'ALTER TABLE clients ADD COLUMN email VARCHAR(255)', + 'dob': 'ALTER TABLE clients ADD COLUMN dob DATE', + 'ssn': 'ALTER TABLE clients ADD COLUMN ssn VARCHAR(20)', + 'legal_status': 'ALTER TABLE clients ADD COLUMN legal_status VARCHAR(50)', + 'memo': 'ALTER TABLE clients ADD COLUMN memo TEXT' + } + client_alters = [] + for col_name, ddl in client_required_sql.items(): + if col_name not in client_cols: + client_alters.append(ddl) + if client_alters: + with engine.begin() as conn: + for ddl in client_alters: + conn.execute(text(ddl)) + except Exception as e: + try: + from .logging_config import setup_logging + import structlog + setup_logging() + _logger = structlog.get_logger(__name__) + _logger.warning("sqlite_migration_clients_failed", error=str(e)) + except Exception: + pass + # Seed default admin user after creating tables try: from .auth import seed_admin_user diff --git a/app/main.py b/app/main.py index fe97d52..b0c44b3 100644 --- a/app/main.py +++ b/app/main.py @@ -29,7 +29,7 @@ import structlog from structlog import contextvars as structlog_contextvars from .database import create_tables, get_db, get_database_url -from .models import User, Case, Client, Phone, Transaction, Document, Payment, ImportLog +from .models import User, Case, Client, Phone, Transaction, Document, Payment, ImportLog, Qdros from .auth import authenticate_user, get_current_user_from_session from .reporting import ( build_phone_book_pdf, @@ -633,16 +633,37 @@ def import_rolodex_data(db: Session, file_path: str) -> Dict[str, Any]: result['errors'].append(f"Row {row_num}: Client with ID '{rolodex_id}' already exists") continue + # Parse DOB (YYYY-MM-DD or MM/DD/YY variants) + dob_raw = row.get('DOB', '').strip() + dob_val = None + for fmt in ("%Y-%m-%d", "%m/%d/%Y", "%m/%d/%y"): + if not dob_raw: + break + try: + dob_val = datetime.strptime(dob_raw, fmt).date() + break + except ValueError: + continue + client = Client( rolodex_id=rolodex_id, + prefix=row.get('Prefix', '').strip() or None, first_name=row.get('First', '').strip() or None, - middle_initial=row.get('Middle', '').strip() or None, + middle_name=row.get('Middle', '').strip() or None, last_name=row.get('Last', '').strip() or None, + suffix=row.get('Suffix', '').strip() or None, + title=row.get('Title', '').strip() or None, company=row.get('Title', '').strip() or None, address=row.get('A1', '').strip() or None, city=row.get('City', '').strip() or None, - state=row.get('St', '').strip() or None, - zip_code=row.get('Zip', '').strip() or None + state=(row.get('Abrev', '').strip() or row.get('St', '').strip() or None), + zip_code=row.get('Zip', '').strip() or None, + group=row.get('Group', '').strip() or None, + email=row.get('Email', '').strip() or None, + dob=dob_val, + ssn=row.get('SS#', '').strip() or None, + legal_status=row.get('Legal_Status', '').strip() or None, + memo=row.get('Memo', '').strip() or None, ) db.add(client) @@ -2298,6 +2319,9 @@ async def case_detail( logger.info("case_detail", case_id=case_obj.id, file_no=case_obj.file_no) + # Determine if QDRO entries exist for this case's file number + has_qdro = db.query(Qdros).filter(Qdros.file_no == case_obj.file_no).count() > 0 + # Get any errors from session and clear them errors = request.session.pop("case_update_errors", None) @@ -2322,6 +2346,7 @@ async def case_detail( "saved": saved, "errors": errors or [], "totals": totals, + "has_qdro": has_qdro, }, ) @@ -2658,13 +2683,23 @@ async def rolodex_view(client_id: int, request: Request, db: Session = Depends(g @app.post("/rolodex/create") async def rolodex_create( request: Request, + prefix: str = Form(None), first_name: str = Form(None), + middle_name: str = Form(None), last_name: str = Form(None), + suffix: str = Form(None), + title: str = Form(None), company: str = Form(None), address: str = Form(None), city: str = Form(None), state: str = Form(None), zip_code: str = Form(None), + group: str = Form(None), + email: str = Form(None), + dob: str = Form(None), + ssn: str = Form(None), + legal_status: str = Form(None), + memo: str = Form(None), rolodex_id: str = Form(None), db: Session = Depends(get_db), ): @@ -2672,14 +2707,32 @@ async def rolodex_create( if not user: return RedirectResponse(url="/login", status_code=302) + # Parse date + dob_dt = None + if dob and dob.strip(): + try: + dob_dt = datetime.strptime(dob.strip(), "%Y-%m-%d").date() + except ValueError: + dob_dt = None + client = Client( + prefix=(prefix or "").strip() or None, first_name=(first_name or "").strip() or None, + middle_name=(middle_name or "").strip() or None, last_name=(last_name or "").strip() or None, + suffix=(suffix or "").strip() or None, + title=(title or "").strip() or None, company=(company or "").strip() or None, address=(address or "").strip() or None, city=(city or "").strip() or None, state=(state or "").strip() or None, zip_code=(zip_code or "").strip() or None, + group=(group or "").strip() or None, + email=(email or "").strip() or None, + dob=dob_dt, + ssn=(ssn or "").strip() or None, + legal_status=(legal_status or "").strip() or None, + memo=(memo or "").strip() or None, rolodex_id=(rolodex_id or "").strip() or None, ) db.add(client) @@ -2693,13 +2746,23 @@ async def rolodex_create( async def rolodex_update( client_id: int, request: Request, + prefix: str = Form(None), first_name: str = Form(None), + middle_name: str = Form(None), last_name: str = Form(None), + suffix: str = Form(None), + title: str = Form(None), company: str = Form(None), address: str = Form(None), city: str = Form(None), state: str = Form(None), zip_code: str = Form(None), + group: str = Form(None), + email: str = Form(None), + dob: str = Form(None), + ssn: str = Form(None), + legal_status: str = Form(None), + memo: str = Form(None), rolodex_id: str = Form(None), db: Session = Depends(get_db), ): @@ -2711,13 +2774,27 @@ async def rolodex_update( if not client: raise HTTPException(status_code=404, detail="Client not found") + client.prefix = (prefix or "").strip() or None client.first_name = (first_name or "").strip() or None + client.middle_name = (middle_name or "").strip() or None client.last_name = (last_name or "").strip() or None + client.suffix = (suffix or "").strip() or None + client.title = (title or "").strip() or None client.company = (company or "").strip() or None client.address = (address or "").strip() or None client.city = (city or "").strip() or None client.state = (state or "").strip() or None client.zip_code = (zip_code or "").strip() or None + client.group = (group or "").strip() or None + client.email = (email or "").strip() or None + if dob and dob.strip(): + try: + client.dob = datetime.strptime(dob.strip(), "%Y-%m-%d").date() + except ValueError: + pass + client.ssn = (ssn or "").strip() or None + client.legal_status = (legal_status or "").strip() or None + client.memo = (memo or "").strip() or None client.rolodex_id = (rolodex_id or "").strip() or None db.commit() @@ -3625,3 +3702,67 @@ async def api_list_ledger( items=items, pagination=Pagination(page=page, page_size=page_size, total=total, total_pages=total_pages), ) + +# ------------------------------ +# QDRO Views +# ------------------------------ + +@app.get("/qdro/{file_no}") +async def qdro_versions( + request: Request, + file_no: str, + db: Session = Depends(get_db), +): + user = get_current_user_from_session(request.session) + if not user: + return RedirectResponse(url="/login", status_code=302) + + versions = ( + db.query(Qdros) + .filter(Qdros.file_no == file_no) + .order_by(Qdros.version.asc()) + .all() + ) + return templates.TemplateResponse( + "qdro.html", + { + "request": request, + "user": user, + "file_no": file_no, + "versions": versions, + "qdro": None, + }, + ) + + +@app.get("/qdro/{file_no}/{version}") +async def qdro_detail( + request: Request, + file_no: str, + version: str, + db: Session = Depends(get_db), +): + user = get_current_user_from_session(request.session) + if not user: + return RedirectResponse(url="/login", status_code=302) + + q = db.query(Qdros).filter(Qdros.file_no == file_no, Qdros.version == version).first() + if not q: + return RedirectResponse(url=f"/qdro/{file_no}", status_code=302) + + versions = ( + db.query(Qdros) + .filter(Qdros.file_no == file_no) + .order_by(Qdros.version.asc()) + .all() + ) + return templates.TemplateResponse( + "qdro.html", + { + "request": request, + "user": user, + "file_no": file_no, + "versions": versions, + "qdro": q, + }, + ) diff --git a/app/models.py b/app/models.py index bea9425..11a7202 100644 --- a/app/models.py +++ b/app/models.py @@ -42,9 +42,20 @@ class Client(Base): id = Column(Integer, primary_key=True, index=True) rolodex_id = Column(String(20), unique=True, index=True) + # Name and identity fields (modernized) + prefix = Column(String(20)) last_name = Column(String(50)) first_name = Column(String(50)) middle_initial = Column(String(10)) + middle_name = Column(String(50)) + suffix = Column(String(20)) # Jr, Sr, etc. + title = Column(String(100)) # Job/role title + group = Column(String(50)) # Legacy rolodex group + email = Column(String(255)) + dob = Column(Date) + ssn = Column(String(20)) + legal_status = Column(String(50)) + memo = Column(Text) company = Column(String(100)) address = Column(String(255)) city = Column(String(50)) diff --git a/app/templates/case.html b/app/templates/case.html index a142655..2e13dc7 100644 --- a/app/templates/case.html +++ b/app/templates/case.html @@ -130,6 +130,11 @@ Case {{ case.file_no if case else '' }} · Delphi Database {% endif %} + {% if has_qdro %} + + QDRO + + {% endif %}