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>
74 lines
2.4 KiB
Python
74 lines
2.4 KiB
Python
from datetime import date
|
|
|
|
from fastapi import APIRouter, Depends, Query
|
|
from sqlalchemy import func, select
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.auth.dependencies import get_current_user
|
|
from app.database import get_session
|
|
from app.models.transaction import Transaction, TransactionType
|
|
from app.models.user import User
|
|
from app.schemas.history import HistoryResponse, MonthSummary
|
|
from app.utils import utcnow
|
|
|
|
router = APIRouter(prefix="/history", tags=["history"])
|
|
|
|
|
|
@router.get("", response_model=HistoryResponse)
|
|
async def get_history(
|
|
year: int = Query(default=None, description="Year (defaults to current year)"),
|
|
session: AsyncSession = Depends(get_session),
|
|
current_user: User = Depends(get_current_user),
|
|
) -> HistoryResponse:
|
|
if year is None:
|
|
year = utcnow().year
|
|
|
|
months: list[MonthSummary] = []
|
|
for mon in range(1, 13):
|
|
start = date(year, mon, 1)
|
|
if mon == 12:
|
|
end = date(year + 1, 1, 1)
|
|
else:
|
|
end = date(year, mon + 1, 1)
|
|
|
|
result = await session.execute(
|
|
select(
|
|
func.coalesce(
|
|
func.sum(
|
|
func.case(
|
|
(Transaction.type == TransactionType.income, Transaction.amount_cents),
|
|
else_=0,
|
|
)
|
|
),
|
|
0,
|
|
).label("income"),
|
|
func.coalesce(
|
|
func.sum(
|
|
func.case(
|
|
(Transaction.type == TransactionType.expense, Transaction.amount_cents),
|
|
else_=0,
|
|
)
|
|
),
|
|
0,
|
|
).label("expense"),
|
|
func.count(Transaction.id).label("count"),
|
|
).where(
|
|
Transaction.user_id == current_user.id,
|
|
Transaction.deleted_at.is_(None),
|
|
Transaction.transaction_date >= start,
|
|
Transaction.transaction_date < end,
|
|
)
|
|
)
|
|
row = result.one()
|
|
months.append(
|
|
MonthSummary(
|
|
month=f"{year}-{mon:02d}",
|
|
income_cents=row.income,
|
|
expense_cents=row.expense,
|
|
balance_cents=row.income - row.expense,
|
|
transaction_count=row.count,
|
|
)
|
|
)
|
|
|
|
return HistoryResponse(year=year, months=months)
|