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