diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py index 0d01900..0df98ae 100644 --- a/backend/app/models/__init__.py +++ b/backend/app/models/__init__.py @@ -6,6 +6,7 @@ from app.models.subject import Subject, SubjectOption from app.models.schedule import ScheduleTemplate, ScheduleBlock from app.models.session import DailySession, TimerEvent, TimerEventType from app.models.activity import ActivityLog +from app.models.strike import StrikeEvent __all__ = [ "Base", @@ -20,4 +21,5 @@ __all__ = [ "TimerEvent", "TimerEventType", "ActivityLog", + "StrikeEvent", ] diff --git a/backend/app/models/strike.py b/backend/app/models/strike.py new file mode 100644 index 0000000..cce6080 --- /dev/null +++ b/backend/app/models/strike.py @@ -0,0 +1,18 @@ +from datetime import datetime + +from sqlalchemy import DateTime, ForeignKey, Integer, func +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from app.models.base import Base + + +class StrikeEvent(Base): + __tablename__ = "strike_events" + + id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) + child_id: Mapped[int] = mapped_column(ForeignKey("children.id", ondelete="CASCADE"), nullable=False) + occurred_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now(), nullable=False) + prev_strikes: Mapped[int] = mapped_column(Integer, nullable=False) + new_strikes: Mapped[int] = mapped_column(Integer, nullable=False) + + child: Mapped["Child"] = relationship("Child") # noqa: F821 diff --git a/backend/app/routers/children.py b/backend/app/routers/children.py index 9a9507a..60cea7d 100644 --- a/backend/app/routers/children.py +++ b/backend/app/routers/children.py @@ -5,6 +5,7 @@ from sqlalchemy import select from app.dependencies import get_db, get_current_user from app.models.child import Child +from app.models.strike import StrikeEvent from app.models.user import User from app.schemas.child import ChildCreate, ChildOut, ChildUpdate from app.websocket.manager import manager @@ -89,7 +90,9 @@ async def update_strikes( child = result.scalar_one_or_none() if not child: raise HTTPException(status_code=404, detail="Child not found") + prev = child.strikes child.strikes = body.strikes + db.add(StrikeEvent(child_id=child.id, prev_strikes=prev, new_strikes=body.strikes)) await db.commit() await db.refresh(child) await manager.broadcast(child_id, {"event": "strikes_update", "strikes": child.strikes}) diff --git a/backend/app/routers/logs.py b/backend/app/routers/logs.py index 9c1dac8..207e0bf 100644 --- a/backend/app/routers/logs.py +++ b/backend/app/routers/logs.py @@ -2,7 +2,7 @@ from datetime import date from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy.ext.asyncio import AsyncSession -from sqlalchemy import select +from sqlalchemy import func, select from sqlalchemy.orm import selectinload from app.dependencies import get_db, get_current_user @@ -10,9 +10,10 @@ from app.models.activity import ActivityLog from app.models.child import Child from app.models.session import DailySession, TimerEvent from app.models.schedule import ScheduleBlock +from app.models.strike import StrikeEvent from app.models.subject import Subject # noqa: F401 from app.models.user import User -from app.schemas.activity import ActivityLogCreate, ActivityLogOut, ActivityLogUpdate, TimelineEventOut, TimelineEventUpdate +from app.schemas.activity import ActivityLogCreate, ActivityLogOut, ActivityLogUpdate, StrikeEventOut, TimelineEventOut, TimelineEventUpdate router = APIRouter(prefix="/api/logs", tags=["logs"]) @@ -119,6 +120,59 @@ async def delete_timeline_event( await db.commit() +@router.get("/strikes", response_model=list[StrikeEventOut]) +async def get_strike_events( + child_id: int | None = None, + log_date: date | None = None, + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +): + query = ( + select(StrikeEvent) + .join(Child, StrikeEvent.child_id == Child.id) + .where(Child.user_id == current_user.id) + .options(selectinload(StrikeEvent.child)) + .order_by(StrikeEvent.occurred_at.desc()) + ) + if child_id: + query = query.where(StrikeEvent.child_id == child_id) + if log_date: + query = query.where(func.date(StrikeEvent.occurred_at) == log_date) + + result = await db.execute(query) + events = result.scalars().all() + return [ + StrikeEventOut( + id=e.id, + child_id=e.child_id, + child_name=e.child.name, + occurred_at=e.occurred_at, + log_date=e.occurred_at.date(), + prev_strikes=e.prev_strikes, + new_strikes=e.new_strikes, + ) + for e in events + ] + + +@router.delete("/strikes/{strike_id}", status_code=status.HTTP_204_NO_CONTENT) +async def delete_strike_event( + strike_id: int, + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +): + result = await db.execute( + select(StrikeEvent) + .join(Child, StrikeEvent.child_id == Child.id) + .where(StrikeEvent.id == strike_id, Child.user_id == current_user.id) + ) + event = result.scalar_one_or_none() + if not event: + raise HTTPException(status_code=404, detail="Strike event not found") + await db.delete(event) + await db.commit() + + @router.get("", response_model=list[ActivityLogOut]) async def list_logs( child_id: int | None = None, diff --git a/backend/app/schemas/activity.py b/backend/app/schemas/activity.py index 914daff..31885bc 100644 --- a/backend/app/schemas/activity.py +++ b/backend/app/schemas/activity.py @@ -1,5 +1,5 @@ from datetime import date, datetime, timezone -from pydantic import BaseModel, field_serializer +from pydantic import BaseModel, computed_field, field_serializer class ActivityLogCreate(BaseModel): @@ -51,3 +51,21 @@ class TimelineEventOut(BaseModel): if dt.tzinfo is None: dt = dt.replace(tzinfo=timezone.utc) return dt.isoformat() + + +class StrikeEventOut(BaseModel): + id: int + child_id: int + child_name: str + occurred_at: datetime + log_date: date + prev_strikes: int + new_strikes: int + + model_config = {"from_attributes": True} + + @field_serializer("occurred_at") + def serialize_occurred_at(self, dt: datetime) -> str: + if dt.tzinfo is None: + dt = dt.replace(tzinfo=timezone.utc) + return dt.isoformat() diff --git a/frontend/src/views/LogView.vue b/frontend/src/views/LogView.vue index 4b66a5d..f8fdd65 100644 --- a/frontend/src/views/LogView.vue +++ b/frontend/src/views/LogView.vue @@ -85,7 +85,7 @@