Time tracker app

This commit is contained in:
2026-01-10 08:24:55 +07:00
parent 3256c40841
commit b6eecc4483
46 changed files with 11368 additions and 2 deletions

View File

@@ -15,6 +15,7 @@ from app.models import (
from app.schemas import (
SpinResult, AssignmentResponse, CompleteResult, DropResult,
GameResponse, ChallengeResponse, GameShort, UserPublic, MessageResponse,
TrackTimeRequest,
)
from app.schemas.game import PlaythroughInfo
from app.services.points import PointsService
@@ -589,7 +590,14 @@ async def complete_assignment(
if assignment.is_playthrough:
game = assignment.game
marathon_id = game.marathon_id
base_playthrough_points = game.playthrough_points
# If tracked time exists (from desktop app), calculate points as hours * 30
# Otherwise use admin-set playthrough_points
if assignment.tracked_time_minutes > 0:
hours = assignment.tracked_time_minutes / 60
base_playthrough_points = int(hours * 30)
else:
base_playthrough_points = game.playthrough_points
# Calculate BASE bonus points from completed bonus assignments (before multiplier)
base_bonus_points = sum(
@@ -850,6 +858,37 @@ async def complete_assignment(
)
@router.patch("/assignments/{assignment_id}/track-time", response_model=MessageResponse)
async def track_assignment_time(
assignment_id: int,
data: TrackTimeRequest,
current_user: CurrentUser,
db: DbSession,
):
"""Update tracked time for an assignment (from desktop app)"""
result = await db.execute(
select(Assignment)
.options(selectinload(Assignment.participant))
.where(Assignment.id == assignment_id)
)
assignment = result.scalar_one_or_none()
if not assignment:
raise HTTPException(status_code=404, detail="Assignment not found")
if assignment.participant.user_id != current_user.id:
raise HTTPException(status_code=403, detail="This is not your assignment")
if assignment.status != AssignmentStatus.ACTIVE.value:
raise HTTPException(status_code=400, detail="Assignment is not active")
# Update tracked time (replace with new value)
assignment.tracked_time_minutes = max(0, data.minutes)
await db.commit()
return MessageResponse(message=f"Tracked time updated to {data.minutes} minutes")
@router.post("/assignments/{assignment_id}/drop", response_model=DropResult)
async def drop_assignment(assignment_id: int, current_user: CurrentUser, db: DbSession):
"""Drop current assignment"""

View File

@@ -60,7 +60,12 @@ app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
# CORS
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:3000", "http://127.0.0.1:3000"],
allow_origins=[
"http://localhost:3000",
"http://127.0.0.1:3000",
"http://localhost:5173", # Desktop app dev
"http://127.0.0.1:5173",
],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],

View File

@@ -32,6 +32,7 @@ class Assignment(Base):
proof_comment: Mapped[str | None] = mapped_column(Text, nullable=True)
points_earned: Mapped[int] = mapped_column(Integer, default=0)
streak_at_completion: Mapped[int | None] = mapped_column(Integer, nullable=True)
tracked_time_minutes: Mapped[int] = mapped_column(Integer, default=0) # Time tracked by desktop app
started_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
completed_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)

View File

@@ -52,6 +52,7 @@ from app.schemas.assignment import (
CompleteBonusAssignment,
BonusCompleteResult,
AvailableGamesCount,
TrackTimeRequest,
)
from app.schemas.activity import (
ActivityResponse,

View File

@@ -52,6 +52,7 @@ class AssignmentResponse(BaseModel):
proof_comment: str | None = None
points_earned: int
streak_at_completion: int | None = None
tracked_time_minutes: int = 0 # Time tracked by desktop app
started_at: datetime
completed_at: datetime | None = None
drop_penalty: int = 0 # Calculated penalty if dropped
@@ -62,6 +63,11 @@ class AssignmentResponse(BaseModel):
from_attributes = True
class TrackTimeRequest(BaseModel):
"""Request to update tracked time for an assignment"""
minutes: int # Total minutes tracked (replaces previous value)
class SpinResult(BaseModel):
assignment_id: int
game: GameResponse