This is the initial push of the outline of the life dashboard

This commit is contained in:
YOUNG
2026-03-09 12:52:48 -05:00
parent bf1e4e754f
commit 7596a5f382
47 changed files with 6522 additions and 3 deletions

View File

@@ -0,0 +1,143 @@
from datetime import date
from fastapi import APIRouter, Depends, HTTPException, Query
from pydantic import BaseModel
from sqlalchemy import func, select
from sqlalchemy.orm import Session
from app.auth import get_current_user
from app.database import get_db
from app.models.finance import FinanceSnapshot, FinanceTransaction
router = APIRouter(
prefix="/api/finances", tags=["finances"], dependencies=[Depends(get_current_user)]
)
# --- Schemas ---
class TransactionCreate(BaseModel):
date: date
amount: float
category: str
description: str | None = None
class TransactionRead(TransactionCreate):
id: int
model_config = {"from_attributes": True}
class SnapshotCreate(BaseModel):
date: date
net_worth: float
notes: str | None = None
class SnapshotRead(SnapshotCreate):
id: int
model_config = {"from_attributes": True}
class CategorySummary(BaseModel):
category: str
total: float
# --- Transaction endpoints ---
@router.get("/transactions", response_model=list[TransactionRead])
def list_transactions(
from_date: date | None = Query(None, alias="from"),
to_date: date | None = Query(None, alias="to"),
category: str | None = None,
db: Session = Depends(get_db),
):
q = select(FinanceTransaction).order_by(FinanceTransaction.date.desc())
if from_date:
q = q.where(FinanceTransaction.date >= from_date)
if to_date:
q = q.where(FinanceTransaction.date <= to_date)
if category:
q = q.where(FinanceTransaction.category == category)
return db.scalars(q).all()
@router.post("/transactions", response_model=TransactionRead, status_code=201)
def create_transaction(body: TransactionCreate, db: Session = Depends(get_db)):
txn = FinanceTransaction(**body.model_dump())
db.add(txn)
db.commit()
db.refresh(txn)
return txn
@router.put("/transactions/{txn_id}", response_model=TransactionRead)
def update_transaction(
txn_id: int, body: TransactionCreate, db: Session = Depends(get_db)
):
txn = db.get(FinanceTransaction, txn_id)
if not txn:
raise HTTPException(status_code=404, detail="Transaction not found")
for key, val in body.model_dump().items():
setattr(txn, key, val)
db.commit()
db.refresh(txn)
return txn
@router.delete("/transactions/{txn_id}", status_code=204)
def delete_transaction(txn_id: int, db: Session = Depends(get_db)):
txn = db.get(FinanceTransaction, txn_id)
if not txn:
raise HTTPException(status_code=404, detail="Transaction not found")
db.delete(txn)
db.commit()
# --- Snapshot endpoints ---
@router.get("/snapshots", response_model=list[SnapshotRead])
def list_snapshots(db: Session = Depends(get_db)):
return db.scalars(
select(FinanceSnapshot).order_by(FinanceSnapshot.date.desc())
).all()
@router.post("/snapshots", response_model=SnapshotRead, status_code=201)
def create_snapshot(body: SnapshotCreate, db: Session = Depends(get_db)):
snap = FinanceSnapshot(**body.model_dump())
db.add(snap)
db.commit()
db.refresh(snap)
return snap
# --- Summary endpoint ---
@router.get("/summary", response_model=list[CategorySummary])
def spending_summary(
period: str = Query("month", pattern="^(month|year)$"),
db: Session = Depends(get_db),
):
today = date.today()
if period == "month":
start = today.replace(day=1)
else:
start = today.replace(month=1, day=1)
rows = db.execute(
select(
FinanceTransaction.category,
func.sum(FinanceTransaction.amount).label("total"),
)
.where(FinanceTransaction.date >= start)
.where(FinanceTransaction.amount < 0)
.group_by(FinanceTransaction.category)
.order_by(func.sum(FinanceTransaction.amount))
).all()
return [CategorySummary(category=r.category, total=float(r.total)) for r in rows]