201 előtti mentés
This commit is contained in:
@@ -9,7 +9,7 @@ from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.api import deps
|
||||
from app.schemas.analytics import TCOSummaryResponse, TCOErrorResponse
|
||||
from app.schemas.analytics import TCOSummaryResponse, TCOErrorResponse, DashboardResponse
|
||||
from app.services.analytics_service import TCOAnalytics
|
||||
from app.models import Vehicle
|
||||
from app.models.marketplace.organization import OrganizationMember
|
||||
@@ -190,6 +190,102 @@ async def get_tco_summary(
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.exception(f"Unexpected error in TCO summary for vehicle {vehicle_id}: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Internal server error: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/dashboard",
|
||||
response_model=DashboardResponse,
|
||||
responses={
|
||||
500: {"model": TCOErrorResponse, "description": "Internal server error"},
|
||||
},
|
||||
summary="Get dashboard analytics data",
|
||||
description="Returns aggregated dashboard data including monthly costs, fuel efficiency trends, "
|
||||
"and business metrics for the user's fleet."
|
||||
)
|
||||
async def get_dashboard_analytics(
|
||||
db: AsyncSession = Depends(deps.get_db),
|
||||
current_user = Depends(deps.get_current_active_user),
|
||||
):
|
||||
"""
|
||||
Retrieve dashboard analytics for the user's fleet.
|
||||
|
||||
This endpoint returns mock data for now, but will be connected to real
|
||||
analytics services in the future.
|
||||
"""
|
||||
try:
|
||||
# For now, return mock data matching the frontend expectations
|
||||
# In production, this would query the database and aggregate real data
|
||||
|
||||
# Import the new schema
|
||||
from app.schemas.analytics import (
|
||||
DashboardResponse, DashboardMonthlyCost, DashboardFuelEfficiency,
|
||||
DashboardCostPerKm, DashboardFunFacts, DashboardBusinessMetrics
|
||||
)
|
||||
|
||||
# Mock monthly costs (last 6 months)
|
||||
monthly_costs = [
|
||||
DashboardMonthlyCost(month="Oct", maintenance=450, fuel=320, insurance=180, total=950),
|
||||
DashboardMonthlyCost(month="Nov", maintenance=520, fuel=310, insurance=180, total=1010),
|
||||
DashboardMonthlyCost(month="Dec", maintenance=380, fuel=290, insurance=180, total=850),
|
||||
DashboardMonthlyCost(month="Jan", maintenance=620, fuel=350, insurance=200, total=1170),
|
||||
DashboardMonthlyCost(month="Feb", maintenance=410, fuel=280, insurance=180, total=870),
|
||||
DashboardMonthlyCost(month="Mar", maintenance=480, fuel=330, insurance=180, total=990),
|
||||
]
|
||||
|
||||
# Mock fuel efficiency trends
|
||||
fuel_efficiency_trends = [
|
||||
DashboardFuelEfficiency(month="Oct", efficiency=12.5),
|
||||
DashboardFuelEfficiency(month="Nov", efficiency=12.8),
|
||||
DashboardFuelEfficiency(month="Dec", efficiency=13.2),
|
||||
DashboardFuelEfficiency(month="Jan", efficiency=12.9),
|
||||
DashboardFuelEfficiency(month="Feb", efficiency=13.5),
|
||||
DashboardFuelEfficiency(month="Mar", efficiency=13.8),
|
||||
]
|
||||
|
||||
# Mock cost per km trends
|
||||
cost_per_km_trends = [
|
||||
DashboardCostPerKm(month="Oct", cost=0.42),
|
||||
DashboardCostPerKm(month="Nov", cost=0.45),
|
||||
DashboardCostPerKm(month="Dec", cost=0.38),
|
||||
DashboardCostPerKm(month="Jan", cost=0.51),
|
||||
DashboardCostPerKm(month="Feb", cost=0.39),
|
||||
DashboardCostPerKm(month="Mar", cost=0.41),
|
||||
]
|
||||
|
||||
# Mock fun facts
|
||||
fun_facts = DashboardFunFacts(
|
||||
total_km_driven=384400,
|
||||
total_trees_saved=42,
|
||||
total_co2_saved=8.5,
|
||||
total_money_saved=12500,
|
||||
moon_trips=1,
|
||||
earth_circuits=10
|
||||
)
|
||||
|
||||
# Mock business metrics
|
||||
business_metrics = DashboardBusinessMetrics(
|
||||
fleet_size=24,
|
||||
average_vehicle_age=3.2,
|
||||
total_monthly_cost=23500,
|
||||
average_cost_per_km=0.43,
|
||||
utilization_rate=78,
|
||||
downtime_hours=42
|
||||
)
|
||||
|
||||
return DashboardResponse(
|
||||
monthly_costs=monthly_costs,
|
||||
fuel_efficiency_trends=fuel_efficiency_trends,
|
||||
cost_per_km_trends=cost_per_km_trends,
|
||||
fun_facts=fun_facts,
|
||||
business_metrics=business_metrics
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.exception(f"Unexpected error in dashboard analytics: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Internal server error: {str(e)}"
|
||||
|
||||
@@ -18,6 +18,52 @@ from app.schemas.asset import AssetResponse, AssetCreate
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/vehicles", response_model=List[AssetResponse])
|
||||
async def get_user_vehicles(
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Get all vehicles/assets belonging to the current user or their organization.
|
||||
|
||||
This endpoint returns a paginated list of vehicles that the authenticated user
|
||||
has access to (either as owner or through organization membership).
|
||||
"""
|
||||
# Query assets where user is owner or organization member
|
||||
from sqlalchemy import or_
|
||||
|
||||
# First, get user's organization memberships
|
||||
from app.models.marketplace.organization import OrganizationMember
|
||||
org_stmt = select(OrganizationMember.organization_id).where(
|
||||
OrganizationMember.user_id == current_user.id
|
||||
)
|
||||
org_result = await db.execute(org_stmt)
|
||||
user_org_ids = [row[0] for row in org_result.all()]
|
||||
|
||||
# Build query: assets owned by user OR assets in user's organizations
|
||||
stmt = (
|
||||
select(Asset)
|
||||
.where(
|
||||
or_(
|
||||
Asset.owner_person_id == current_user.id,
|
||||
Asset.owner_org_id.in_(user_org_ids) if user_org_ids else False,
|
||||
Asset.operator_person_id == current_user.id,
|
||||
Asset.operator_org_id.in_(user_org_ids) if user_org_ids else False
|
||||
)
|
||||
)
|
||||
.order_by(Asset.created_at.desc())
|
||||
.offset(skip)
|
||||
.limit(limit)
|
||||
.options(selectinload(Asset.catalog))
|
||||
)
|
||||
|
||||
result = await db.execute(stmt)
|
||||
assets = result.scalars().all()
|
||||
|
||||
return assets
|
||||
|
||||
@router.get("/{asset_id}/financial-summary", response_model=Dict[str, Any])
|
||||
async def get_asset_financial_report(
|
||||
asset_id: uuid.UUID,
|
||||
|
||||
@@ -3,39 +3,65 @@ from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
from app.api.deps import get_db, get_current_user
|
||||
from app.models import Asset, AssetCost # JAVÍTVA
|
||||
from pydantic import BaseModel
|
||||
from datetime import date
|
||||
from app.models import Asset, AssetCost
|
||||
from app.schemas.asset_cost import AssetCostCreate
|
||||
from datetime import datetime
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
class ExpenseCreate(BaseModel):
|
||||
asset_id: str
|
||||
category: str
|
||||
amount: float
|
||||
date: date
|
||||
|
||||
@router.post("/add")
|
||||
async def add_expense(expense: ExpenseCreate, db: AsyncSession = Depends(get_db), current_user = Depends(get_current_user)):
|
||||
@router.post("/", status_code=201)
|
||||
async def create_expense(
|
||||
expense: AssetCostCreate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Create a new expense (fuel, service, tax, insurance) for an asset.
|
||||
Uses AssetCostCreate schema which includes mileage_at_cost, cost_type, etc.
|
||||
"""
|
||||
# Validate asset exists
|
||||
stmt = select(Asset).where(Asset.id == expense.asset_id)
|
||||
result = await db.execute(stmt)
|
||||
asset = result.scalar_one_or_none()
|
||||
if not asset:
|
||||
raise HTTPException(status_code=404, detail="Jármű nem található.")
|
||||
raise HTTPException(status_code=404, detail="Asset not found.")
|
||||
|
||||
# Determine organization_id from asset
|
||||
# Determine organization_id from asset (required by AssetCost model)
|
||||
organization_id = asset.current_organization_id or asset.owner_org_id
|
||||
if not organization_id:
|
||||
raise HTTPException(status_code=400, detail="Az eszközhez nincs társított szervezet.")
|
||||
raise HTTPException(status_code=400, detail="Asset has no associated organization.")
|
||||
|
||||
# Map cost_type to cost_category (AssetCost uses cost_category)
|
||||
cost_category = expense.cost_type
|
||||
|
||||
# Prepare data JSON for extra fields (mileage_at_cost, description, etc.)
|
||||
data = expense.data.copy() if expense.data else {}
|
||||
if expense.mileage_at_cost is not None:
|
||||
data["mileage_at_cost"] = expense.mileage_at_cost
|
||||
if expense.description:
|
||||
data["description"] = expense.description
|
||||
|
||||
# Create AssetCost instance
|
||||
new_cost = AssetCost(
|
||||
asset_id=expense.asset_id,
|
||||
cost_category=expense.category,
|
||||
amount_net=expense.amount,
|
||||
currency="HUF",
|
||||
organization_id=organization_id,
|
||||
cost_category=cost_category,
|
||||
amount_net=expense.amount_local,
|
||||
currency=expense.currency_local,
|
||||
date=expense.date,
|
||||
organization_id=organization_id
|
||||
invoice_number=data.get("invoice_number"),
|
||||
data=data
|
||||
)
|
||||
|
||||
db.add(new_cost)
|
||||
await db.commit()
|
||||
return {"status": "success"}
|
||||
await db.refresh(new_cost)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"id": new_cost.id,
|
||||
"asset_id": new_cost.asset_id,
|
||||
"cost_category": new_cost.cost_category,
|
||||
"amount_net": new_cost.amount_net,
|
||||
"date": new_cost.date
|
||||
}
|
||||
@@ -472,4 +472,455 @@ async def get_leaderboard_top10(
|
||||
current_level=stats.current_level
|
||||
)
|
||||
)
|
||||
return leaderboard
|
||||
return leaderboard
|
||||
|
||||
|
||||
# --- QUIZ ENDPOINTS FOR DAILY QUIZ GAMIFICATION ---
|
||||
|
||||
@router.get("/quiz/daily")
|
||||
async def get_daily_quiz(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Returns daily quiz questions for the user.
|
||||
Checks if user has already played today.
|
||||
"""
|
||||
# Check if user has already played today
|
||||
today = datetime.now().date()
|
||||
stmt = select(PointsLedger).where(
|
||||
PointsLedger.user_id == current_user.id,
|
||||
func.date(PointsLedger.created_at) == today,
|
||||
PointsLedger.reason.ilike("%quiz%")
|
||||
)
|
||||
result = await db.execute(stmt)
|
||||
already_played = result.scalar_one_or_none()
|
||||
|
||||
if already_played:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="You have already played the daily quiz today. Try again tomorrow."
|
||||
)
|
||||
|
||||
# Return quiz questions (for now, using mock questions - in production these would come from a database)
|
||||
quiz_questions = [
|
||||
{
|
||||
"id": 1,
|
||||
"question": "Melyik alkatrész felelős a motor levegő‑üzemanyag keverékének szabályozásáért?",
|
||||
"options": ["Generátor", "Lambda‑szonda", "Féktárcsa", "Olajszűrő"],
|
||||
"correctAnswer": 1,
|
||||
"explanation": "A lambda‑szonda méri a kipufogógáz oxigéntartalmát, és ezen alapul a befecskendezés."
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"question": "Mennyi ideig érvényes egy gépjármű műszaki vizsgája Magyarországon?",
|
||||
"options": ["1 év", "2 év", "4 év", "6 év"],
|
||||
"correctAnswer": 1,
|
||||
"explanation": "A személygépkocsik műszaki vizsgája 2 évre érvényes, kivéve az újonnan forgalomba helyezett autókat."
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"question": "Melyik anyag NEM része a hibrid autók akkumulátorának?",
|
||||
"options": ["Lítium", "Nikkel", "Ólom", "Kobalt"],
|
||||
"correctAnswer": 2,
|
||||
"explanation": "A hibrid és elektromos autók akkumulátoraiban általában lítium, nikkel és kobalt található, ólom az ólom‑savas akkukban van."
|
||||
}
|
||||
]
|
||||
|
||||
return {
|
||||
"questions": quiz_questions,
|
||||
"total_questions": len(quiz_questions),
|
||||
"date": today.isoformat()
|
||||
}
|
||||
|
||||
|
||||
@router.post("/quiz/answer")
|
||||
async def submit_quiz_answer(
|
||||
question_id: int = Body(...),
|
||||
selected_option: int = Body(...),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Submit answer to a quiz question and award points if correct.
|
||||
"""
|
||||
# Check if user has already played today
|
||||
today = datetime.now().date()
|
||||
stmt = select(PointsLedger).where(
|
||||
PointsLedger.user_id == current_user.id,
|
||||
func.date(PointsLedger.created_at) == today,
|
||||
PointsLedger.reason.ilike("%quiz%")
|
||||
)
|
||||
result = await db.execute(stmt)
|
||||
already_played = result.scalar_one_or_none()
|
||||
|
||||
if already_played:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="You have already played the daily quiz today. Try again tomorrow."
|
||||
)
|
||||
|
||||
# Mock quiz data - in production this would come from a database
|
||||
quiz_data = {
|
||||
1: {"correct_answer": 1, "points": 10, "explanation": "A lambda‑szonda méri a kipufogógáz oxigéntartalmát, és ezen alapul a befecskendezés."},
|
||||
2: {"correct_answer": 1, "points": 10, "explanation": "A személygépkocsik műszaki vizsgája 2 évre érvényes, kivéve az újonnan forgalomba helyezett autókat."},
|
||||
3: {"correct_answer": 2, "points": 10, "explanation": "A hibrid és elektromos autók akkumulátoraiban általában lítium, nikkel és kobalt található, ólom az ólom‑savas akkukban van."}
|
||||
}
|
||||
|
||||
if question_id not in quiz_data:
|
||||
raise HTTPException(status_code=404, detail="Question not found")
|
||||
|
||||
question_info = quiz_data[question_id]
|
||||
is_correct = selected_option == question_info["correct_answer"]
|
||||
|
||||
# Award points if correct
|
||||
if is_correct:
|
||||
# Update user stats
|
||||
stats_stmt = select(UserStats).where(UserStats.user_id == current_user.id)
|
||||
stats_result = await db.execute(stats_stmt)
|
||||
user_stats = stats_result.scalar_one_or_none()
|
||||
|
||||
if not user_stats:
|
||||
# Create user stats if they don't exist
|
||||
user_stats = UserStats(
|
||||
user_id=current_user.id,
|
||||
total_xp=question_info["points"],
|
||||
current_level=1
|
||||
)
|
||||
db.add(user_stats)
|
||||
else:
|
||||
user_stats.total_xp += question_info["points"]
|
||||
|
||||
# Add points ledger entry
|
||||
points_ledger = PointsLedger(
|
||||
user_id=current_user.id,
|
||||
points=question_info["points"],
|
||||
reason=f"Daily quiz correct answer - Question {question_id}",
|
||||
created_at=datetime.now()
|
||||
)
|
||||
db.add(points_ledger)
|
||||
|
||||
await db.commit()
|
||||
|
||||
return {
|
||||
"is_correct": is_correct,
|
||||
"correct_answer": question_info["correct_answer"],
|
||||
"points_awarded": question_info["points"] if is_correct else 0,
|
||||
"explanation": question_info["explanation"]
|
||||
}
|
||||
|
||||
|
||||
@router.post("/quiz/complete")
|
||||
async def complete_daily_quiz(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Mark daily quiz as completed for today.
|
||||
This prevents the user from playing again today.
|
||||
"""
|
||||
today = datetime.now().date()
|
||||
|
||||
# Check if already completed today
|
||||
stmt = select(PointsLedger).where(
|
||||
PointsLedger.user_id == current_user.id,
|
||||
func.date(PointsLedger.created_at) == today,
|
||||
PointsLedger.reason == "Daily quiz completed"
|
||||
)
|
||||
result = await db.execute(stmt)
|
||||
already_completed = result.scalar_one_or_none()
|
||||
|
||||
if already_completed:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Daily quiz already marked as completed today."
|
||||
)
|
||||
|
||||
# Add completion entry
|
||||
completion_ledger = PointsLedger(
|
||||
user_id=current_user.id,
|
||||
points=0,
|
||||
reason="Daily quiz completed",
|
||||
created_at=datetime.now()
|
||||
)
|
||||
db.add(completion_ledger)
|
||||
await db.commit()
|
||||
|
||||
return {"message": "Daily quiz marked as completed for today."}
|
||||
|
||||
|
||||
@router.get("/quiz/stats")
|
||||
async def get_quiz_stats(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Get user's quiz statistics including points, streak, and last played date.
|
||||
"""
|
||||
# Get user stats
|
||||
stats_stmt = select(UserStats).where(UserStats.user_id == current_user.id)
|
||||
stats_result = await db.execute(stats_stmt)
|
||||
user_stats = stats_result.scalar_one_or_none()
|
||||
|
||||
# Get quiz points from ledger
|
||||
points_stmt = select(func.sum(PointsLedger.points)).where(
|
||||
PointsLedger.user_id == current_user.id,
|
||||
PointsLedger.reason.ilike("%quiz%")
|
||||
)
|
||||
points_result = await db.execute(points_stmt)
|
||||
quiz_points = points_result.scalar() or 0
|
||||
|
||||
# Get last played date
|
||||
last_played_stmt = select(PointsLedger.created_at).where(
|
||||
PointsLedger.user_id == current_user.id,
|
||||
PointsLedger.reason.ilike("%quiz%")
|
||||
).order_by(desc(PointsLedger.created_at)).limit(1)
|
||||
last_played_result = await db.execute(last_played_stmt)
|
||||
last_played = last_played_result.scalar()
|
||||
|
||||
# Calculate streak (simplified - in production would be more sophisticated)
|
||||
streak = 0
|
||||
if last_played:
|
||||
# Simple streak calculation - check last 7 days
|
||||
streak = 1 # Placeholder
|
||||
|
||||
return {
|
||||
"total_quiz_points": quiz_points,
|
||||
"total_xp": user_stats.total_xp if user_stats else 0,
|
||||
"current_level": user_stats.current_level if user_stats else 1,
|
||||
"last_played": last_played.isoformat() if last_played else None,
|
||||
"current_streak": streak,
|
||||
"can_play_today": not await has_played_today(db, current_user.id)
|
||||
}
|
||||
|
||||
|
||||
async def has_played_today(db: AsyncSession, user_id: int) -> bool:
|
||||
"""Check if user has already played quiz today."""
|
||||
today = datetime.now().date()
|
||||
stmt = select(PointsLedger).where(
|
||||
PointsLedger.user_id == user_id,
|
||||
func.date(PointsLedger.created_at) == today,
|
||||
PointsLedger.reason.ilike("%quiz%")
|
||||
)
|
||||
result = await db.execute(stmt)
|
||||
return result.scalar_one_or_none() is not None
|
||||
|
||||
|
||||
# --- BADGE/TROPHY ENDPOINTS ---
|
||||
|
||||
@router.get("/badges")
|
||||
async def get_all_badges(
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Get all available badges in the system.
|
||||
"""
|
||||
stmt = select(Badge)
|
||||
result = await db.execute(stmt)
|
||||
badges = result.scalars().all()
|
||||
|
||||
return [
|
||||
{
|
||||
"id": badge.id,
|
||||
"name": badge.name,
|
||||
"description": badge.description,
|
||||
"icon_url": badge.icon_url
|
||||
}
|
||||
for badge in badges
|
||||
]
|
||||
|
||||
|
||||
@router.get("/my-badges")
|
||||
async def get_my_badges(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Get badges earned by the current user.
|
||||
"""
|
||||
stmt = (
|
||||
select(UserBadge, Badge)
|
||||
.join(Badge, UserBadge.badge_id == Badge.id)
|
||||
.where(UserBadge.user_id == current_user.id)
|
||||
.order_by(desc(UserBadge.earned_at))
|
||||
)
|
||||
result = await db.execute(stmt)
|
||||
user_badges = result.all()
|
||||
|
||||
return [
|
||||
{
|
||||
"badge_id": badge.id,
|
||||
"badge_name": badge.name,
|
||||
"badge_description": badge.description,
|
||||
"badge_icon_url": badge.icon_url,
|
||||
"earned_at": user_badge.earned_at.isoformat() if user_badge.earned_at else None
|
||||
}
|
||||
for user_badge, badge in user_badges
|
||||
]
|
||||
|
||||
|
||||
@router.post("/badges/award/{badge_id}")
|
||||
async def award_badge_to_user(
|
||||
badge_id: int,
|
||||
user_id: int = Body(None),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Award a badge to a user (admin only or automated system).
|
||||
"""
|
||||
# Check if badge exists
|
||||
badge_stmt = select(Badge).where(Badge.id == badge_id)
|
||||
badge_result = await db.execute(badge_stmt)
|
||||
badge = badge_result.scalar_one_or_none()
|
||||
|
||||
if not badge:
|
||||
raise HTTPException(status_code=404, detail="Badge not found")
|
||||
|
||||
# Determine target user (default to current user if not specified)
|
||||
target_user_id = user_id if user_id else current_user.id
|
||||
|
||||
# Check if user already has this badge
|
||||
existing_stmt = select(UserBadge).where(
|
||||
UserBadge.user_id == target_user_id,
|
||||
UserBadge.badge_id == badge_id
|
||||
)
|
||||
existing_result = await db.execute(existing_stmt)
|
||||
existing = existing_result.scalar_one_or_none()
|
||||
|
||||
if existing:
|
||||
raise HTTPException(status_code=400, detail="User already has this badge")
|
||||
|
||||
# Award the badge
|
||||
user_badge = UserBadge(
|
||||
user_id=target_user_id,
|
||||
badge_id=badge_id,
|
||||
earned_at=datetime.now()
|
||||
)
|
||||
db.add(user_badge)
|
||||
|
||||
# Also add points for earning a badge
|
||||
points_ledger = PointsLedger(
|
||||
user_id=target_user_id,
|
||||
points=50, # Points for earning a badge
|
||||
reason=f"Badge earned: {badge.name}",
|
||||
created_at=datetime.now()
|
||||
)
|
||||
db.add(points_ledger)
|
||||
|
||||
# Update user stats
|
||||
stats_stmt = select(UserStats).where(UserStats.user_id == target_user_id)
|
||||
stats_result = await db.execute(stats_stmt)
|
||||
user_stats = stats_result.scalar_one_or_none()
|
||||
|
||||
if user_stats:
|
||||
user_stats.total_xp += 50
|
||||
else:
|
||||
user_stats = UserStats(
|
||||
user_id=target_user_id,
|
||||
total_xp=50,
|
||||
current_level=1
|
||||
)
|
||||
db.add(user_stats)
|
||||
|
||||
await db.commit()
|
||||
|
||||
return {
|
||||
"message": f"Badge '{badge.name}' awarded to user",
|
||||
"badge_id": badge.id,
|
||||
"badge_name": badge.name,
|
||||
"points_awarded": 50
|
||||
}
|
||||
|
||||
|
||||
@router.get("/achievements")
|
||||
async def get_achievements_progress(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Get user's progress on various achievements (combines badges and other metrics).
|
||||
"""
|
||||
# Get user badges
|
||||
badges_stmt = select(UserBadge.badge_id).where(UserBadge.user_id == current_user.id)
|
||||
badges_result = await db.execute(badges_stmt)
|
||||
user_badge_ids = [row[0] for row in badges_result.all()]
|
||||
|
||||
# Get all badges
|
||||
all_badges_stmt = select(Badge)
|
||||
all_badges_result = await db.execute(all_badges_stmt)
|
||||
all_badges = all_badges_result.scalars().all()
|
||||
|
||||
# Get user stats
|
||||
stats_stmt = select(UserStats).where(UserStats.user_id == current_user.id)
|
||||
stats_result = await db.execute(stats_stmt)
|
||||
user_stats = stats_result.scalar_one_or_none()
|
||||
|
||||
# Define achievement categories
|
||||
achievements = []
|
||||
|
||||
# Badge-based achievements
|
||||
for badge in all_badges:
|
||||
achievements.append({
|
||||
"id": f"badge_{badge.id}",
|
||||
"title": badge.name,
|
||||
"description": badge.description,
|
||||
"icon_url": badge.icon_url,
|
||||
"is_earned": badge.id in user_badge_ids,
|
||||
"category": "badge",
|
||||
"progress": 100 if badge.id in user_badge_ids else 0
|
||||
})
|
||||
|
||||
# XP-based achievements
|
||||
xp_levels = [
|
||||
{"title": "Novice", "xp_required": 100, "description": "Earn 100 XP"},
|
||||
{"title": "Apprentice", "xp_required": 500, "description": "Earn 500 XP"},
|
||||
{"title": "Expert", "xp_required": 2000, "description": "Earn 2000 XP"},
|
||||
{"title": "Master", "xp_required": 5000, "description": "Earn 5000 XP"},
|
||||
]
|
||||
|
||||
current_xp = user_stats.total_xp if user_stats else 0
|
||||
for level in xp_levels:
|
||||
progress = min((current_xp / level["xp_required"]) * 100, 100)
|
||||
achievements.append({
|
||||
"id": f"xp_{level['xp_required']}",
|
||||
"title": level["title"],
|
||||
"description": level["description"],
|
||||
"icon_url": None,
|
||||
"is_earned": current_xp >= level["xp_required"],
|
||||
"category": "xp",
|
||||
"progress": progress
|
||||
})
|
||||
|
||||
# Quiz-based achievements
|
||||
quiz_points_stmt = select(func.sum(PointsLedger.points)).where(
|
||||
PointsLedger.user_id == current_user.id,
|
||||
PointsLedger.reason.ilike("%quiz%")
|
||||
)
|
||||
quiz_points_result = await db.execute(quiz_points_stmt)
|
||||
quiz_points = quiz_points_result.scalar() or 0
|
||||
|
||||
quiz_achievements = [
|
||||
{"title": "Quiz Beginner", "points_required": 50, "description": "Earn 50 quiz points"},
|
||||
{"title": "Quiz Enthusiast", "points_required": 200, "description": "Earn 200 quiz points"},
|
||||
{"title": "Quiz Master", "points_required": 500, "description": "Earn 500 quiz points"},
|
||||
]
|
||||
|
||||
for achievement in quiz_achievements:
|
||||
progress = min((quiz_points / achievement["points_required"]) * 100, 100)
|
||||
achievements.append({
|
||||
"id": f"quiz_{achievement['points_required']}",
|
||||
"title": achievement["title"],
|
||||
"description": achievement["description"],
|
||||
"icon_url": None,
|
||||
"is_earned": quiz_points >= achievement["points_required"],
|
||||
"category": "quiz",
|
||||
"progress": progress
|
||||
})
|
||||
|
||||
return {
|
||||
"achievements": achievements,
|
||||
"total_achievements": len(achievements),
|
||||
"earned_count": sum(1 for a in achievements if a["is_earned"]),
|
||||
"progress_percentage": round((sum(1 for a in achievements if a["is_earned"]) / len(achievements)) * 100, 1) if achievements else 0
|
||||
}
|
||||
@@ -37,7 +37,7 @@ async def get_monthly_trends(vehicle_id: str, db: AsyncSession = Depends(get_db)
|
||||
Visszaadja az utolsó 6 hónap költéseit havi bontásban.
|
||||
"""
|
||||
query = text("""
|
||||
SELECT
|
||||
SELECT
|
||||
TO_CHAR(date, 'YYYY-MM') as month,
|
||||
SUM(amount) as monthly_total
|
||||
FROM vehicle.vehicle_expenses
|
||||
@@ -47,4 +47,19 @@ async def get_monthly_trends(vehicle_id: str, db: AsyncSession = Depends(get_db)
|
||||
LIMIT 6
|
||||
""")
|
||||
result = await db.execute(query, {"v_id": vehicle_id})
|
||||
return [dict(row._mapping) for row in result.fetchall()]
|
||||
return [dict(row._mapping) for row in result.fetchall()]
|
||||
|
||||
@router.get("/summary/latest")
|
||||
async def get_latest_summary(db: AsyncSession = Depends(get_db), current_user = Depends(get_current_user)):
|
||||
"""
|
||||
Returns a simple summary for the dashboard (mock data for now).
|
||||
This endpoint is called by the frontend dashboard.
|
||||
"""
|
||||
# For now, return mock data to satisfy the frontend
|
||||
return {
|
||||
"total_vehicles": 4,
|
||||
"total_cost_this_month": 1250.50,
|
||||
"most_expensive_category": "Fuel",
|
||||
"trend": "down",
|
||||
"trend_percentage": -5.2
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
#/opt/docker/dev/service_finder/backend/app/api/v1/endpoints/users.py
|
||||
from fastapi import APIRouter, Depends
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from typing import Dict, Any
|
||||
|
||||
from app.api.deps import get_db, get_current_user
|
||||
from app.schemas.user import UserResponse
|
||||
from app.schemas.user import UserResponse, UserUpdate
|
||||
from app.models.identity import User
|
||||
from app.services.trust_engine import TrustEngine
|
||||
|
||||
@@ -41,3 +41,34 @@ async def get_user_trust(
|
||||
force_recalculate=force_recalculate
|
||||
)
|
||||
return trust_data
|
||||
|
||||
|
||||
@router.patch("/me/preferences", response_model=UserResponse)
|
||||
async def update_user_preferences(
|
||||
update_data: UserUpdate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
Update user preferences (ui_mode, preferred_language, etc.)
|
||||
"""
|
||||
# Filter out None values
|
||||
update_dict = update_data.dict(exclude_unset=True)
|
||||
if not update_dict:
|
||||
raise HTTPException(status_code=400, detail="No fields to update")
|
||||
|
||||
# Validate ui_mode if present
|
||||
if "ui_mode" in update_dict:
|
||||
if update_dict["ui_mode"] not in ["personal", "fleet"]:
|
||||
raise HTTPException(status_code=422, detail="ui_mode must be 'personal' or 'fleet'")
|
||||
|
||||
# Update user fields
|
||||
for field, value in update_dict.items():
|
||||
if hasattr(current_user, field):
|
||||
setattr(current_user, field, value)
|
||||
else:
|
||||
raise HTTPException(status_code=400, detail=f"Invalid field: {field}")
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(current_user)
|
||||
return current_user
|
||||
|
||||
Reference in New Issue
Block a user