149 lines
5.9 KiB
Python
149 lines
5.9 KiB
Python
from fastapi import APIRouter, Depends, HTTPException
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.core.deps import ensure_class_access, ensure_class_module_enabled, ensure_class_permission, require_role, resolve_class_id_for_user
|
|
from app.db.database import get_db
|
|
from app.db.models import FundRecord, User
|
|
from app.schemas.fund import FundRecordCreate, FundRecordUpdate, FundRecordOut, FundStatistics
|
|
from app.schemas.common import PageResponse
|
|
from app.services.fund_service import (
|
|
create_fund_record, update_fund_record, delete_fund_record,
|
|
get_fund_record_by_id, list_fund_records, get_fund_statistics,
|
|
)
|
|
|
|
router = APIRouter(prefix="/api/fund", tags=["fund"])
|
|
|
|
|
|
def record_to_out(record: FundRecord) -> FundRecordOut:
|
|
return FundRecordOut(
|
|
id=record.id,
|
|
class_id=record.class_id,
|
|
type=record.type,
|
|
amount=record.amount,
|
|
category=record.category,
|
|
description=record.description,
|
|
image_urls=record.get_image_urls_list(),
|
|
record_date=record.record_date,
|
|
recorder_id=record.recorder_id,
|
|
recorder_name=record.recorder.name if record.recorder else "Unknown",
|
|
created_at=record.created_at,
|
|
updated_at=record.updated_at,
|
|
)
|
|
|
|
|
|
@router.get("/statistics", response_model=FundStatistics)
|
|
async def get_statistics(
|
|
class_id: int | None = None,
|
|
user: User = Depends(require_role("super_admin", "teacher", "student")),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
effective_class_id = resolve_class_id_for_user(user, class_id)
|
|
if effective_class_id is None:
|
|
return FundStatistics(
|
|
total_income=0, total_expense=0, balance=0,
|
|
income_by_category=[], expense_by_category=[]
|
|
)
|
|
ensure_class_access(user, effective_class_id)
|
|
await ensure_class_module_enabled(db, effective_class_id, "fund")
|
|
return await get_fund_statistics(db, effective_class_id)
|
|
|
|
|
|
@router.get("/", response_model=PageResponse[FundRecordOut])
|
|
async def get_fund_records(
|
|
page: int = 1,
|
|
page_size: int = 20,
|
|
type: str | None = None,
|
|
category: str | None = None,
|
|
class_id: int | None = None,
|
|
user: User = Depends(require_role("super_admin", "teacher", "student")),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
effective_class_id = resolve_class_id_for_user(user, class_id)
|
|
if effective_class_id is None:
|
|
return PageResponse(items=[], total=0, page=page, page_size=page_size, total_pages=0)
|
|
ensure_class_access(user, effective_class_id)
|
|
await ensure_class_module_enabled(db, effective_class_id, "fund")
|
|
|
|
records, total = await list_fund_records(db, effective_class_id, page, page_size, type, category)
|
|
total_pages = (total + page_size - 1) // page_size
|
|
items = [record_to_out(r) for r in records]
|
|
return PageResponse(items=items, total=total, page=page, page_size=page_size, total_pages=total_pages)
|
|
|
|
|
|
@router.get("/{record_id}", response_model=FundRecordOut)
|
|
async def get_fund_record_detail(
|
|
record_id: int,
|
|
user: User = Depends(require_role("super_admin", "teacher", "student")),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
record = await get_fund_record_by_id(db, record_id)
|
|
if record is None:
|
|
raise HTTPException(status_code=404, detail="Record not found")
|
|
ensure_class_access(user, record.class_id)
|
|
await ensure_class_module_enabled(db, record.class_id, "fund")
|
|
return record_to_out(record)
|
|
|
|
|
|
@router.post("/", response_model=FundRecordOut)
|
|
async def create_new_record(
|
|
data: FundRecordCreate,
|
|
class_id: int | None = None,
|
|
user: User = Depends(require_role("super_admin", "teacher", "student")),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
effective_class_id = resolve_class_id_for_user(user, class_id)
|
|
if effective_class_id is None:
|
|
raise HTTPException(status_code=400, detail="No class specified")
|
|
await ensure_class_module_enabled(db, effective_class_id, "fund")
|
|
ensure_class_permission(user, "fund_manage", effective_class_id)
|
|
|
|
if data.type not in ("income", "expense"):
|
|
raise HTTPException(status_code=400, detail="Type must be 'income' or 'expense'")
|
|
if data.amount <= 0:
|
|
raise HTTPException(status_code=400, detail="Amount must be positive")
|
|
|
|
record = await create_fund_record(db, effective_class_id, user.id, data)
|
|
# Reload with recorder relationship
|
|
record = await get_fund_record_by_id(db, record.id)
|
|
return record_to_out(record)
|
|
|
|
|
|
@router.put("/{record_id}", response_model=FundRecordOut)
|
|
async def update_existing_record(
|
|
record_id: int,
|
|
data: FundRecordUpdate,
|
|
user: User = Depends(require_role("super_admin", "teacher", "student")),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
record = await get_fund_record_by_id(db, record_id)
|
|
if record is None:
|
|
raise HTTPException(status_code=404, detail="Record not found")
|
|
await ensure_class_module_enabled(db, record.class_id, "fund")
|
|
ensure_class_permission(user, "fund_manage", record.class_id)
|
|
|
|
if data.type is not None and data.type not in ("income", "expense"):
|
|
raise HTTPException(status_code=400, detail="Type must be 'income' or 'expense'")
|
|
if data.amount is not None and data.amount <= 0:
|
|
raise HTTPException(status_code=400, detail="Amount must be positive")
|
|
|
|
updated = await update_fund_record(db, record, data)
|
|
# Reload with recorder relationship
|
|
updated = await get_fund_record_by_id(db, updated.id)
|
|
return record_to_out(updated)
|
|
|
|
|
|
@router.delete("/{record_id}")
|
|
async def delete_existing_record(
|
|
record_id: int,
|
|
user: User = Depends(require_role("super_admin", "teacher", "student")),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
record = await get_fund_record_by_id(db, record_id)
|
|
if record is None:
|
|
raise HTTPException(status_code=404, detail="Record not found")
|
|
await ensure_class_module_enabled(db, record.class_id, "fund")
|
|
ensure_class_permission(user, "fund_manage", record.class_id)
|
|
|
|
await delete_fund_record(db, record)
|
|
return {"message": "Record deleted"}
|