Compare commits
4 Commits
d456ae4f39
...
c68ba45ceb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c68ba45ceb | ||
|
|
432f303a33 | ||
|
|
950d261eb4 | ||
|
|
f9c3b3cc9c |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
22
app/auth.py
22
app/auth.py
@@ -12,7 +12,8 @@ from .models import User
|
|||||||
from .database import SessionLocal
|
from .database import SessionLocal
|
||||||
|
|
||||||
# Configure password hashing context
|
# Configure password hashing context
|
||||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
# Prefer pbkdf2_sha256 for portability; include bcrypt for legacy compatibility
|
||||||
|
pwd_context = CryptContext(schemes=["pbkdf2_sha256", "bcrypt"], deprecated="auto")
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -140,7 +141,24 @@ def seed_admin_user() -> None:
|
|||||||
# Check if admin user already exists
|
# Check if admin user already exists
|
||||||
existing_admin = db.query(User).filter(User.username == admin_username).first()
|
existing_admin = db.query(User).filter(User.username == admin_username).first()
|
||||||
if existing_admin:
|
if existing_admin:
|
||||||
logger.info(f"Admin user '{admin_username}' already exists")
|
# Ensure default credentials work in development
|
||||||
|
needs_reset = False
|
||||||
|
try:
|
||||||
|
needs_reset = not verify_password(admin_password, existing_admin.password_hash)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Password verify failed for admin (will reset): {e}")
|
||||||
|
needs_reset = True
|
||||||
|
|
||||||
|
if needs_reset:
|
||||||
|
try:
|
||||||
|
existing_admin.password_hash = hash_password(admin_password)
|
||||||
|
db.add(existing_admin)
|
||||||
|
db.commit()
|
||||||
|
logger.info(f"Admin user '{admin_username}' password reset to default")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error updating admin password: {e}")
|
||||||
|
else:
|
||||||
|
logger.info(f"Admin user '{admin_username}' already exists")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Create admin user
|
# Create admin user
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ from sqlalchemy import create_engine
|
|||||||
from sqlalchemy.ext.declarative import declarative_base
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
from sqlalchemy.orm import sessionmaker, Session
|
from sqlalchemy.orm import sessionmaker, Session
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
|
from sqlalchemy import inspect, text
|
||||||
|
|
||||||
# Load environment variables from .env file
|
# Load environment variables from .env file
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
@@ -68,6 +69,42 @@ def create_tables() -> None:
|
|||||||
"""
|
"""
|
||||||
Base.metadata.create_all(bind=engine)
|
Base.metadata.create_all(bind=engine)
|
||||||
|
|
||||||
|
# Lightweight migration: ensure ledger-specific columns exist on transactions
|
||||||
|
try:
|
||||||
|
inspector = inspect(engine)
|
||||||
|
columns = {col['name'] for col in inspector.get_columns('transactions')}
|
||||||
|
|
||||||
|
migration_alters = []
|
||||||
|
# Map of column name to SQL for SQLite ALTER TABLE ADD COLUMN
|
||||||
|
required_columns_sql = {
|
||||||
|
'item_no': 'ALTER TABLE transactions ADD COLUMN item_no INTEGER',
|
||||||
|
'employee_number': 'ALTER TABLE transactions ADD COLUMN employee_number VARCHAR(20)',
|
||||||
|
't_code': 'ALTER TABLE transactions ADD COLUMN t_code VARCHAR(10)',
|
||||||
|
't_type_l': 'ALTER TABLE transactions ADD COLUMN t_type_l VARCHAR(1)',
|
||||||
|
'quantity': 'ALTER TABLE transactions ADD COLUMN quantity FLOAT',
|
||||||
|
'rate': 'ALTER TABLE transactions ADD COLUMN rate FLOAT',
|
||||||
|
'billed': 'ALTER TABLE transactions ADD COLUMN billed VARCHAR(1)'
|
||||||
|
}
|
||||||
|
|
||||||
|
for col_name, ddl in required_columns_sql.items():
|
||||||
|
if col_name not in columns:
|
||||||
|
migration_alters.append(ddl)
|
||||||
|
|
||||||
|
if migration_alters:
|
||||||
|
with engine.begin() as conn:
|
||||||
|
for ddl in migration_alters:
|
||||||
|
conn.execute(text(ddl))
|
||||||
|
except Exception as e:
|
||||||
|
# Log but do not fail startup; migrations are best-effort for SQLite
|
||||||
|
try:
|
||||||
|
from .logging_config import setup_logging
|
||||||
|
import structlog
|
||||||
|
setup_logging()
|
||||||
|
_logger = structlog.get_logger(__name__)
|
||||||
|
_logger.warning("sqlite_migration_failed", error=str(e))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
# Seed default admin user after creating tables
|
# Seed default admin user after creating tables
|
||||||
try:
|
try:
|
||||||
from .auth import seed_admin_user
|
from .auth import seed_admin_user
|
||||||
|
|||||||
723
app/main.py
723
app/main.py
@@ -16,13 +16,13 @@ from typing import Optional, List, Dict, Any
|
|||||||
from io import StringIO
|
from io import StringIO
|
||||||
|
|
||||||
from fastapi import FastAPI, Depends, Request, Query, HTTPException, UploadFile, File, Form
|
from fastapi import FastAPI, Depends, Request, Query, HTTPException, UploadFile, File, Form
|
||||||
from fastapi.responses import RedirectResponse
|
from fastapi.responses import RedirectResponse, Response
|
||||||
from starlette.middleware.sessions import SessionMiddleware
|
from starlette.middleware.sessions import SessionMiddleware
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
from fastapi.templating import Jinja2Templates
|
from fastapi.templating import Jinja2Templates
|
||||||
from sqlalchemy.orm import Session, joinedload
|
from sqlalchemy.orm import Session, joinedload
|
||||||
from sqlalchemy import or_
|
from sqlalchemy import or_, and_, func as sa_func
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
from starlette.middleware.base import BaseHTTPMiddleware
|
from starlette.middleware.base import BaseHTTPMiddleware
|
||||||
import structlog
|
import structlog
|
||||||
@@ -611,13 +611,39 @@ def import_ledger_data(db: Session, file_path: str) -> Dict[str, Any]:
|
|||||||
result['errors'].append(f"Row {row_num}: Invalid amount")
|
result['errors'].append(f"Row {row_num}: Invalid amount")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
tx_date = parse_date(row.get('Date', ''))
|
||||||
|
item_no = parse_int(row.get('Item_No', '') or '')
|
||||||
|
# ensure unique item_no per date by increment
|
||||||
|
# temp session-less check via while loop
|
||||||
|
desired_item_no = item_no if item_no is not None else 1
|
||||||
|
while True:
|
||||||
|
exists = (
|
||||||
|
db.query(Transaction)
|
||||||
|
.filter(
|
||||||
|
Transaction.case_id == case.id,
|
||||||
|
Transaction.transaction_date == tx_date,
|
||||||
|
Transaction.item_no == desired_item_no,
|
||||||
|
)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
if not exists:
|
||||||
|
break
|
||||||
|
desired_item_no += 1
|
||||||
|
|
||||||
transaction = Transaction(
|
transaction = Transaction(
|
||||||
case_id=case.id,
|
case_id=case.id,
|
||||||
transaction_date=parse_date(row.get('Date', '')),
|
transaction_date=tx_date,
|
||||||
transaction_type=row.get('T_Type', '').strip() or None,
|
transaction_type=(row.get('T_Type', '').strip() or None),
|
||||||
|
t_type_l=(row.get('T_Type_L', '').strip().upper() or None),
|
||||||
amount=amount,
|
amount=amount,
|
||||||
description=row.get('Note', '').strip() or None,
|
description=(row.get('Note', '').strip() or None),
|
||||||
reference=row.get('Item_No', '').strip() or None
|
reference=(row.get('Item_No', '').strip() or None),
|
||||||
|
item_no=desired_item_no,
|
||||||
|
employee_number=(row.get('Empl_Num', '').strip() or None),
|
||||||
|
t_code=(row.get('T_Code', '').strip().upper() or None),
|
||||||
|
quantity=parse_float(row.get('Quantity', '')),
|
||||||
|
rate=parse_float(row.get('Rate', '')),
|
||||||
|
billed=((row.get('Billed', '') or '').strip().upper() or None),
|
||||||
)
|
)
|
||||||
|
|
||||||
db.add(transaction)
|
db.add(transaction)
|
||||||
@@ -791,12 +817,277 @@ def process_csv_import(db: Session, import_type: str, file_path: str) -> Dict[st
|
|||||||
return import_func(db, file_path)
|
return import_func(db, file_path)
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------
|
||||||
|
# Ledger CRUD and helpers
|
||||||
|
# ------------------------------
|
||||||
|
|
||||||
|
def validate_ledger_fields(
|
||||||
|
*,
|
||||||
|
transaction_date: Optional[str],
|
||||||
|
t_code: Optional[str],
|
||||||
|
employee_number: Optional[str],
|
||||||
|
quantity: Optional[str],
|
||||||
|
rate: Optional[str],
|
||||||
|
amount: Optional[str],
|
||||||
|
billed: Optional[str],
|
||||||
|
) -> tuple[list[str], dict[str, Any]]:
|
||||||
|
"""Validate incoming ledger form fields and return (errors, parsed_values)."""
|
||||||
|
errors: list[str] = []
|
||||||
|
parsed: dict[str, Any] = {}
|
||||||
|
|
||||||
|
# Date
|
||||||
|
tx_dt = parse_date(transaction_date or "") if transaction_date is not None else None
|
||||||
|
if tx_dt is None:
|
||||||
|
errors.append("Date is required and must be valid")
|
||||||
|
else:
|
||||||
|
parsed["transaction_date"] = tx_dt
|
||||||
|
|
||||||
|
# T_Code
|
||||||
|
if t_code is None or not t_code.strip():
|
||||||
|
errors.append("T_Code is required")
|
||||||
|
else:
|
||||||
|
parsed["t_code"] = t_code.strip().upper()
|
||||||
|
|
||||||
|
# Employee number
|
||||||
|
if employee_number is None or not employee_number.strip():
|
||||||
|
errors.append("Empl_Num is required")
|
||||||
|
else:
|
||||||
|
parsed["employee_number"] = employee_number.strip()
|
||||||
|
|
||||||
|
# Quantity, Rate, Amount
|
||||||
|
qty = parse_float(quantity or "") if quantity is not None else None
|
||||||
|
rt = parse_float(rate or "") if rate is not None else None
|
||||||
|
amt = parse_float(amount or "") if amount is not None else None
|
||||||
|
|
||||||
|
if qty is not None:
|
||||||
|
parsed["quantity"] = qty
|
||||||
|
if rt is not None:
|
||||||
|
parsed["rate"] = rt
|
||||||
|
|
||||||
|
# Auto-compute amount if missing but quantity and rate present
|
||||||
|
if amt is None and qty is not None and rt is not None:
|
||||||
|
amt = round(qty * rt, 2)
|
||||||
|
if amt is None:
|
||||||
|
errors.append("Amount is required or derivable from Quantity × Rate")
|
||||||
|
else:
|
||||||
|
parsed["amount"] = amt
|
||||||
|
|
||||||
|
# Billed flag
|
||||||
|
billed_flag = (billed or "").strip().upper() if billed is not None else ""
|
||||||
|
if billed_flag not in ("Y", "N"):
|
||||||
|
errors.append("Billed must be 'Y' or 'N'")
|
||||||
|
else:
|
||||||
|
parsed["billed"] = billed_flag
|
||||||
|
|
||||||
|
return errors, parsed
|
||||||
|
|
||||||
|
|
||||||
|
def next_unique_item_no(db: Session, case_id: int, tx_date: datetime, desired_item_no: Optional[int]) -> int:
|
||||||
|
"""Ensure (transaction_date, item_no) uniqueness per case by incrementing if needed."""
|
||||||
|
# Start at provided item_no or at 1 if missing
|
||||||
|
item_no = int(desired_item_no) if desired_item_no is not None else 1
|
||||||
|
while True:
|
||||||
|
exists = (
|
||||||
|
db.query(Transaction)
|
||||||
|
.filter(
|
||||||
|
Transaction.case_id == case_id,
|
||||||
|
Transaction.transaction_date == tx_date,
|
||||||
|
Transaction.item_no == item_no,
|
||||||
|
)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
if not exists:
|
||||||
|
return item_no
|
||||||
|
item_no += 1
|
||||||
|
|
||||||
|
|
||||||
|
def compute_case_totals_from_case(case_obj: Case) -> Dict[str, float]:
|
||||||
|
"""
|
||||||
|
Compute simple totals for a case from its transactions.
|
||||||
|
|
||||||
|
Returns billed, unbilled, and total sums. Amounts are treated as positive;
|
||||||
|
future enhancement could apply sign based on t_type_l.
|
||||||
|
"""
|
||||||
|
billed_total = 0.0
|
||||||
|
unbilled_total = 0.0
|
||||||
|
overall_total = 0.0
|
||||||
|
|
||||||
|
for t in (case_obj.transactions or []):
|
||||||
|
amt = float(t.amount) if t.amount is not None else 0.0
|
||||||
|
overall_total += amt
|
||||||
|
if (t.billed or '').upper() == 'Y':
|
||||||
|
billed_total += amt
|
||||||
|
else:
|
||||||
|
unbilled_total += amt
|
||||||
|
|
||||||
|
return {
|
||||||
|
'billed_total': round(billed_total, 2),
|
||||||
|
'unbilled_total': round(unbilled_total, 2),
|
||||||
|
'overall_total': round(overall_total, 2),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/case/{case_id}/ledger")
|
||||||
|
async def ledger_create(
|
||||||
|
request: Request,
|
||||||
|
case_id: int,
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
user = get_current_user_from_session(request.session)
|
||||||
|
if not user:
|
||||||
|
return RedirectResponse(url="/login", status_code=302)
|
||||||
|
|
||||||
|
form = await request.form()
|
||||||
|
|
||||||
|
# Validate
|
||||||
|
errors, parsed = validate_ledger_fields(
|
||||||
|
transaction_date=form.get("transaction_date"),
|
||||||
|
t_code=form.get("t_code"),
|
||||||
|
employee_number=form.get("employee_number"),
|
||||||
|
quantity=form.get("quantity"),
|
||||||
|
rate=form.get("rate"),
|
||||||
|
amount=form.get("amount"),
|
||||||
|
billed=form.get("billed"),
|
||||||
|
)
|
||||||
|
|
||||||
|
if errors:
|
||||||
|
request.session["case_update_errors"] = errors
|
||||||
|
return RedirectResponse(url=f"/case/{case_id}", status_code=302)
|
||||||
|
|
||||||
|
# Ensure case exists
|
||||||
|
case_obj = db.query(Case).filter(Case.id == case_id).first()
|
||||||
|
if not case_obj:
|
||||||
|
request.session["case_update_errors"] = ["Case not found"]
|
||||||
|
return RedirectResponse(url=f"/case/{case_id}", status_code=302)
|
||||||
|
|
||||||
|
# Assign optional fields
|
||||||
|
t_type = (form.get("transaction_type") or "").strip() or None
|
||||||
|
t_type_l = (form.get("t_type_l") or "").strip().upper() or None
|
||||||
|
reference = (form.get("reference") or "").strip() or None
|
||||||
|
desc = (form.get("description") or "").strip() or None
|
||||||
|
|
||||||
|
desired_item_no = parse_int(form.get("item_no") or "")
|
||||||
|
item_no = next_unique_item_no(db, case_id, parsed["transaction_date"], desired_item_no)
|
||||||
|
|
||||||
|
try:
|
||||||
|
tx = Transaction(
|
||||||
|
case_id=case_id,
|
||||||
|
transaction_date=parsed["transaction_date"],
|
||||||
|
transaction_type=t_type,
|
||||||
|
t_type_l=t_type_l,
|
||||||
|
amount=parsed["amount"],
|
||||||
|
description=desc,
|
||||||
|
reference=reference,
|
||||||
|
item_no=item_no,
|
||||||
|
employee_number=parsed["employee_number"],
|
||||||
|
t_code=parsed["t_code"],
|
||||||
|
quantity=parsed.get("quantity"),
|
||||||
|
rate=parsed.get("rate"),
|
||||||
|
billed=parsed["billed"],
|
||||||
|
)
|
||||||
|
db.add(tx)
|
||||||
|
db.commit()
|
||||||
|
logger.info("ledger_create", case_id=case_id, transaction_id=tx.id)
|
||||||
|
return RedirectResponse(url=f"/case/{case_id}?saved=1", status_code=302)
|
||||||
|
except Exception as e:
|
||||||
|
db.rollback()
|
||||||
|
logger.error("ledger_create_failed", case_id=case_id, error=str(e))
|
||||||
|
request.session["case_update_errors"] = ["Failed to create ledger entry"]
|
||||||
|
return RedirectResponse(url=f"/case/{case_id}", status_code=302)
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/case/{case_id}/ledger/{tx_id}")
|
||||||
|
async def ledger_update(
|
||||||
|
request: Request,
|
||||||
|
case_id: int,
|
||||||
|
tx_id: int,
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
user = get_current_user_from_session(request.session)
|
||||||
|
if not user:
|
||||||
|
return RedirectResponse(url="/login", status_code=302)
|
||||||
|
|
||||||
|
form = await request.form()
|
||||||
|
|
||||||
|
tx = db.query(Transaction).filter(Transaction.id == tx_id, Transaction.case_id == case_id).first()
|
||||||
|
if not tx:
|
||||||
|
request.session["case_update_errors"] = ["Ledger entry not found"]
|
||||||
|
return RedirectResponse(url=f"/case/{case_id}", status_code=302)
|
||||||
|
|
||||||
|
errors, parsed = validate_ledger_fields(
|
||||||
|
transaction_date=form.get("transaction_date"),
|
||||||
|
t_code=form.get("t_code"),
|
||||||
|
employee_number=form.get("employee_number"),
|
||||||
|
quantity=form.get("quantity"),
|
||||||
|
rate=form.get("rate"),
|
||||||
|
amount=form.get("amount"),
|
||||||
|
billed=form.get("billed"),
|
||||||
|
)
|
||||||
|
if errors:
|
||||||
|
request.session["case_update_errors"] = errors
|
||||||
|
return RedirectResponse(url=f"/case/{case_id}", status_code=302)
|
||||||
|
|
||||||
|
try:
|
||||||
|
tx.transaction_date = parsed["transaction_date"]
|
||||||
|
# Ensure uniqueness of (date, item_no)
|
||||||
|
desired_item_no = parse_int(form.get("item_no") or "") or tx.item_no
|
||||||
|
tx.item_no = next_unique_item_no(db, case_id, parsed["transaction_date"], desired_item_no)
|
||||||
|
|
||||||
|
tx.t_code = parsed["t_code"]
|
||||||
|
tx.employee_number = parsed["employee_number"]
|
||||||
|
tx.quantity = parsed.get("quantity")
|
||||||
|
tx.rate = parsed.get("rate")
|
||||||
|
tx.amount = parsed["amount"]
|
||||||
|
tx.billed = parsed["billed"]
|
||||||
|
tx.transaction_type = (form.get("transaction_type") or "").strip() or None
|
||||||
|
tx.t_type_l = (form.get("t_type_l") or "").strip().upper() or None
|
||||||
|
tx.reference = (form.get("reference") or "").strip() or None
|
||||||
|
tx.description = (form.get("description") or "").strip() or None
|
||||||
|
|
||||||
|
db.commit()
|
||||||
|
logger.info("ledger_update", case_id=case_id, transaction_id=tx.id)
|
||||||
|
return RedirectResponse(url=f"/case/{case_id}?saved=1", status_code=302)
|
||||||
|
except Exception as e:
|
||||||
|
db.rollback()
|
||||||
|
logger.error("ledger_update_failed", case_id=case_id, tx_id=tx_id, error=str(e))
|
||||||
|
request.session["case_update_errors"] = ["Failed to update ledger entry"]
|
||||||
|
return RedirectResponse(url=f"/case/{case_id}", status_code=302)
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/case/{case_id}/ledger/{tx_id}/delete")
|
||||||
|
async def ledger_delete(
|
||||||
|
request: Request,
|
||||||
|
case_id: int,
|
||||||
|
tx_id: int,
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
user = get_current_user_from_session(request.session)
|
||||||
|
if not user:
|
||||||
|
return RedirectResponse(url="/login", status_code=302)
|
||||||
|
|
||||||
|
tx = db.query(Transaction).filter(Transaction.id == tx_id, Transaction.case_id == case_id).first()
|
||||||
|
if not tx:
|
||||||
|
request.session["case_update_errors"] = ["Ledger entry not found"]
|
||||||
|
return RedirectResponse(url=f"/case/{case_id}", status_code=302)
|
||||||
|
|
||||||
|
try:
|
||||||
|
db.delete(tx)
|
||||||
|
db.commit()
|
||||||
|
logger.info("ledger_delete", case_id=case_id, transaction_id=tx_id)
|
||||||
|
return RedirectResponse(url=f"/case/{case_id}?saved=1", status_code=302)
|
||||||
|
except Exception as e:
|
||||||
|
db.rollback()
|
||||||
|
logger.error("ledger_delete_failed", case_id=case_id, tx_id=tx_id, error=str(e))
|
||||||
|
request.session["case_update_errors"] = ["Failed to delete ledger entry"]
|
||||||
|
return RedirectResponse(url=f"/case/{case_id}", status_code=302)
|
||||||
|
|
||||||
|
|
||||||
@app.get("/")
|
@app.get("/")
|
||||||
async def root():
|
async def root():
|
||||||
"""
|
"""
|
||||||
Root endpoint - health check.
|
Root endpoint - serves login form for web interface.
|
||||||
"""
|
"""
|
||||||
return {"message": "Delphi Database API is running"}
|
return RedirectResponse(url="/login", status_code=302)
|
||||||
|
|
||||||
|
|
||||||
@app.get("/health")
|
@app.get("/health")
|
||||||
@@ -1277,6 +1568,18 @@ async def case_detail(
|
|||||||
# Get any errors from session and clear them
|
# Get any errors from session and clear them
|
||||||
errors = request.session.pop("case_update_errors", None)
|
errors = request.session.pop("case_update_errors", None)
|
||||||
|
|
||||||
|
# Sort transactions by date then item_no for stable display
|
||||||
|
sorted_transactions = sorted(
|
||||||
|
case_obj.transactions or [],
|
||||||
|
key=lambda t: (
|
||||||
|
t.transaction_date or datetime.min,
|
||||||
|
t.item_no or 0,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
case_obj.transactions = sorted_transactions
|
||||||
|
|
||||||
|
totals = compute_case_totals_from_case(case_obj)
|
||||||
|
|
||||||
return templates.TemplateResponse(
|
return templates.TemplateResponse(
|
||||||
"case.html",
|
"case.html",
|
||||||
{
|
{
|
||||||
@@ -1285,6 +1588,7 @@ async def case_detail(
|
|||||||
"case": case_obj,
|
"case": case_obj,
|
||||||
"saved": saved,
|
"saved": saved,
|
||||||
"errors": errors or [],
|
"errors": errors or [],
|
||||||
|
"totals": totals,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1478,3 +1782,406 @@ async def case_reopen(
|
|||||||
request.session["case_update_errors"] = ["Failed to reopen case. Please try again."]
|
request.session["case_update_errors"] = ["Failed to reopen case. Please try again."]
|
||||||
|
|
||||||
return RedirectResponse(url=f"/case/{case_id}", status_code=302)
|
return RedirectResponse(url=f"/case/{case_id}", status_code=302)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/rolodex")
|
||||||
|
async def rolodex_list(
|
||||||
|
request: Request,
|
||||||
|
q: str | None = Query(None, description="Search by name or company"),
|
||||||
|
phone: str | None = Query(None, description="Search by phone contains"),
|
||||||
|
page: int = Query(1, ge=1, description="Page number (1-indexed)"),
|
||||||
|
page_size: int = Query(20, ge=1, le=100, description="Results per page"),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Rolodex list with simple search and pagination.
|
||||||
|
|
||||||
|
Filters clients by name/company and optional phone substring.
|
||||||
|
"""
|
||||||
|
user = get_current_user_from_session(request.session)
|
||||||
|
if not user:
|
||||||
|
return RedirectResponse(url="/login", status_code=302)
|
||||||
|
|
||||||
|
# Eager-load phones to avoid N+1 in template
|
||||||
|
query = db.query(Client).options(joinedload(Client.phones))
|
||||||
|
|
||||||
|
if q:
|
||||||
|
like = f"%{q}%"
|
||||||
|
query = query.filter(
|
||||||
|
or_(
|
||||||
|
Client.first_name.ilike(like),
|
||||||
|
Client.last_name.ilike(like),
|
||||||
|
Client.company.ilike(like),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if phone:
|
||||||
|
like_phone = f"%{phone}%"
|
||||||
|
# Use EXISTS over join to avoid duplicate rows
|
||||||
|
query = query.filter(Client.phones.any(Phone.phone_number.ilike(like_phone)))
|
||||||
|
|
||||||
|
# Order by last then first for stable display
|
||||||
|
query = query.order_by(Client.last_name.asc().nulls_last(), Client.first_name.asc().nulls_last())
|
||||||
|
|
||||||
|
total: int = query.count()
|
||||||
|
total_pages: int = (total + page_size - 1) // page_size if total > 0 else 1
|
||||||
|
if page > total_pages:
|
||||||
|
page = total_pages
|
||||||
|
|
||||||
|
offset = (page - 1) * page_size
|
||||||
|
clients = query.offset(offset).limit(page_size).all()
|
||||||
|
|
||||||
|
start_page = max(1, page - 2)
|
||||||
|
end_page = min(total_pages, page + 2)
|
||||||
|
page_numbers = list(range(start_page, end_page + 1))
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"rolodex_render",
|
||||||
|
query=q,
|
||||||
|
phone=phone,
|
||||||
|
page=page,
|
||||||
|
page_size=page_size,
|
||||||
|
total=total,
|
||||||
|
)
|
||||||
|
|
||||||
|
return templates.TemplateResponse(
|
||||||
|
"rolodex.html",
|
||||||
|
{
|
||||||
|
"request": request,
|
||||||
|
"user": user,
|
||||||
|
"clients": clients,
|
||||||
|
"q": q,
|
||||||
|
"phone": phone,
|
||||||
|
"page": page,
|
||||||
|
"page_size": page_size,
|
||||||
|
"total": total,
|
||||||
|
"total_pages": total_pages,
|
||||||
|
"page_numbers": page_numbers,
|
||||||
|
"start_index": (offset + 1) if total > 0 else 0,
|
||||||
|
"end_index": min(offset + len(clients), total),
|
||||||
|
"enable_bulk": True,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/rolodex/new")
|
||||||
|
async def rolodex_new(request: Request):
|
||||||
|
user = get_current_user_from_session(request.session)
|
||||||
|
if not user:
|
||||||
|
return RedirectResponse(url="/login", status_code=302)
|
||||||
|
|
||||||
|
return templates.TemplateResponse("rolodex_edit.html", {"request": request, "user": user, "client": None})
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/rolodex/{client_id}")
|
||||||
|
async def rolodex_view(client_id: int, request: Request, db: Session = Depends(get_db)):
|
||||||
|
user = get_current_user_from_session(request.session)
|
||||||
|
if not user:
|
||||||
|
return RedirectResponse(url="/login", status_code=302)
|
||||||
|
|
||||||
|
client = (
|
||||||
|
db.query(Client)
|
||||||
|
.options(joinedload(Client.phones), joinedload(Client.cases))
|
||||||
|
.filter(Client.id == client_id)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
if not client:
|
||||||
|
raise HTTPException(status_code=404, detail="Client not found")
|
||||||
|
|
||||||
|
return templates.TemplateResponse("rolodex_view.html", {"request": request, "user": user, "client": client})
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/rolodex/create")
|
||||||
|
async def rolodex_create(
|
||||||
|
request: Request,
|
||||||
|
first_name: str = Form(None),
|
||||||
|
last_name: str = Form(None),
|
||||||
|
company: str = Form(None),
|
||||||
|
address: str = Form(None),
|
||||||
|
city: str = Form(None),
|
||||||
|
state: str = Form(None),
|
||||||
|
zip_code: str = Form(None),
|
||||||
|
rolodex_id: str = Form(None),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
user = get_current_user_from_session(request.session)
|
||||||
|
if not user:
|
||||||
|
return RedirectResponse(url="/login", status_code=302)
|
||||||
|
|
||||||
|
client = Client(
|
||||||
|
first_name=(first_name or "").strip() or None,
|
||||||
|
last_name=(last_name 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,
|
||||||
|
rolodex_id=(rolodex_id or "").strip() or None,
|
||||||
|
)
|
||||||
|
db.add(client)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(client)
|
||||||
|
logger.info("rolodex_create", client_id=client.id, rolodex_id=client.rolodex_id)
|
||||||
|
return RedirectResponse(url=f"/rolodex/{client.id}", status_code=302)
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/rolodex/{client_id}/update")
|
||||||
|
async def rolodex_update(
|
||||||
|
client_id: int,
|
||||||
|
request: Request,
|
||||||
|
first_name: str = Form(None),
|
||||||
|
last_name: str = Form(None),
|
||||||
|
company: str = Form(None),
|
||||||
|
address: str = Form(None),
|
||||||
|
city: str = Form(None),
|
||||||
|
state: str = Form(None),
|
||||||
|
zip_code: str = Form(None),
|
||||||
|
rolodex_id: str = Form(None),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
user = get_current_user_from_session(request.session)
|
||||||
|
if not user:
|
||||||
|
return RedirectResponse(url="/login", status_code=302)
|
||||||
|
|
||||||
|
client = db.query(Client).filter(Client.id == client_id).first()
|
||||||
|
if not client:
|
||||||
|
raise HTTPException(status_code=404, detail="Client not found")
|
||||||
|
|
||||||
|
client.first_name = (first_name or "").strip() or None
|
||||||
|
client.last_name = (last_name 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.rolodex_id = (rolodex_id or "").strip() or None
|
||||||
|
|
||||||
|
db.commit()
|
||||||
|
logger.info(
|
||||||
|
"rolodex_update",
|
||||||
|
client_id=client.id,
|
||||||
|
fields={
|
||||||
|
"first_name": client.first_name,
|
||||||
|
"last_name": client.last_name,
|
||||||
|
"company": client.company,
|
||||||
|
"rolodex_id": client.rolodex_id,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return RedirectResponse(url=f"/rolodex/{client.id}", status_code=302)
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/rolodex/{client_id}/delete")
|
||||||
|
async def rolodex_delete(client_id: int, request: Request, db: Session = Depends(get_db)):
|
||||||
|
user = get_current_user_from_session(request.session)
|
||||||
|
if not user:
|
||||||
|
return RedirectResponse(url="/login", status_code=302)
|
||||||
|
|
||||||
|
client = db.query(Client).filter(Client.id == client_id).first()
|
||||||
|
if not client:
|
||||||
|
raise HTTPException(status_code=404, detail="Client not found")
|
||||||
|
|
||||||
|
db.delete(client)
|
||||||
|
db.commit()
|
||||||
|
logger.info("rolodex_delete", client_id=client_id)
|
||||||
|
return RedirectResponse(url="/rolodex", status_code=302)
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/rolodex/{client_id}/phone/add")
|
||||||
|
async def rolodex_add_phone(
|
||||||
|
client_id: int,
|
||||||
|
request: Request,
|
||||||
|
phone_number: str = Form(...),
|
||||||
|
phone_type: str = Form(None),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
user = get_current_user_from_session(request.session)
|
||||||
|
if not user:
|
||||||
|
return RedirectResponse(url="/login", status_code=302)
|
||||||
|
|
||||||
|
client = db.query(Client).filter(Client.id == client_id).first()
|
||||||
|
if not client:
|
||||||
|
raise HTTPException(status_code=404, detail="Client not found")
|
||||||
|
|
||||||
|
phone = Phone(
|
||||||
|
client_id=client.id,
|
||||||
|
phone_number=(phone_number or "").strip(),
|
||||||
|
phone_type=(phone_type or "").strip() or None,
|
||||||
|
)
|
||||||
|
db.add(phone)
|
||||||
|
db.commit()
|
||||||
|
logger.info("rolodex_phone_add", client_id=client.id, phone_id=phone.id, number=phone.phone_number)
|
||||||
|
return RedirectResponse(url=f"/rolodex/{client.id}", status_code=302)
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/rolodex/{client_id}/phone/{phone_id}/delete")
|
||||||
|
async def rolodex_delete_phone(client_id: int, phone_id: int, request: Request, db: Session = Depends(get_db)):
|
||||||
|
user = get_current_user_from_session(request.session)
|
||||||
|
if not user:
|
||||||
|
return RedirectResponse(url="/login", status_code=302)
|
||||||
|
|
||||||
|
phone = db.query(Phone).filter(Phone.id == phone_id, Phone.client_id == client_id).first()
|
||||||
|
if not phone:
|
||||||
|
raise HTTPException(status_code=404, detail="Phone not found")
|
||||||
|
|
||||||
|
db.delete(phone)
|
||||||
|
db.commit()
|
||||||
|
logger.info("rolodex_phone_delete", client_id=client_id, phone_id=phone_id)
|
||||||
|
return RedirectResponse(url=f"/rolodex/{client_id}", status_code=302)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/payments")
|
||||||
|
async def payments_search(
|
||||||
|
request: Request,
|
||||||
|
from_date: str | None = Query(None, description="YYYY-MM-DD"),
|
||||||
|
to_date: str | None = Query(None, description="YYYY-MM-DD"),
|
||||||
|
file_no: str | None = Query(None, description="Case file number"),
|
||||||
|
rolodex_id: str | None = Query(None, description="Legacy client Id"),
|
||||||
|
q: str | None = Query(None, description="Description contains"),
|
||||||
|
page: int = Query(1, ge=1),
|
||||||
|
page_size: int = Query(50, ge=1, le=200),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
user = get_current_user_from_session(request.session)
|
||||||
|
if not user:
|
||||||
|
return RedirectResponse(url="/login", status_code=302)
|
||||||
|
|
||||||
|
query = (
|
||||||
|
db.query(Payment)
|
||||||
|
.join(Case, Payment.case_id == Case.id)
|
||||||
|
.join(Client, Case.client_id == Client.id)
|
||||||
|
.order_by(Payment.payment_date.desc().nulls_last(), Payment.id.desc())
|
||||||
|
)
|
||||||
|
|
||||||
|
filters = []
|
||||||
|
if from_date:
|
||||||
|
try:
|
||||||
|
dt = datetime.strptime(from_date, "%Y-%m-%d")
|
||||||
|
filters.append(Payment.payment_date >= dt)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
if to_date:
|
||||||
|
try:
|
||||||
|
dt = datetime.strptime(to_date, "%Y-%m-%d")
|
||||||
|
filters.append(Payment.payment_date <= dt)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
if file_no:
|
||||||
|
filters.append(Case.file_no.ilike(f"%{file_no}%"))
|
||||||
|
if rolodex_id:
|
||||||
|
filters.append(Client.rolodex_id.ilike(f"%{rolodex_id}%"))
|
||||||
|
if q:
|
||||||
|
filters.append(Payment.description.ilike(f"%{q}%"))
|
||||||
|
|
||||||
|
if filters:
|
||||||
|
query = query.filter(and_(*filters))
|
||||||
|
|
||||||
|
total = query.count()
|
||||||
|
total_pages = (total + page_size - 1) // page_size if total > 0 else 1
|
||||||
|
if page > total_pages:
|
||||||
|
page = total_pages
|
||||||
|
offset = (page - 1) * page_size
|
||||||
|
payments = query.offset(offset).limit(page_size).all()
|
||||||
|
|
||||||
|
# Totals for current result page
|
||||||
|
page_total_amount = sum(p.amount or 0 for p in payments)
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"payments_render",
|
||||||
|
from_date=from_date,
|
||||||
|
to_date=to_date,
|
||||||
|
file_no=file_no,
|
||||||
|
rolodex_id=rolodex_id,
|
||||||
|
q=q,
|
||||||
|
total=total,
|
||||||
|
)
|
||||||
|
|
||||||
|
return templates.TemplateResponse(
|
||||||
|
"payments.html",
|
||||||
|
{
|
||||||
|
"request": request,
|
||||||
|
"user": user,
|
||||||
|
"payments": payments,
|
||||||
|
"from_date": from_date,
|
||||||
|
"to_date": to_date,
|
||||||
|
"file_no": file_no,
|
||||||
|
"rolodex_id": rolodex_id,
|
||||||
|
"q": q,
|
||||||
|
"page": page,
|
||||||
|
"page_size": page_size,
|
||||||
|
"total": total,
|
||||||
|
"total_pages": total_pages,
|
||||||
|
"start_index": (offset + 1) if total > 0 else 0,
|
||||||
|
"end_index": min(offset + len(payments), total),
|
||||||
|
"page_total_amount": page_total_amount,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/reports/phone-book")
|
||||||
|
async def phone_book_report_post(request: Request):
|
||||||
|
"""Accepts selected client IDs from forms and redirects to GET for rendering."""
|
||||||
|
user = get_current_user_from_session(request.session)
|
||||||
|
if not user:
|
||||||
|
return RedirectResponse(url="/login", status_code=302)
|
||||||
|
|
||||||
|
form = await request.form()
|
||||||
|
client_ids = form.getlist("client_ids")
|
||||||
|
if not client_ids:
|
||||||
|
return RedirectResponse(url="/rolodex", status_code=302)
|
||||||
|
|
||||||
|
ids_param = "&".join([f"client_ids={cid}" for cid in client_ids])
|
||||||
|
return RedirectResponse(url=f"/reports/phone-book?{ids_param}", status_code=302)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/reports/phone-book")
|
||||||
|
async def phone_book_report(
|
||||||
|
request: Request,
|
||||||
|
client_ids: List[int] | None = Query(None),
|
||||||
|
q: str | None = Query(None, description="Filter by name/company"),
|
||||||
|
format: str | None = Query(None, description="csv for CSV output"),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
user = get_current_user_from_session(request.session)
|
||||||
|
if not user:
|
||||||
|
return RedirectResponse(url="/login", status_code=302)
|
||||||
|
|
||||||
|
query = db.query(Client).options(joinedload(Client.phones))
|
||||||
|
if client_ids:
|
||||||
|
query = query.filter(Client.id.in_(client_ids))
|
||||||
|
elif q:
|
||||||
|
like = f"%{q}%"
|
||||||
|
query = query.filter(
|
||||||
|
or_(Client.first_name.ilike(like), Client.last_name.ilike(like), Client.company.ilike(like))
|
||||||
|
)
|
||||||
|
|
||||||
|
clients = query.order_by(Client.last_name.asc().nulls_last(), Client.first_name.asc().nulls_last()).all()
|
||||||
|
|
||||||
|
if format == "csv":
|
||||||
|
# Build CSV output
|
||||||
|
output = StringIO()
|
||||||
|
writer = csv.writer(output)
|
||||||
|
writer.writerow(["Last", "First", "Company", "Phone Type", "Phone Number"])
|
||||||
|
for c in clients:
|
||||||
|
if c.phones:
|
||||||
|
for p in c.phones:
|
||||||
|
writer.writerow([
|
||||||
|
c.last_name or "",
|
||||||
|
c.first_name or "",
|
||||||
|
c.company or "",
|
||||||
|
p.phone_type or "",
|
||||||
|
p.phone_number or "",
|
||||||
|
])
|
||||||
|
else:
|
||||||
|
writer.writerow([c.last_name or "", c.first_name or "", c.company or "", "", ""])
|
||||||
|
csv_bytes = output.getvalue().encode("utf-8")
|
||||||
|
return Response(
|
||||||
|
content=csv_bytes,
|
||||||
|
media_type="text/csv",
|
||||||
|
headers={"Content-Disposition": "attachment; filename=phone_book.csv"},
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info("phone_book_render", count=len(clients))
|
||||||
|
return templates.TemplateResponse(
|
||||||
|
"report_phone_book.html",
|
||||||
|
{"request": request, "user": user, "clients": clients, "q": q, "client_ids": client_ids or []},
|
||||||
|
)
|
||||||
|
|||||||
483
app/models.py
483
app/models.py
@@ -4,7 +4,7 @@ SQLAlchemy models for the Delphi database.
|
|||||||
All models inherit from Base which is configured in the database module.
|
All models inherit from Base which is configured in the database module.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, Float, Text, Boolean
|
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, Float, Text, Boolean, Date, Numeric, Index, UniqueConstraint, ForeignKeyConstraint
|
||||||
from sqlalchemy.orm import relationship
|
from sqlalchemy.orm import relationship
|
||||||
from sqlalchemy.ext.declarative import declarative_base
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
from sqlalchemy.sql import func
|
from sqlalchemy.sql import func
|
||||||
@@ -123,10 +123,20 @@ class Transaction(Base):
|
|||||||
id = Column(Integer, primary_key=True, index=True)
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
case_id = Column(Integer, ForeignKey("cases.id"), nullable=False)
|
case_id = Column(Integer, ForeignKey("cases.id"), nullable=False)
|
||||||
transaction_date = Column(DateTime(timezone=True))
|
transaction_date = Column(DateTime(timezone=True))
|
||||||
transaction_type = Column(String(20))
|
# Legacy/basic fields
|
||||||
|
transaction_type = Column(String(20)) # Maps to legacy T_Type
|
||||||
amount = Column(Float)
|
amount = Column(Float)
|
||||||
description = Column(Text)
|
description = Column(Text) # Maps to legacy Note
|
||||||
reference = Column(String(50))
|
reference = Column(String(50)) # Previously used for Item_No
|
||||||
|
|
||||||
|
# Ledger-specific fields (added for File Cabinet MVP)
|
||||||
|
item_no = Column(Integer)
|
||||||
|
employee_number = Column(String(20)) # Empl_Num
|
||||||
|
t_code = Column(String(10)) # T_Code
|
||||||
|
t_type_l = Column(String(1)) # T_Type_L (Credit/Debit marker)
|
||||||
|
quantity = Column(Float)
|
||||||
|
rate = Column(Float)
|
||||||
|
billed = Column(String(1)) # 'Y' or 'N'
|
||||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||||
|
|
||||||
# Relationships
|
# Relationships
|
||||||
@@ -208,3 +218,468 @@ class ImportLog(Base):
|
|||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<ImportLog(id={self.id}, type='{self.import_type}', status='{self.status}')>"
|
return f"<ImportLog(id={self.id}, type='{self.import_type}', status='{self.status}')>"
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------
|
||||||
|
# Legacy schema models (read-only/migration support)
|
||||||
|
# Derived from docs/legacy-schema.md
|
||||||
|
# -----------------------------
|
||||||
|
|
||||||
|
|
||||||
|
class Rolodex(Base):
|
||||||
|
"""
|
||||||
|
Legacy ROLODEX master table.
|
||||||
|
|
||||||
|
Primary key is the human-readable `Id`.
|
||||||
|
"""
|
||||||
|
__tablename__ = "rolodex"
|
||||||
|
|
||||||
|
id = Column(String, primary_key=True, index=True) # Id (TEXT)
|
||||||
|
prefix = Column(String)
|
||||||
|
first = Column(String)
|
||||||
|
middle = Column(String)
|
||||||
|
last = Column(String, index=True)
|
||||||
|
suffix = Column(String)
|
||||||
|
title = Column(String)
|
||||||
|
a1 = Column(String)
|
||||||
|
a2 = Column(String)
|
||||||
|
a3 = Column(String)
|
||||||
|
city = Column(String)
|
||||||
|
abrev = Column(String(2)) # state abbreviation
|
||||||
|
st = Column(String) # state name
|
||||||
|
zip = Column(String(10))
|
||||||
|
email = Column(String)
|
||||||
|
dob = Column(Date)
|
||||||
|
ss = Column(String) # SS#
|
||||||
|
legal_status = Column(String)
|
||||||
|
group = Column(String)
|
||||||
|
memo = Column(Text)
|
||||||
|
|
||||||
|
__table_args__ = (
|
||||||
|
Index("ix_rolodex_last_first", "last", "first"),
|
||||||
|
Index("ix_rolodex_group", "group"),
|
||||||
|
Index("ix_rolodex_email", "email"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class LegacyPhone(Base):
|
||||||
|
"""
|
||||||
|
Legacy PHONE table (phones by Rolodex Id).
|
||||||
|
|
||||||
|
Composite PK on (id, phone) to prevent duplicates per person.
|
||||||
|
"""
|
||||||
|
__tablename__ = "phone"
|
||||||
|
|
||||||
|
id = Column(String, ForeignKey("rolodex.id", ondelete="CASCADE"), primary_key=True, index=True)
|
||||||
|
phone = Column(String, primary_key=True)
|
||||||
|
location = Column(String)
|
||||||
|
|
||||||
|
__table_args__ = (
|
||||||
|
Index("ix_phone_id", "id"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TrnsType(Base):
|
||||||
|
"""TRNSTYPE (transaction groups)."""
|
||||||
|
__tablename__ = "trnstype"
|
||||||
|
|
||||||
|
t_type = Column(String, primary_key=True)
|
||||||
|
t_type_l = Column(String)
|
||||||
|
header = Column(String)
|
||||||
|
footer = Column(String)
|
||||||
|
|
||||||
|
|
||||||
|
class TrnsLkup(Base):
|
||||||
|
"""TRNSLKUP (transaction codes)."""
|
||||||
|
__tablename__ = "trnslkup"
|
||||||
|
|
||||||
|
t_code = Column(String, primary_key=True)
|
||||||
|
t_type = Column(String, ForeignKey("trnstype.t_type"), index=True)
|
||||||
|
t_type_l = Column(String)
|
||||||
|
amount = Column(Numeric(12, 2))
|
||||||
|
description = Column(Text)
|
||||||
|
|
||||||
|
|
||||||
|
class Footers(Base):
|
||||||
|
"""FOOTERS table for footer codes and labels."""
|
||||||
|
__tablename__ = "footers"
|
||||||
|
|
||||||
|
f_code = Column(String, primary_key=True)
|
||||||
|
f_footer = Column(Text)
|
||||||
|
|
||||||
|
|
||||||
|
class FileStat(Base):
|
||||||
|
"""FILESTAT table for file statuses."""
|
||||||
|
__tablename__ = "filestat"
|
||||||
|
|
||||||
|
status = Column(String, primary_key=True)
|
||||||
|
definition = Column(Text)
|
||||||
|
send = Column(String)
|
||||||
|
footer_code = Column(String, ForeignKey("footers.f_code"), index=True)
|
||||||
|
|
||||||
|
|
||||||
|
class Employee(Base):
|
||||||
|
"""EMPLOYEE table linking employees to `Rolodex` and rate."""
|
||||||
|
__tablename__ = "employee"
|
||||||
|
|
||||||
|
empl_num = Column(String, primary_key=True)
|
||||||
|
empl_id = Column(String, ForeignKey("rolodex.id"), index=True)
|
||||||
|
rate_per_hour = Column(Numeric(12, 2))
|
||||||
|
|
||||||
|
|
||||||
|
class States(Base):
|
||||||
|
"""STATES reference table."""
|
||||||
|
__tablename__ = "states"
|
||||||
|
|
||||||
|
abrev = Column(String(2), primary_key=True)
|
||||||
|
st = Column(String)
|
||||||
|
|
||||||
|
|
||||||
|
class GroupLkup(Base):
|
||||||
|
"""GRUPLKUP reference table."""
|
||||||
|
__tablename__ = "gruplkup"
|
||||||
|
|
||||||
|
code = Column(String, primary_key=True)
|
||||||
|
description = Column(Text)
|
||||||
|
title = Column(String)
|
||||||
|
|
||||||
|
|
||||||
|
class Printers(Base):
|
||||||
|
"""PRINTERS configuration table."""
|
||||||
|
__tablename__ = "printers"
|
||||||
|
|
||||||
|
number = Column(Integer, primary_key=True)
|
||||||
|
name = Column(String)
|
||||||
|
port = Column(String)
|
||||||
|
page_break = Column(String)
|
||||||
|
setup_st = Column(String)
|
||||||
|
phone_book = Column(String)
|
||||||
|
rolodex_info = Column(String)
|
||||||
|
envelope = Column(String)
|
||||||
|
file_cabinet = Column(String)
|
||||||
|
accounts = Column(String)
|
||||||
|
statements = Column(String)
|
||||||
|
calendar = Column(String)
|
||||||
|
reset_st = Column(String)
|
||||||
|
b_underline = Column(String)
|
||||||
|
e_underline = Column(String)
|
||||||
|
b_bold = Column(String)
|
||||||
|
e_bold = Column(String)
|
||||||
|
|
||||||
|
|
||||||
|
class Setup(Base):
|
||||||
|
"""
|
||||||
|
SETUP application configuration. Not strictly keyed in legacy; introduce surrogate PK.
|
||||||
|
"""
|
||||||
|
__tablename__ = "setup"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||||
|
appl_title = Column(String)
|
||||||
|
l_head1 = Column(String)
|
||||||
|
l_head2 = Column(String)
|
||||||
|
l_head3 = Column(String)
|
||||||
|
l_head4 = Column(String)
|
||||||
|
l_head5 = Column(String)
|
||||||
|
l_head6 = Column(String)
|
||||||
|
l_head7 = Column(String)
|
||||||
|
l_head8 = Column(String)
|
||||||
|
l_head9 = Column(String)
|
||||||
|
l_head10 = Column(String)
|
||||||
|
default_printer = Column(Integer, ForeignKey("printers.number"), index=True)
|
||||||
|
|
||||||
|
|
||||||
|
class LegacyFile(Base):
|
||||||
|
"""
|
||||||
|
FILES (file cabinet) primary table.
|
||||||
|
"""
|
||||||
|
__tablename__ = "files"
|
||||||
|
|
||||||
|
file_no = Column(String, primary_key=True, index=True)
|
||||||
|
id = Column(String, ForeignKey("rolodex.id"), index=True)
|
||||||
|
file_type = Column(String, index=True)
|
||||||
|
regarding = Column(Text)
|
||||||
|
opened = Column(Date)
|
||||||
|
closed = Column(Date)
|
||||||
|
empl_num = Column(String, ForeignKey("employee.empl_num"), index=True)
|
||||||
|
rate_per_hour = Column(Numeric(12, 2))
|
||||||
|
status = Column(String, ForeignKey("filestat.status"), index=True)
|
||||||
|
footer_code = Column(String, ForeignKey("footers.f_code"), index=True)
|
||||||
|
opposing = Column(String, ForeignKey("rolodex.id"), index=True)
|
||||||
|
hours = Column(Numeric(12, 2))
|
||||||
|
hours_p = Column(Numeric(12, 2))
|
||||||
|
trust_bal = Column(Numeric(12, 2))
|
||||||
|
trust_bal_p = Column(Numeric(12, 2))
|
||||||
|
hourly_fees = Column(Numeric(12, 2))
|
||||||
|
hourly_fees_p = Column(Numeric(12, 2))
|
||||||
|
flat_fees = Column(Numeric(12, 2))
|
||||||
|
flat_fees_p = Column(Numeric(12, 2))
|
||||||
|
disbursements = Column(Numeric(12, 2))
|
||||||
|
disbursements_p = Column(Numeric(12, 2))
|
||||||
|
credit_bal = Column(Numeric(12, 2))
|
||||||
|
credit_bal_p = Column(Numeric(12, 2))
|
||||||
|
total_charges = Column(Numeric(12, 2))
|
||||||
|
total_charges_p = Column(Numeric(12, 2))
|
||||||
|
amount_owing = Column(Numeric(12, 2))
|
||||||
|
amount_owing_p = Column(Numeric(12, 2))
|
||||||
|
transferable = Column(Numeric(12, 2))
|
||||||
|
memo = Column(Text)
|
||||||
|
|
||||||
|
__table_args__ = (
|
||||||
|
Index("ix_files_id", "id"),
|
||||||
|
Index("ix_files_opposing", "opposing"),
|
||||||
|
Index("ix_files_status", "status"),
|
||||||
|
Index("ix_files_type", "file_type"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class FilesR(Base):
|
||||||
|
"""FILES_R relationships per file."""
|
||||||
|
__tablename__ = "files_r"
|
||||||
|
|
||||||
|
file_no = Column(String, ForeignKey("files.file_no", ondelete="CASCADE"), primary_key=True)
|
||||||
|
relationship = Column(String, primary_key=True)
|
||||||
|
rolodex_id = Column(String, ForeignKey("rolodex.id", ondelete="CASCADE"), primary_key=True)
|
||||||
|
|
||||||
|
__table_args__ = (
|
||||||
|
Index("ix_files_r_rolodex_id", "rolodex_id"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class FilesV(Base):
|
||||||
|
"""FILES_V variables per file."""
|
||||||
|
__tablename__ = "files_v"
|
||||||
|
|
||||||
|
file_no = Column(String, ForeignKey("files.file_no", ondelete="CASCADE"), primary_key=True)
|
||||||
|
identifier = Column(String, primary_key=True)
|
||||||
|
response = Column(Text)
|
||||||
|
|
||||||
|
|
||||||
|
class Ledger(Base):
|
||||||
|
"""LEDGER entries for time/charges per file."""
|
||||||
|
__tablename__ = "ledger"
|
||||||
|
|
||||||
|
file_no = Column(String, ForeignKey("files.file_no", ondelete="CASCADE"), primary_key=True)
|
||||||
|
date = Column(Date, index=True)
|
||||||
|
item_no = Column(Integer, primary_key=True)
|
||||||
|
empl_num = Column(String, ForeignKey("employee.empl_num"), index=True)
|
||||||
|
t_code = Column(String, ForeignKey("trnslkup.t_code"), index=True)
|
||||||
|
t_type = Column(String, ForeignKey("trnstype.t_type"), index=True)
|
||||||
|
t_type_l = Column(String)
|
||||||
|
quantity = Column(Numeric(12, 2))
|
||||||
|
rate = Column(Numeric(12, 2))
|
||||||
|
amount = Column(Numeric(12, 2))
|
||||||
|
billed = Column(String(1)) # 'Y' or 'N'
|
||||||
|
note = Column(Text)
|
||||||
|
|
||||||
|
__table_args__ = (
|
||||||
|
Index("ix_ledger_file_date", "file_no", "date"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Deposits(Base):
|
||||||
|
"""DEPOSITS daily totals."""
|
||||||
|
__tablename__ = "deposits"
|
||||||
|
|
||||||
|
deposit_date = Column(Date, primary_key=True)
|
||||||
|
total = Column(Numeric(12, 2))
|
||||||
|
|
||||||
|
|
||||||
|
class LegacyPayment(Base):
|
||||||
|
"""PAYMENTS legacy payments (separate from modern `payments`)."""
|
||||||
|
__tablename__ = "payments_legacy"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||||
|
deposit_date = Column(Date, ForeignKey("deposits.deposit_date"), index=True)
|
||||||
|
file_no = Column(String, ForeignKey("files.file_no"), index=True)
|
||||||
|
rolodex_id = Column(String, ForeignKey("rolodex.id"), index=True)
|
||||||
|
regarding = Column(Text)
|
||||||
|
amount = Column(Numeric(12, 2))
|
||||||
|
note = Column(Text)
|
||||||
|
|
||||||
|
|
||||||
|
class PlanInfo(Base):
|
||||||
|
"""PLANINFO reference table."""
|
||||||
|
__tablename__ = "planinfo"
|
||||||
|
|
||||||
|
plan_id = Column(String, primary_key=True)
|
||||||
|
plan_name = Column(String)
|
||||||
|
plan_type = Column(String)
|
||||||
|
empl_id_no = Column(String)
|
||||||
|
plan_no = Column(String)
|
||||||
|
nra = Column(String)
|
||||||
|
era = Column(String)
|
||||||
|
errf = Column(String)
|
||||||
|
colas = Column(String)
|
||||||
|
divided_by = Column(String)
|
||||||
|
drafted = Column(String)
|
||||||
|
benefit_c = Column(String)
|
||||||
|
qdro_c = Column(String)
|
||||||
|
rev = Column(String) # ^REV
|
||||||
|
pa = Column(String) # ^PA
|
||||||
|
form_name = Column(String)
|
||||||
|
drafted_on = Column(Date)
|
||||||
|
memo = Column(Text)
|
||||||
|
|
||||||
|
|
||||||
|
class Qdros(Base):
|
||||||
|
"""QDROS table for QDRO case data."""
|
||||||
|
__tablename__ = "qdros"
|
||||||
|
|
||||||
|
file_no = Column(String, ForeignKey("files.file_no"), primary_key=True)
|
||||||
|
version = Column(String, primary_key=True)
|
||||||
|
plan_id = Column(String, ForeignKey("planinfo.plan_id"), index=True)
|
||||||
|
_1 = Column(String) # ^1
|
||||||
|
_2 = Column(String) # ^2
|
||||||
|
part = Column(String) # ^Part
|
||||||
|
altp = Column(String) # ^AltP
|
||||||
|
pet = Column(String) # ^Pet
|
||||||
|
res = Column(String) # ^Res
|
||||||
|
case_type = Column(String)
|
||||||
|
case_code = Column(String)
|
||||||
|
section = Column(String)
|
||||||
|
case_number = Column(String)
|
||||||
|
judgment_date = Column(Date)
|
||||||
|
valuation_date = Column(Date)
|
||||||
|
married_on = Column(Date)
|
||||||
|
percent_awarded = Column(Numeric(12, 2))
|
||||||
|
ven_city = Column(String)
|
||||||
|
ven_cnty = Column(String)
|
||||||
|
ven_st = Column(String(2))
|
||||||
|
draft_out = Column(Date)
|
||||||
|
draft_apr = Column(Date)
|
||||||
|
final_out = Column(Date)
|
||||||
|
judge = Column(String)
|
||||||
|
form_name = Column(String)
|
||||||
|
|
||||||
|
|
||||||
|
class Pensions(Base):
|
||||||
|
"""PENSIONS primary table; composite key (file_no, version)."""
|
||||||
|
__tablename__ = "pensions"
|
||||||
|
|
||||||
|
file_no = Column(String, ForeignKey("files.file_no"), primary_key=True)
|
||||||
|
version = Column(String, primary_key=True)
|
||||||
|
plan_id = Column(String, ForeignKey("planinfo.plan_id"), index=True)
|
||||||
|
plan_name = Column(String)
|
||||||
|
title = Column(String)
|
||||||
|
first = Column(String)
|
||||||
|
last = Column(String)
|
||||||
|
birth = Column(Date)
|
||||||
|
race = Column(String)
|
||||||
|
sex = Column(String)
|
||||||
|
info = Column(Date)
|
||||||
|
valu = Column(Date)
|
||||||
|
accrued = Column(Numeric(12, 2))
|
||||||
|
vested_per = Column(Numeric(12, 2))
|
||||||
|
start_age = Column(Numeric(12, 2))
|
||||||
|
cola = Column(Numeric(12, 2))
|
||||||
|
max_cola = Column(Numeric(12, 2))
|
||||||
|
withdrawal = Column(Numeric(12, 2))
|
||||||
|
pre_dr = Column(Numeric(12, 2))
|
||||||
|
post_dr = Column(Numeric(12, 2))
|
||||||
|
tax_rate = Column(Numeric(12, 2))
|
||||||
|
|
||||||
|
|
||||||
|
class PensionResults(Base):
|
||||||
|
"""RESULTS derived values per pension (by file_no, version)."""
|
||||||
|
__tablename__ = "pension_results"
|
||||||
|
|
||||||
|
file_no = Column(String, primary_key=True)
|
||||||
|
version = Column(String, primary_key=True)
|
||||||
|
|
||||||
|
accrued = Column(Numeric(12, 2))
|
||||||
|
start_age = Column(Numeric(12, 2))
|
||||||
|
cola = Column(Numeric(12, 2))
|
||||||
|
withdrawal = Column(Numeric(12, 2))
|
||||||
|
pre_dr = Column(Numeric(12, 2))
|
||||||
|
post_dr = Column(Numeric(12, 2))
|
||||||
|
tax_rate = Column(Numeric(12, 2))
|
||||||
|
age = Column(Numeric(12, 2))
|
||||||
|
years_from = Column(Numeric(12, 2))
|
||||||
|
life_exp = Column(Numeric(12, 2))
|
||||||
|
ev_monthly = Column(Numeric(12, 2))
|
||||||
|
payments = Column(Numeric(12, 2))
|
||||||
|
pay_out = Column(Numeric(12, 2))
|
||||||
|
fund_value = Column(Numeric(12, 2))
|
||||||
|
pv = Column(Numeric(12, 2))
|
||||||
|
mortality = Column(Numeric(12, 2))
|
||||||
|
pv_am = Column(Numeric(12, 2))
|
||||||
|
pv_amt = Column(Numeric(12, 2))
|
||||||
|
pv_pre_db = Column(Numeric(12, 2))
|
||||||
|
pv_annuity = Column(Numeric(12, 2))
|
||||||
|
wv_at = Column(Numeric(12, 2))
|
||||||
|
pv_plan = Column(Numeric(12, 2))
|
||||||
|
years_married = Column(Numeric(12, 2))
|
||||||
|
years_service = Column(Numeric(12, 2))
|
||||||
|
marr_per = Column(Numeric(12, 2))
|
||||||
|
marr_amt = Column(Numeric(12, 2))
|
||||||
|
|
||||||
|
__table_args__ = (
|
||||||
|
ForeignKeyConstraint(["file_no", "version"], ["pensions.file_no", "pensions.version"], ondelete="CASCADE"),
|
||||||
|
Index("ix_pension_results_file_version", "file_no", "version"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class PensionMarriage(Base):
|
||||||
|
"""MARRIAGE periods related to pensions."""
|
||||||
|
__tablename__ = "pension_marriage"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||||
|
file_no = Column(String, nullable=False)
|
||||||
|
version = Column(String, nullable=False)
|
||||||
|
married_from = Column(Date)
|
||||||
|
married_to = Column(Date)
|
||||||
|
married_years = Column(Numeric(12, 2))
|
||||||
|
service_from = Column(Date)
|
||||||
|
service_to = Column(Date)
|
||||||
|
service_years = Column(Numeric(12, 2))
|
||||||
|
marital_pct = Column(Numeric(12, 2))
|
||||||
|
|
||||||
|
__table_args__ = (
|
||||||
|
ForeignKeyConstraint(["file_no", "version"], ["pensions.file_no", "pensions.version"], ondelete="CASCADE"),
|
||||||
|
Index("ix_pension_marriage_file_version", "file_no", "version"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class PensionDeath(Base):
|
||||||
|
"""DEATH related amounts for pensions."""
|
||||||
|
__tablename__ = "pension_death"
|
||||||
|
|
||||||
|
file_no = Column(String, primary_key=True)
|
||||||
|
version = Column(String, primary_key=True)
|
||||||
|
lump1 = Column(Numeric(12, 2))
|
||||||
|
lump2 = Column(Numeric(12, 2))
|
||||||
|
growth1 = Column(Numeric(12, 2))
|
||||||
|
growth2 = Column(Numeric(12, 2))
|
||||||
|
disc1 = Column(Numeric(12, 2))
|
||||||
|
disc2 = Column(Numeric(12, 2))
|
||||||
|
|
||||||
|
__table_args__ = (
|
||||||
|
ForeignKeyConstraint(["file_no", "version"], ["pensions.file_no", "pensions.version"], ondelete="CASCADE"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class PensionSchedule(Base):
|
||||||
|
"""SCHEDULE vesting schedule for pensions."""
|
||||||
|
__tablename__ = "pension_schedule"
|
||||||
|
|
||||||
|
file_no = Column(String, primary_key=True)
|
||||||
|
version = Column(String, primary_key=True)
|
||||||
|
vests_on = Column(Date)
|
||||||
|
vests_at = Column(Numeric(12, 2))
|
||||||
|
|
||||||
|
__table_args__ = (
|
||||||
|
ForeignKeyConstraint(["file_no", "version"], ["pensions.file_no", "pensions.version"], ondelete="CASCADE"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class PensionSeparate(Base):
|
||||||
|
"""SEPARATE calculations for pensions."""
|
||||||
|
__tablename__ = "pension_separate"
|
||||||
|
|
||||||
|
file_no = Column(String, primary_key=True)
|
||||||
|
version = Column(String, primary_key=True)
|
||||||
|
separation_rate = Column(Numeric(12, 2))
|
||||||
|
|
||||||
|
__table_args__ = (
|
||||||
|
ForeignKeyConstraint(["file_no", "version"], ["pensions.file_no", "pensions.version"], ondelete="CASCADE"),
|
||||||
|
)
|
||||||
|
|||||||
@@ -37,6 +37,12 @@
|
|||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link {% if 'dashboard' in request.url.path %}active{% endif %}" href="/dashboard">Dashboard</a>
|
<a class="nav-link {% if 'dashboard' in request.url.path %}active{% endif %}" href="/dashboard">Dashboard</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link {% if 'rolodex' in request.url.path %}active{% endif %}" href="/rolodex">Rolodex</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link {% if 'payments' in request.url.path %}active{% endif %}" href="/payments">Payments</a>
|
||||||
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link {% if 'admin' in request.url.path %}active{% endif %}" href="/admin">Admin</a>
|
<a class="nav-link {% if 'admin' in request.url.path %}active{% endif %}" href="/admin">Admin</a>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -180,17 +180,77 @@ Case {{ case.file_no if case else '' }} · Delphi Database
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-xl-4">
|
<div class="col-xl-8">
|
||||||
<div class="card h-100">
|
<div class="card h-100">
|
||||||
<div class="card-header">Transactions</div>
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
<div class="card-body p-0">
|
<span>Ledger</span>
|
||||||
<div class="table-responsive">
|
<div class="small text-muted">
|
||||||
<table class="table table-sm mb-0 align-middle">
|
<span class="me-3">Billed: {{ '%.2f'|format(totals.billed_total) }}</span>
|
||||||
|
<span class="me-3">Unbilled: {{ '%.2f'|format(totals.unbilled_total) }}</span>
|
||||||
|
<span>Total: {{ '%.2f'|format(totals.overall_total) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form class="row g-2 align-items-end" method="post" action="/case/{{ case.id }}/ledger">
|
||||||
|
<div class="col-md-2">
|
||||||
|
<label class="form-label">Date</label>
|
||||||
|
<input type="date" class="form-control" name="transaction_date" required>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-1">
|
||||||
|
<label class="form-label">Item #</label>
|
||||||
|
<input type="number" class="form-control" name="item_no" min="1">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2">
|
||||||
|
<label class="form-label">T_Code</label>
|
||||||
|
<input type="text" class="form-control" name="t_code" maxlength="10" required>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2">
|
||||||
|
<label class="form-label">Empl_Num</label>
|
||||||
|
<input type="text" class="form-control" name="employee_number" maxlength="20" required>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-1">
|
||||||
|
<label class="form-label">Qty</label>
|
||||||
|
<input type="number" class="form-control js-qty" name="quantity" step="0.01">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-1">
|
||||||
|
<label class="form-label">Rate</label>
|
||||||
|
<input type="number" class="form-control js-rate" name="rate" step="0.01">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2">
|
||||||
|
<label class="form-label">Amount</label>
|
||||||
|
<input type="number" class="form-control js-amount" name="amount" step="0.01" required>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-1">
|
||||||
|
<label class="form-label">Billed</label>
|
||||||
|
<select class="form-select" name="billed" required>
|
||||||
|
<option value="N">N</option>
|
||||||
|
<option value="Y">Y</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-12">
|
||||||
|
<label class="form-label">Description</label>
|
||||||
|
<input type="text" class="form-control" name="description" maxlength="255">
|
||||||
|
</div>
|
||||||
|
<div class="col-12 d-flex gap-2">
|
||||||
|
<button type="submit" class="btn btn-primary"><i class="bi bi-plus-lg me-1"></i>Add</button>
|
||||||
|
<button type="reset" class="btn btn-outline-secondary">Clear</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="table-responsive mt-3">
|
||||||
|
<table class="table table-sm align-middle">
|
||||||
<thead class="table-light">
|
<thead class="table-light">
|
||||||
<tr>
|
<tr>
|
||||||
<th style="width: 110px;">Date</th>
|
<th style="width: 110px;">Date</th>
|
||||||
<th>Type</th>
|
<th style="width: 70px;">Item</th>
|
||||||
|
<th style="width: 90px;">T_Code</th>
|
||||||
|
<th style="width: 110px;">Empl</th>
|
||||||
|
<th class="text-end" style="width: 100px;">Qty</th>
|
||||||
|
<th class="text-end" style="width: 100px;">Rate</th>
|
||||||
<th class="text-end" style="width: 120px;">Amount</th>
|
<th class="text-end" style="width: 120px;">Amount</th>
|
||||||
|
<th style="width: 70px;">Billed</th>
|
||||||
|
<th>Description</th>
|
||||||
|
<th class="text-end" style="width: 120px;">Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -198,12 +258,35 @@ Case {{ case.file_no if case else '' }} · Delphi Database
|
|||||||
{% for t in case.transactions %}
|
{% for t in case.transactions %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ t.transaction_date.strftime('%Y-%m-%d') if t.transaction_date else '' }}</td>
|
<td>{{ t.transaction_date.strftime('%Y-%m-%d') if t.transaction_date else '' }}</td>
|
||||||
<td>{{ t.transaction_type or '' }}</td>
|
<td>{{ t.item_no or '' }}</td>
|
||||||
|
<td>{{ t.t_code or '' }}</td>
|
||||||
|
<td>{{ t.employee_number or '' }}</td>
|
||||||
|
<td class="text-end">{{ '%.2f'|format(t.quantity) if t.quantity is not none else '' }}</td>
|
||||||
|
<td class="text-end">{{ '%.2f'|format(t.rate) if t.rate is not none else '' }}</td>
|
||||||
<td class="text-end">{{ '%.2f'|format(t.amount) if t.amount is not none else '' }}</td>
|
<td class="text-end">{{ '%.2f'|format(t.amount) if t.amount is not none else '' }}</td>
|
||||||
|
<td>{{ t.billed or '' }}</td>
|
||||||
|
<td>{{ t.description or '' }}</td>
|
||||||
|
<td class="text-end">
|
||||||
|
<form method="post" action="/case/{{ case.id }}/ledger/{{ t.id }}" class="d-inline">
|
||||||
|
<input type="hidden" name="transaction_date" value="{{ t.transaction_date.strftime('%Y-%m-%d') if t.transaction_date else '' }}">
|
||||||
|
<input type="hidden" name="item_no" value="{{ t.item_no or '' }}">
|
||||||
|
<input type="hidden" name="t_code" value="{{ t.t_code or '' }}">
|
||||||
|
<input type="hidden" name="employee_number" value="{{ t.employee_number or '' }}">
|
||||||
|
<input type="hidden" name="quantity" value="{{ t.quantity or '' }}">
|
||||||
|
<input type="hidden" name="rate" value="{{ t.rate or '' }}">
|
||||||
|
<input type="hidden" name="amount" value="{{ t.amount or '' }}">
|
||||||
|
<input type="hidden" name="billed" value="{{ t.billed or '' }}">
|
||||||
|
<input type="hidden" name="description" value="{{ t.description or '' }}">
|
||||||
|
<button type="submit" class="btn btn-sm btn-outline-secondary" data-bs-toggle="tooltip" title="Quick save last values"><i class="bi bi-arrow-repeat"></i></button>
|
||||||
|
</form>
|
||||||
|
<form method="post" action="/case/{{ case.id }}/ledger/{{ t.id }}/delete" class="d-inline">
|
||||||
|
<button type="submit" class="btn btn-sm btn-outline-danger" data-confirm-delete="Delete this entry?"><i class="bi bi-trash"></i></button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<tr><td colspan="3" class="text-center text-muted py-3">No transactions.</td></tr>
|
<tr><td colspan="10" class="text-center text-muted py-3">No ledger entries.</td></tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@@ -275,6 +358,13 @@ Case {{ case.file_no if case else '' }} · Delphi Database
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="alert alert-warning" role="alert">
|
||||||
|
<i class="bi bi-info-circle me-2"></i>
|
||||||
|
You must be logged in to view case details and ledger.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
106
app/templates/payments.html
Normal file
106
app/templates/payments.html
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Payments · Delphi Database{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row g-3 align-items-center mb-3">
|
||||||
|
<div class="col-auto">
|
||||||
|
<h2 class="mb-0">Payments</h2>
|
||||||
|
</div>
|
||||||
|
<div class="col ms-auto">
|
||||||
|
<form class="row g-2" method="get">
|
||||||
|
<div class="col-md-2">
|
||||||
|
<input type="date" class="form-control" name="from_date" value="{{ from_date or '' }}" placeholder="From">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2">
|
||||||
|
<input type="date" class="form-control" name="to_date" value="{{ to_date or '' }}" placeholder="To">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2">
|
||||||
|
<input type="text" class="form-control" name="file_no" value="{{ file_no or '' }}" placeholder="File #">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2">
|
||||||
|
<input type="text" class="form-control" name="rolodex_id" value="{{ rolodex_id or '' }}" placeholder="Rolodex Id">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<input type="text" class="form-control" name="q" value="{{ q or '' }}" placeholder="Description contains">
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<input type="hidden" name="page_size" value="{{ page_size }}">
|
||||||
|
<button class="btn btn-outline-primary" type="submit"><i class="bi bi-search me-1"></i>Search</button>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<a class="btn btn-outline-secondary" href="/payments"><i class="bi bi-x-circle me-1"></i>Clear</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 text-muted small">
|
||||||
|
{% if total and total > 0 %}
|
||||||
|
Showing {{ start_index }}–{{ end_index }} of {{ total }} | Page total: ${{ '%.2f'|format(page_total_amount) }}
|
||||||
|
{% else %}
|
||||||
|
No results
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover align-middle">
|
||||||
|
<thead class="table-light">
|
||||||
|
<tr>
|
||||||
|
<th style="width: 120px;">Date</th>
|
||||||
|
<th style="width: 140px;">File #</th>
|
||||||
|
<th>Client</th>
|
||||||
|
<th>Type</th>
|
||||||
|
<th>Description</th>
|
||||||
|
<th class="text-end" style="width: 140px;">Amount</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% if payments and payments|length > 0 %}
|
||||||
|
{% for p in payments %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ p.payment_date.strftime('%Y-%m-%d') if p.payment_date else '' }}</td>
|
||||||
|
<td>{{ p.case.file_no if p.case else '' }}</td>
|
||||||
|
<td>
|
||||||
|
{% set client = p.case.client if p.case else None %}
|
||||||
|
{% if client %}{{ client.last_name }}, {{ client.first_name }}{% else %}<span class="text-muted">—</span>{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>{{ p.payment_type or '' }}</td>
|
||||||
|
<td>{{ p.description or '' }}</td>
|
||||||
|
<td class="text-end">{{ '%.2f'|format(p.amount) if p.amount is not none else '' }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<tr><td colspan="6" class="text-center text-muted py-4">No payments found.</td></tr>
|
||||||
|
{% endif %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12">
|
||||||
|
{% if total_pages and total_pages > 1 %}
|
||||||
|
<nav aria-label="Payments pagination">
|
||||||
|
<ul class="pagination mb-0">
|
||||||
|
<li class="page-item {% if page <= 1 %}disabled{% endif %}">
|
||||||
|
<a class="page-link" href="/payments?page={{ page - 1 if page > 1 else 1 }}&page_size={{ page_size }}{% if from_date %}&from_date={{ from_date }}{% endif %}{% if to_date %}&to_date={{ to_date }}{% endif %}{% if file_no %}&file_no={{ file_no | urlencode }}{% endif %}{% if rolodex_id %}&rolodex_id={{ rolodex_id | urlencode }}{% endif %}{% if q %}&q={{ q | urlencode }}{% endif %}">
|
||||||
|
«
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% for p in range(1, total_pages + 1) %}
|
||||||
|
<li class="page-item {% if p == page %}active{% endif %}">
|
||||||
|
<a class="page-link" href="/payments?page={{ p }}&page_size={{ page_size }}{% if from_date %}&from_date={{ from_date }}{% endif %}{% if to_date %}&to_date={{ to_date }}{% endif %}{% if file_no %}&file_no={{ file_no | urlencode }}{% endif %}{% if rolodex_id %}&rolodex_id={{ rolodex_id | urlencode }}{% endif %}{% if q %}&q={{ q | urlencode }}{% endif %}">{{ p }}</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
<li class="page-item {% if page >= total_pages %}disabled{% endif %}">
|
||||||
|
<a class="page-link" href="/payments?page={{ page + 1 if page < total_pages else total_pages }}&page_size={{ page_size }}{% if from_date %}&from_date={{ from_date }}{% endif %}{% if to_date %}&to_date={{ to_date }}{% endif %}{% if file_no %}&file_no={{ file_no | urlencode }}{% endif %}{% if rolodex_id %}&rolodex_id={{ rolodex_id | urlencode }}{% endif %}{% if q %}&q={{ q | urlencode }}{% endif %}">
|
||||||
|
»
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
61
app/templates/report_phone_book.html
Normal file
61
app/templates/report_phone_book.html
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Phone Book · Delphi Database{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-12 d-flex align-items-center">
|
||||||
|
<a class="btn btn-sm btn-outline-secondary me-2" href="/rolodex">
|
||||||
|
<i class="bi bi-arrow-left"></i> Back
|
||||||
|
</a>
|
||||||
|
<h2 class="mb-0">Phone Book</h2>
|
||||||
|
<div class="ms-auto d-flex gap-2">
|
||||||
|
<a class="btn btn-outline-secondary btn-sm" href="/reports/phone-book?format=csv{% for id in client_ids %}&client_ids={{ id }}{% endfor %}{% if q %}&q={{ q | urlencode }}{% endif %}">
|
||||||
|
<i class="bi bi-filetype-csv me-1"></i>Download CSV
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-sm align-middle">
|
||||||
|
<thead class="table-light">
|
||||||
|
<tr>
|
||||||
|
<th style="width: 220px;">Name</th>
|
||||||
|
<th>Company</th>
|
||||||
|
<th style="width: 160px;">Phone Type</th>
|
||||||
|
<th style="width: 220px;">Phone Number</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% if clients and clients|length > 0 %}
|
||||||
|
{% for c in clients %}
|
||||||
|
{% if c.phones and c.phones|length > 0 %}
|
||||||
|
{% for p in c.phones %}
|
||||||
|
<tr>
|
||||||
|
<td><span class="fw-semibold">{{ c.last_name or '' }}, {{ c.first_name or '' }}</span></td>
|
||||||
|
<td>{{ c.company or '' }}</td>
|
||||||
|
<td>{{ p.phone_type or '' }}</td>
|
||||||
|
<td>{{ p.phone_number or '' }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<tr>
|
||||||
|
<td><span class="fw-semibold">{{ c.last_name or '' }}, {{ c.first_name or '' }}</span></td>
|
||||||
|
<td>{{ c.company or '' }}</td>
|
||||||
|
<td class="text-muted">—</td>
|
||||||
|
<td class="text-muted">—</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<tr><td colspan="4" class="text-center text-muted py-4">No data.</td></tr>
|
||||||
|
{% endif %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
153
app/templates/rolodex.html
Normal file
153
app/templates/rolodex.html
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Rolodex · Delphi Database{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row g-3 align-items-center mb-3">
|
||||||
|
<div class="col-auto">
|
||||||
|
<h2 class="mb-0">Rolodex</h2>
|
||||||
|
</div>
|
||||||
|
<div class="col ms-auto">
|
||||||
|
<form class="row g-2" method="get" action="/rolodex">
|
||||||
|
<div class="col-md">
|
||||||
|
<input class="form-control" type="search" name="q" placeholder="Search name or company" aria-label="Search" value="{{ q or '' }}">
|
||||||
|
</div>
|
||||||
|
<div class="col-md">
|
||||||
|
<input class="form-control" type="search" name="phone" placeholder="Phone contains" aria-label="Phone" value="{{ phone or '' }}">
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<input type="hidden" name="page_size" value="{{ page_size }}">
|
||||||
|
<button class="btn btn-outline-primary" type="submit">
|
||||||
|
<i class="bi bi-search me-1"></i>Search
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<a class="btn btn-outline-secondary" href="/rolodex">
|
||||||
|
<i class="bi bi-x-circle me-1"></i>Clear
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<a class="btn btn-primary" href="/rolodex/new">
|
||||||
|
<i class="bi bi-plus-lg me-1"></i>New Client
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 text-muted small">
|
||||||
|
{% if total and total > 0 %}
|
||||||
|
Showing {{ start_index }}–{{ end_index }} of {{ total }}
|
||||||
|
{% else %}
|
||||||
|
No results
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<form method="post" action="/reports/phone-book" id="bulkForm">
|
||||||
|
<table class="table table-hover align-middle">
|
||||||
|
<thead class="table-light">
|
||||||
|
<tr>
|
||||||
|
{% if enable_bulk %}
|
||||||
|
<th style="width: 40px;"><input class="form-check-input" type="checkbox" id="selectAll"></th>
|
||||||
|
{% endif %}
|
||||||
|
<th style="width: 220px;">Name</th>
|
||||||
|
<th>Company</th>
|
||||||
|
<th>Address</th>
|
||||||
|
<th>City</th>
|
||||||
|
<th style="width: 80px;">State</th>
|
||||||
|
<th style="width: 110px;">ZIP</th>
|
||||||
|
<th style="width: 200px;">Phones</th>
|
||||||
|
<th class="text-end" style="width: 140px;">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% if clients and clients|length > 0 %}
|
||||||
|
{% for c in clients %}
|
||||||
|
<tr>
|
||||||
|
{% if enable_bulk %}
|
||||||
|
<td>
|
||||||
|
<input class="form-check-input" type="checkbox" name="client_ids" value="{{ c.id }}">
|
||||||
|
</td>
|
||||||
|
{% endif %}
|
||||||
|
<td>
|
||||||
|
<span class="fw-semibold">{{ c.last_name or '' }}, {{ c.first_name or '' }}</span>
|
||||||
|
</td>
|
||||||
|
<td>{{ c.company or '' }}</td>
|
||||||
|
<td>{{ c.address or '' }}</td>
|
||||||
|
<td>{{ c.city or '' }}</td>
|
||||||
|
<td>{{ c.state or '' }}</td>
|
||||||
|
<td>{{ c.zip_code or '' }}</td>
|
||||||
|
<td>
|
||||||
|
{% if c.phones and c.phones|length > 0 %}
|
||||||
|
{% for p in c.phones[:3] %}
|
||||||
|
<span class="badge bg-light text-dark me-1">{{ p.phone_number }}</span>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">—</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td class="text-end">
|
||||||
|
<a class="btn btn-sm btn-outline-primary" href="/rolodex/{{ c.id }}">
|
||||||
|
<i class="bi bi-person-lines-fill me-1"></i>View
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<tr>
|
||||||
|
<td colspan="8" class="text-center text-muted py-4">No clients found.</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% if enable_bulk %}
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<button type="submit" class="btn btn-outline-secondary">
|
||||||
|
<i class="bi bi-journal-text me-1"></i>Phone Book (Selected)
|
||||||
|
</button>
|
||||||
|
<a class="btn btn-outline-secondary" href="/reports/phone-book?format=csv{% if q %}&q={{ q | urlencode }}{% endif %}">
|
||||||
|
<i class="bi bi-filetype-csv me-1"></i>Phone Book CSV (Current Filter)
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12">
|
||||||
|
{% if total_pages and total_pages > 1 %}
|
||||||
|
<nav aria-label="Rolodex pagination">
|
||||||
|
<ul class="pagination mb-0">
|
||||||
|
<li class="page-item {% if page <= 1 %}disabled{% endif %}">
|
||||||
|
<a class="page-link" href="/rolodex?page={{ page - 1 if page > 1 else 1 }}&page_size={{ page_size }}{% if q %}&q={{ q | urlencode }}{% endif %}{% if phone %}&phone={{ phone | urlencode }}{% endif %}" aria-label="Previous">
|
||||||
|
<span aria-hidden="true">«</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% for p in page_numbers %}
|
||||||
|
<li class="page-item {% if p == page %}active{% endif %}">
|
||||||
|
<a class="page-link" href="/rolodex?page={{ p }}&page_size={{ page_size }}{% if q %}&q={{ q | urlencode }}{% endif %}{% if phone %}&phone={{ phone | urlencode }}{% endif %}">{{ p }}</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
<li class="page-item {% if page >= total_pages %}disabled{% endif %}">
|
||||||
|
<a class="page-link" href="/rolodex?page={{ page + 1 if page < total_pages else total_pages }}&page_size={{ page_size }}{% if q %}&q={{ q | urlencode }}{% endif %}{% if phone %}&phone={{ phone | urlencode }}{% endif %}" aria-label="Next">
|
||||||
|
<span aria-hidden="true">»</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% block extra_scripts %}
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const selectAll = document.getElementById('selectAll');
|
||||||
|
if (selectAll) {
|
||||||
|
selectAll.addEventListener('change', function() {
|
||||||
|
document.querySelectorAll('input[name="client_ids"]').forEach(cb => cb.checked = selectAll.checked);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
71
app/templates/rolodex_edit.html
Normal file
71
app/templates/rolodex_edit.html
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}{{ 'New Client' if not client else 'Edit Client' }} · Delphi Database{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-12 d-flex align-items-center">
|
||||||
|
<a class="btn btn-sm btn-outline-secondary me-2" href="/rolodex">
|
||||||
|
<i class="bi bi-arrow-left"></i>
|
||||||
|
Back
|
||||||
|
</a>
|
||||||
|
<h2 class="mb-0">{{ 'New Client' if not client else 'Edit Client' }}</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="post" action="{{ '/rolodex/create' if not client else '/rolodex/' ~ client.id ~ '/update' }}">
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label for="last_name" class="form-label">Last Name</label>
|
||||||
|
<input type="text" class="form-control" id="last_name" name="last_name" value="{{ client.last_name if client else '' }}">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label for="first_name" class="form-label">First Name</label>
|
||||||
|
<input type="text" class="form-control" id="first_name" name="first_name" value="{{ client.first_name if client else '' }}">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label for="company" class="form-label">Company</label>
|
||||||
|
<input type="text" class="form-control" id="company" name="company" value="{{ client.company if client else '' }}">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="address" class="form-label">Address</label>
|
||||||
|
<input type="text" class="form-control" id="address" name="address" value="{{ client.address if client else '' }}">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label for="city" class="form-label">City</label>
|
||||||
|
<input type="text" class="form-control" id="city" name="city" value="{{ client.city if client else '' }}">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-1">
|
||||||
|
<label for="state" class="form-label">State</label>
|
||||||
|
<input type="text" class="form-control" id="state" name="state" value="{{ client.state if client else '' }}">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2">
|
||||||
|
<label for="zip_code" class="form-label">ZIP</label>
|
||||||
|
<input type="text" class="form-control" id="zip_code" name="zip_code" value="{{ client.zip_code if client else '' }}">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label for="rolodex_id" class="form-label">Legacy Rolodex Id</label>
|
||||||
|
<input type="text" class="form-control" id="rolodex_id" name="rolodex_id" value="{{ client.rolodex_id if client 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
|
||||||
|
</button>
|
||||||
|
<a href="{{ '/rolodex/' ~ client.id if client else '/rolodex' }}" class="btn btn-outline-secondary">Cancel</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
170
app/templates/rolodex_view.html
Normal file
170
app/templates/rolodex_view.html
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Client · {{ client.last_name }}, {{ client.first_name }} · Delphi Database{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-12 d-flex align-items-center">
|
||||||
|
<a class="btn btn-sm btn-outline-secondary me-2" href="/rolodex">
|
||||||
|
<i class="bi bi-arrow-left"></i>
|
||||||
|
Back
|
||||||
|
</a>
|
||||||
|
<h2 class="mb-0">Client</h2>
|
||||||
|
<div class="ms-auto">
|
||||||
|
<a class="btn btn-sm btn-outline-primary" href="/rolodex/{{ client.id }}/edit" onclick="event.preventDefault(); document.getElementById('editFormLink').submit();">
|
||||||
|
<i class="bi bi-pencil-square me-1"></i>Edit
|
||||||
|
</a>
|
||||||
|
<form id="editFormLink" method="get" action="/rolodex/new" class="d-none">
|
||||||
|
<input type="hidden" name="_" value="1">
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="text-muted small">Name</div>
|
||||||
|
<div class="fw-semibold">{{ client.last_name or '' }}, {{ client.first_name or '' }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="text-muted small">Company</div>
|
||||||
|
<div>{{ client.company or '' }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="text-muted small">Legacy Rolodex Id</div>
|
||||||
|
<div>{{ client.rolodex_id or '' }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="text-muted small">Address</div>
|
||||||
|
<div>{{ client.address or '' }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="text-muted small">City</div>
|
||||||
|
<div>{{ client.city or '' }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-1">
|
||||||
|
<div class="text-muted small">State</div>
|
||||||
|
<div>{{ client.state or '' }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2">
|
||||||
|
<div class="text-muted small">ZIP</div>
|
||||||
|
<div>{{ client.zip_code or '' }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<a class="btn btn-primary" href="/rolodex/new" onclick="event.preventDefault(); document.getElementById('editClientForm').submit();">
|
||||||
|
<i class="bi bi-pencil-square me-1"></i>Edit Client
|
||||||
|
</a>
|
||||||
|
<form id="editClientForm" method="get" action="/rolodex/new" class="d-none">
|
||||||
|
<input type="hidden" name="_prefill" value="{{ client.id }}">
|
||||||
|
</form>
|
||||||
|
<form method="post" action="/rolodex/{{ client.id }}/delete" onsubmit="return confirm('Delete this client? This cannot be undone.');">
|
||||||
|
<button type="submit" class="btn btn-outline-danger">
|
||||||
|
<i class="bi bi-trash me-1"></i>Delete
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<div class="card h-100">
|
||||||
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
|
<h5 class="mb-0">Phones</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form class="row g-2 mb-3" method="post" action="/rolodex/{{ client.id }}/phone/add">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<input type="text" class="form-control" name="phone_number" placeholder="Phone number" required>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<input type="text" class="form-control" name="phone_type" placeholder="Type (home, work)">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2">
|
||||||
|
<button type="submit" class="btn btn-primary w-100"><i class="bi bi-plus"></i></button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-sm align-middle">
|
||||||
|
<thead class="table-light">
|
||||||
|
<tr>
|
||||||
|
<th>Number</th>
|
||||||
|
<th>Type</th>
|
||||||
|
<th class="text-end" style="width: 100px;">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% if client.phones and client.phones|length > 0 %}
|
||||||
|
{% for p in client.phones %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ p.phone_number }}</td>
|
||||||
|
<td>{{ p.phone_type or '' }}</td>
|
||||||
|
<td class="text-end">
|
||||||
|
<form method="post" action="/rolodex/{{ client.id }}/phone/{{ p.id }}/delete" onsubmit="return confirm('Delete this phone?');">
|
||||||
|
<button type="submit" class="btn btn-sm btn-outline-danger">
|
||||||
|
<i class="bi bi-trash"></i>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<tr><td colspan="3" class="text-center text-muted py-3">No phones.</td></tr>
|
||||||
|
{% endif %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<div class="card h-100">
|
||||||
|
<div class="card-header">Related Cases</div>
|
||||||
|
<div class="card-body p-0">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-sm mb-0 align-middle">
|
||||||
|
<thead class="table-light">
|
||||||
|
<tr>
|
||||||
|
<th style="width: 140px;">File #</th>
|
||||||
|
<th>Description</th>
|
||||||
|
<th style="width: 90px;">Status</th>
|
||||||
|
<th style="width: 110px;">Opened</th>
|
||||||
|
<th class="text-end" style="width: 110px;">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% if client.cases and client.cases|length > 0 %}
|
||||||
|
{% for c in client.cases %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ c.file_no }}</td>
|
||||||
|
<td>{{ c.description or '' }}</td>
|
||||||
|
<td>{{ c.status or '' }}</td>
|
||||||
|
<td>{{ c.open_date.strftime('%Y-%m-%d') if c.open_date else '' }}</td>
|
||||||
|
<td class="text-end">
|
||||||
|
<a class="btn btn-sm btn-outline-primary" href="/case/{{ c.id }}">
|
||||||
|
<i class="bi bi-folder2-open"></i>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<tr><td colspan="5" class="text-center text-muted py-3">No related cases.</td></tr>
|
||||||
|
{% endif %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
@@ -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 1761016563 session eyJ1c2VyX2lkIjogMSwgInVzZXIiOiB7ImlkIjogMSwgInVzZXJuYW1lIjogImFkbWluIn19.aOSF8w.gmvSLjQ8LTg_OFCZNUZppoDIjrY
|
#HttpOnly_localhost FALSE / FALSE 1761056616 session eyJ1c2VyX2lkIjogMSwgInVzZXIiOiB7ImlkIjogMSwgInVzZXJuYW1lIjogImFkbWluIn19.aOUiaA.b0ACR1u9vUHgu86iSQ9Mnzw1j6U
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
version: '3.8'
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
delphi-db:
|
delphi-db:
|
||||||
build: .
|
build: .
|
||||||
|
|||||||
262
docs/legacy-schema.md
Normal file
262
docs/legacy-schema.md
Normal file
@@ -0,0 +1,262 @@
|
|||||||
|
## Legacy schema (inferred)
|
||||||
|
|
||||||
|
Source: headers in `old-database/Office/*.csv` (and subfolders) cross-checked against legacy `.SC` scripts.
|
||||||
|
|
||||||
|
Notes
|
||||||
|
- Types are inferred from names/usage; adjust during migration as needed.
|
||||||
|
- Date fields are typically legacy date types (store as DATE). Money/amounts as DECIMAL(12,2).
|
||||||
|
|
||||||
|
### ROLODEX
|
||||||
|
- Id (TEXT, pk)
|
||||||
|
- Prefix (TEXT)
|
||||||
|
- First (TEXT)
|
||||||
|
- Middle (TEXT)
|
||||||
|
- Last (TEXT)
|
||||||
|
- Suffix (TEXT)
|
||||||
|
- Title (TEXT)
|
||||||
|
- A1 (TEXT)
|
||||||
|
- A2 (TEXT)
|
||||||
|
- A3 (TEXT)
|
||||||
|
- City (TEXT)
|
||||||
|
- Abrev (TEXT)
|
||||||
|
- St (TEXT)
|
||||||
|
- Zip (TEXT)
|
||||||
|
- Email (TEXT)
|
||||||
|
- DOB (DATE)
|
||||||
|
- SS# (TEXT)
|
||||||
|
- Legal_Status (TEXT)
|
||||||
|
- Group (TEXT)
|
||||||
|
- Memo (TEXT)
|
||||||
|
|
||||||
|
### PHONE (by Id)
|
||||||
|
- Id (TEXT, fk → Rolodex.Id)
|
||||||
|
- Phone (TEXT)
|
||||||
|
- Location (TEXT)
|
||||||
|
|
||||||
|
### FILES (file cabinet)
|
||||||
|
- File_No (TEXT, pk)
|
||||||
|
- Id (TEXT, fk → Rolodex.Id)
|
||||||
|
- File_Type (TEXT, fk → FileType.File_Type)
|
||||||
|
- Regarding (TEXT)
|
||||||
|
- Opened (DATE)
|
||||||
|
- Closed (DATE, nullable)
|
||||||
|
- Empl_Num (TEXT, fk → Employee.Empl_Num)
|
||||||
|
- Rate_Per_Hour (DECIMAL)
|
||||||
|
- Status (TEXT, fk → FileStat.Status)
|
||||||
|
- Footer_Code (TEXT, fk → Footers.F_Code)
|
||||||
|
- Opposing (TEXT, fk → Rolodex.Id)
|
||||||
|
- Hours (DECIMAL)
|
||||||
|
- Hours_P (DECIMAL)
|
||||||
|
- Trust_Bal (DECIMAL)
|
||||||
|
- Trust_Bal_P (DECIMAL)
|
||||||
|
- Hourly_Fees (DECIMAL)
|
||||||
|
- Hourly_Fees_P (DECIMAL)
|
||||||
|
- Flat_Fees (DECIMAL)
|
||||||
|
- Flat_Fees_P (DECIMAL)
|
||||||
|
- Disbursements (DECIMAL)
|
||||||
|
- Disbursements_P (DECIMAL)
|
||||||
|
- Credit_Bal (DECIMAL)
|
||||||
|
- Credit_Bal_P (DECIMAL)
|
||||||
|
- Total_Charges (DECIMAL)
|
||||||
|
- Total_Charges_P (DECIMAL)
|
||||||
|
- Amount_Owing (DECIMAL)
|
||||||
|
- Amount_Owing_P (DECIMAL)
|
||||||
|
- Transferable (DECIMAL)
|
||||||
|
- Memo (TEXT)
|
||||||
|
|
||||||
|
### FILES_R (relationships)
|
||||||
|
- File_No (TEXT, fk → Files.File_No)
|
||||||
|
- Relationship (TEXT)
|
||||||
|
- Rolodex_Id (TEXT, fk → Rolodex.Id)
|
||||||
|
|
||||||
|
### FILES_V (variables)
|
||||||
|
- File_No (TEXT, fk → Files.File_No)
|
||||||
|
- Identifier (TEXT)
|
||||||
|
- Response (TEXT)
|
||||||
|
|
||||||
|
### LEDGER
|
||||||
|
- File_No (TEXT, fk → Files.File_No)
|
||||||
|
- Date (DATE)
|
||||||
|
- Item_No (INTEGER, part of pk per scripts use)
|
||||||
|
- Empl_Num (TEXT, fk → Employee.Empl_Num)
|
||||||
|
- T_Code (TEXT, fk → TrnsLkup.T_Code)
|
||||||
|
- T_Type (TEXT, fk → TrnsType.T_Type)
|
||||||
|
- T_Type_L (TEXT)
|
||||||
|
- Quantity (DECIMAL)
|
||||||
|
- Rate (DECIMAL)
|
||||||
|
- Amount (DECIMAL)
|
||||||
|
- Billed (TEXT, one of 'Y'/'N')
|
||||||
|
- Note (TEXT)
|
||||||
|
|
||||||
|
### TRNSACTN (joined outputs)
|
||||||
|
- File_No (TEXT)
|
||||||
|
- Id (TEXT)
|
||||||
|
- Footer_Code (TEXT)
|
||||||
|
- Date (DATE)
|
||||||
|
- Item_No (INTEGER)
|
||||||
|
- Empl_Num (TEXT)
|
||||||
|
- T_Code (TEXT)
|
||||||
|
- T_Type (TEXT)
|
||||||
|
- T_Type_L (TEXT)
|
||||||
|
- Quantity (DECIMAL)
|
||||||
|
- Rate (DECIMAL)
|
||||||
|
- Amount (DECIMAL)
|
||||||
|
- Billed (TEXT)
|
||||||
|
- Note (TEXT)
|
||||||
|
|
||||||
|
### TRNSLKUP (transaction codes)
|
||||||
|
- T_Code (TEXT, pk)
|
||||||
|
- T_Type (TEXT, fk → TrnsType.T_Type)
|
||||||
|
- T_Type_L (TEXT)
|
||||||
|
- Amount (DECIMAL)
|
||||||
|
- Description (TEXT)
|
||||||
|
|
||||||
|
### TRNSTYPE (transaction groups)
|
||||||
|
- T_Type (TEXT, pk)
|
||||||
|
- T_Type_L (TEXT)
|
||||||
|
- Header (TEXT)
|
||||||
|
- Footer (TEXT)
|
||||||
|
|
||||||
|
### FILESTAT
|
||||||
|
- Status (TEXT, pk)
|
||||||
|
- Definition (TEXT)
|
||||||
|
- Send (TEXT)
|
||||||
|
- Footer_Code (TEXT, fk → Footers.F_Code)
|
||||||
|
|
||||||
|
### FOOTERS
|
||||||
|
- F_Code (TEXT, pk)
|
||||||
|
- F_Footer (TEXT)
|
||||||
|
|
||||||
|
### EMPLOYEE
|
||||||
|
- Empl_Num (TEXT, pk)
|
||||||
|
- Empl_Id (TEXT, fk → Rolodex.Id)
|
||||||
|
- Rate_Per_Hour (DECIMAL)
|
||||||
|
|
||||||
|
### STATES
|
||||||
|
- Abrev (TEXT, pk)
|
||||||
|
- St (TEXT)
|
||||||
|
|
||||||
|
### GRUPLKUP
|
||||||
|
- Code (TEXT, pk)
|
||||||
|
- Description (TEXT)
|
||||||
|
- Title (TEXT)
|
||||||
|
|
||||||
|
### SETUP
|
||||||
|
- Appl_Title (TEXT)
|
||||||
|
- L_Head1..L_Head10 (TEXT)
|
||||||
|
- Default_Printer (INTEGER, fk → Printers.Number)
|
||||||
|
|
||||||
|
### PRINTERS
|
||||||
|
- Number (INTEGER, pk)
|
||||||
|
- Name (TEXT)
|
||||||
|
- Port (TEXT)
|
||||||
|
- Page_Break (TEXT)
|
||||||
|
- Setup_St (TEXT)
|
||||||
|
- Phone_Book (TEXT)
|
||||||
|
- Rolodex_Info (TEXT)
|
||||||
|
- Envelope (TEXT)
|
||||||
|
- File_Cabinet (TEXT)
|
||||||
|
- Accounts (TEXT)
|
||||||
|
- Statements (TEXT)
|
||||||
|
- Calendar (TEXT)
|
||||||
|
- Reset_St (TEXT)
|
||||||
|
- B_Underline (TEXT)
|
||||||
|
- E_Underline (TEXT)
|
||||||
|
- B_Bold (TEXT)
|
||||||
|
- E_Bold (TEXT)
|
||||||
|
|
||||||
|
### DEPOSITS / PAYMENTS
|
||||||
|
- Deposits
|
||||||
|
- Deposit_Date (DATE, pk?)
|
||||||
|
- Total (DECIMAL)
|
||||||
|
- Payments
|
||||||
|
- Deposit_Date (DATE, fk → Deposits.Deposit_Date)
|
||||||
|
- File_No (TEXT, fk → Files.File_No)
|
||||||
|
- Id (TEXT, fk → Rolodex.Id)
|
||||||
|
- Regarding (TEXT)
|
||||||
|
- Amount (DECIMAL)
|
||||||
|
- Note (TEXT)
|
||||||
|
|
||||||
|
### QDROS
|
||||||
|
- File_No (TEXT)
|
||||||
|
- Version (TEXT)
|
||||||
|
- Plan_Id (TEXT)
|
||||||
|
- ^1, ^2, ^Part, ^AltP, ^Pet, ^Res (TEXT)
|
||||||
|
- Case_Type (TEXT)
|
||||||
|
- Case_Code (TEXT)
|
||||||
|
- Section (TEXT)
|
||||||
|
- Case_Number (TEXT)
|
||||||
|
- Judgment_Date (DATE)
|
||||||
|
- Valuation_Date (DATE)
|
||||||
|
- Married_On (DATE)
|
||||||
|
- Percent_Awarded (DECIMAL)
|
||||||
|
- Ven_City (TEXT)
|
||||||
|
- Ven_Cnty (TEXT)
|
||||||
|
- Ven_St (TEXT)
|
||||||
|
- Draft_Out (DATE)
|
||||||
|
- Draft_Apr (DATE)
|
||||||
|
- Final_Out (DATE)
|
||||||
|
- Judge (TEXT)
|
||||||
|
- Form_Name (TEXT)
|
||||||
|
|
||||||
|
### PLANINFO
|
||||||
|
- Plan_Id (TEXT, pk)
|
||||||
|
- Plan_Name (TEXT)
|
||||||
|
- Plan_Type (TEXT)
|
||||||
|
- Empl_Id_No (TEXT)
|
||||||
|
- Plan_No (TEXT)
|
||||||
|
- NRA (TEXT)
|
||||||
|
- ERA (TEXT)
|
||||||
|
- ERRF (TEXT)
|
||||||
|
- COLAS (TEXT)
|
||||||
|
- Divided_By (TEXT)
|
||||||
|
- Drafted (TEXT)
|
||||||
|
- Benefit_C (TEXT)
|
||||||
|
- QDRO_C (TEXT)
|
||||||
|
- ^REV (TEXT)
|
||||||
|
- ^PA (TEXT)
|
||||||
|
- Form_Name (TEXT)
|
||||||
|
- Drafted_On (DATE)
|
||||||
|
- Memo (TEXT)
|
||||||
|
|
||||||
|
### PENSIONS (and related)
|
||||||
|
- PENSIONS
|
||||||
|
- File_No (TEXT)
|
||||||
|
- Version (TEXT)
|
||||||
|
- Plan_Id (TEXT)
|
||||||
|
- Plan_Name (TEXT)
|
||||||
|
- Title (TEXT)
|
||||||
|
- First (TEXT)
|
||||||
|
- Last (TEXT)
|
||||||
|
- Birth (DATE)
|
||||||
|
- Race (TEXT)
|
||||||
|
- Sex (TEXT)
|
||||||
|
- Info (DATE)
|
||||||
|
- Valu (DATE)
|
||||||
|
- Accrued (DECIMAL)
|
||||||
|
- Vested_Per (DECIMAL)
|
||||||
|
- Start_Age (DECIMAL)
|
||||||
|
- COLA (DECIMAL)
|
||||||
|
- Max_COLA (DECIMAL)
|
||||||
|
- Withdrawal (DECIMAL)
|
||||||
|
- Pre_DR (DECIMAL)
|
||||||
|
- Post_DR (DECIMAL)
|
||||||
|
- Tax_Rate (DECIMAL)
|
||||||
|
- RESULTS
|
||||||
|
- Accrued, Start_Age, COLA, Withdrawal, Pre_DR, Post_DR, Tax_Rate,
|
||||||
|
- Age, Years_From, Life_Exp, EV_Monthly, Payments, Pay_Out,
|
||||||
|
- Fund_Value, PV, Mortality, PV_AM, PV_AMT, PV_Pre_DB, PV_Annuity,
|
||||||
|
- WV_AT, PV_Plan, Years_Married, Years_Service, Marr_Per, Marr_Amt
|
||||||
|
- MARRIAGE
|
||||||
|
- File_No, Version, Married_From, Married_To, Married_Years,
|
||||||
|
- Service_From, Service_To, Service_Years, Marital_%
|
||||||
|
- DEATH
|
||||||
|
- File_No, Version, Lump1, Lump2, Growth1, Growth2, Disc1, Disc2
|
||||||
|
- SCHEDULE
|
||||||
|
- File_No, Version, Vests_On (DATE), Vests_At (DECIMAL)
|
||||||
|
- SEPARATE
|
||||||
|
- File_No, Version, Separation_Rate (DECIMAL)
|
||||||
|
- Life Tables (reference data)
|
||||||
|
- FORMS/LIFETABL, FORMS/NUMBERAL, PENSIONS/LIFETABL, PENSIONS/NUMBERAL
|
||||||
|
|
||||||
|
|
||||||
BIN
old-database/Table Structures (Some).pdf
Normal file
BIN
old-database/Table Structures (Some).pdf
Normal file
Binary file not shown.
@@ -50,6 +50,24 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Auto-compute Amount = Quantity × Rate in ledger add form
|
||||||
|
var qtyInput = document.querySelector('form[action*="/ledger"] .js-qty');
|
||||||
|
var rateInput = document.querySelector('form[action*="/ledger"] .js-rate');
|
||||||
|
var amountInput = document.querySelector('form[action*="/ledger"] .js-amount');
|
||||||
|
|
||||||
|
function recomputeAmount() {
|
||||||
|
if (!qtyInput || !rateInput || !amountInput) return;
|
||||||
|
var q = parseFloat(qtyInput.value);
|
||||||
|
var r = parseFloat(rateInput.value);
|
||||||
|
if (!isNaN(q) && !isNaN(r)) {
|
||||||
|
var amt = (q * r);
|
||||||
|
amountInput.value = amt.toFixed(2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (qtyInput) qtyInput.addEventListener('input', recomputeAmount);
|
||||||
|
if (rateInput) rateInput.addEventListener('input', recomputeAmount);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Utility functions
|
// Utility functions
|
||||||
|
|||||||
Reference in New Issue
Block a user