e3fac99045
Backend: - GET /api/v1/dashboard?month=YYYY-MM: KPIs, by_category, 6-month trend, budget alerts - GET/POST/PUT/DELETE /api/v1/budgets: budget envelopes with spent_cents/remaining_cents - POST /api/v1/budgets/rollover: copy budgets from M-1 to target month - GET /api/v1/history?year=YYYY: monthly summary for the year - GET /api/v1/export/csv|pdf?month=YYYY-MM: StreamingResponse exports (WeasyPrint PDF) - New schemas: dashboard, budget, history - Services: dashboard_service, budget_service - Routers mounted in main.py Frontend: - DashboardPage: 4 KPI cards, PieChart (expenses by category), BarChart (6-month trend), month navigation, budget alert badges, CSV/PDF export buttons - BudgetsPage: progress bars (green/orange/red), create/edit form, delete, rollover M-1 - HistoryPage: annual table with month click → dashboard, LineChart revenues/expenses - CategoriesPage: list by type with create/edit/delete (was missing from Phase 2) - TransactionsPage: added CSV/PDF export buttons - App.tsx: full routing with ProtectedRoute + Layout for all authenticated pages - New hooks: useDashboard, useBudgets (with mutations), useHistory - API types + client updated for all new endpoints Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
68 lines
2.4 KiB
Python
68 lines
2.4 KiB
Python
import uuid
|
|
|
|
from fastapi import APIRouter, Depends, Query, status
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.auth.dependencies import get_current_user
|
|
from app.database import get_session
|
|
from app.models.user import User
|
|
from app.schemas.budget import BudgetCreate, BudgetResponse, BudgetUpdate
|
|
from app.services import budget_service
|
|
from app.utils import utcnow
|
|
|
|
router = APIRouter(prefix="/budgets", tags=["budgets"])
|
|
|
|
|
|
def _current_month() -> str:
|
|
now = utcnow()
|
|
return f"{now.year}-{now.month:02d}"
|
|
|
|
|
|
@router.get("", response_model=list[BudgetResponse])
|
|
async def list_budgets(
|
|
month: str = Query(default=None, description="Month YYYY-MM (defaults to current month)"),
|
|
session: AsyncSession = Depends(get_session),
|
|
current_user: User = Depends(get_current_user),
|
|
) -> list[BudgetResponse]:
|
|
if month is None:
|
|
month = _current_month()
|
|
return await budget_service.list_budgets(session, current_user.id, month)
|
|
|
|
|
|
@router.post("", response_model=BudgetResponse, status_code=status.HTTP_201_CREATED)
|
|
async def create_budget(
|
|
data: BudgetCreate,
|
|
session: AsyncSession = Depends(get_session),
|
|
current_user: User = Depends(get_current_user),
|
|
) -> BudgetResponse:
|
|
return await budget_service.create_budget(session, current_user.id, data)
|
|
|
|
|
|
@router.put("/{budget_id}", response_model=BudgetResponse)
|
|
async def update_budget(
|
|
budget_id: uuid.UUID,
|
|
data: BudgetUpdate,
|
|
session: AsyncSession = Depends(get_session),
|
|
current_user: User = Depends(get_current_user),
|
|
) -> BudgetResponse:
|
|
return await budget_service.update_budget(session, current_user.id, budget_id, data)
|
|
|
|
|
|
@router.delete("/{budget_id}", status_code=status.HTTP_204_NO_CONTENT)
|
|
async def delete_budget(
|
|
budget_id: uuid.UUID,
|
|
session: AsyncSession = Depends(get_session),
|
|
current_user: User = Depends(get_current_user),
|
|
) -> None:
|
|
await budget_service.delete_budget(session, current_user.id, budget_id)
|
|
|
|
|
|
@router.post("/rollover", response_model=list[BudgetResponse], status_code=status.HTTP_201_CREATED)
|
|
async def rollover_budgets(
|
|
month: str = Query(..., description="Target month YYYY-MM to copy budgets into"),
|
|
session: AsyncSession = Depends(get_session),
|
|
current_user: User = Depends(get_current_user),
|
|
) -> list[BudgetResponse]:
|
|
"""Copy budgets from month-1 into the given month (skips existing ones)."""
|
|
return await budget_service.rollover_budgets(session, current_user.id, month)
|