diff --git a/src/baby_monitor/main.py b/src/baby_monitor/main.py index c4082dc..1d6a848 100644 --- a/src/baby_monitor/main.py +++ b/src/baby_monitor/main.py @@ -12,6 +12,7 @@ from fastapi.staticfiles import StaticFiles from baby_monitor.routers.auth import router as auth_router from baby_monitor.routers.auth import verify_token from baby_monitor.routers.admin import router as admin_router +from baby_monitor.routers.child import router as child_router from baby_monitor.routers.health import router as health_router from baby_monitor.repositories.dependencies.get_database import init_db @@ -47,6 +48,7 @@ app.mount("/static", StaticFiles(directory=str(static_path)), name="static") # Include routers app.include_router(auth_router) app.include_router(admin_router) +app.include_router(child_router) app.include_router(health_router) @@ -75,6 +77,12 @@ def serve_admin() -> FileResponse: return FileResponse(static_path / "admin.html") +@app.get("/add-child.html", include_in_schema=False) +def serve_add_child() -> FileResponse: + """Serve the add child page.""" + return FileResponse(static_path / "add-child.html") + + @app.get("/api/") def read_root(token: Annotated[str, Depends(verify_token)]) -> dict: """API root endpoint (requires authentication).""" diff --git a/src/baby_monitor/models/child.py b/src/baby_monitor/models/child.py new file mode 100644 index 0000000..86096e7 --- /dev/null +++ b/src/baby_monitor/models/child.py @@ -0,0 +1,23 @@ +"""Child-related request and response models.""" + +from datetime import datetime +from pydantic import BaseModel, Field + + +class CreateChildRequest(BaseModel): + """Request model for creating a child.""" + + name: str = Field(..., min_length=1, max_length=100) + birth_time: datetime + birth_weight: float = Field(..., gt=0, description="Birth weight in grams") + + +class ChildResponse(BaseModel): + """Response model for child data.""" + + id: int + name: str + birth_time: datetime + birth_weight: float + user_id: int + created_at: datetime diff --git a/src/baby_monitor/models/db/child.py b/src/baby_monitor/models/db/child.py new file mode 100644 index 0000000..7a633ae --- /dev/null +++ b/src/baby_monitor/models/db/child.py @@ -0,0 +1,28 @@ +"""Child database model.""" + +from typing import TYPE_CHECKING +from sqlalchemy import Column, Integer, String, DateTime, Float, ForeignKey +from datetime import datetime + +if TYPE_CHECKING: + from sqlalchemy.orm import DeclarativeBase + + Base = DeclarativeBase +else: + from baby_monitor.repositories.dependencies.get_database import Base + + +class Child(Base): + """Child database model.""" + + __tablename__ = "children" + + id = Column(Integer, primary_key=True, index=True) + name = Column(String, nullable=False) + birth_time = Column(DateTime, nullable=False) + birth_weight = Column(Float, nullable=False) # in grams + user_id = Column(Integer, ForeignKey("users.id"), nullable=False) + created_at = Column(DateTime, default=datetime.utcnow, nullable=False) + + def __repr__(self) -> str: + return f"" diff --git a/src/baby_monitor/repositories/child/sqlite_child.py b/src/baby_monitor/repositories/child/sqlite_child.py new file mode 100644 index 0000000..e18d2dc --- /dev/null +++ b/src/baby_monitor/repositories/child/sqlite_child.py @@ -0,0 +1,111 @@ +"""SQLite implementation of child repository.""" + +from datetime import datetime +from sqlalchemy.orm import Session + +from baby_monitor.repositories.interfaces.child_repository_interface import ( + ChildRepositoryInterface, +) +from baby_monitor.models.db.child import Child + + +class SQLiteChildRepository(ChildRepositoryInterface): + """SQLite implementation for child data access.""" + + def __init__(self, db: Session): + self.db = db + + def create( + self, name: str, birth_time: datetime, birth_weight: float, user_id: int + ) -> dict: + """Create a new child record.""" + child = Child( + name=name, + birth_time=birth_time, + birth_weight=birth_weight, + user_id=user_id, + ) + self.db.add(child) + self.db.commit() + self.db.refresh(child) + + return { + "id": child.id, + "name": child.name, + "birth_time": child.birth_time, + "birth_weight": child.birth_weight, + "user_id": child.user_id, + "created_at": child.created_at, + } + + def get_by_id(self, child_id: int) -> dict | None: + """Get a child by ID.""" + child = self.db.query(Child).filter(Child.id == child_id).first() + if not child: + return None + + return { + "id": child.id, + "name": child.name, + "birth_time": child.birth_time, + "birth_weight": child.birth_weight, + "user_id": child.user_id, + "created_at": child.created_at, + } + + def get_by_user_id(self, user_id: int) -> list[dict]: + """Get all children for a specific user.""" + children = self.db.query(Child).filter(Child.user_id == user_id).all() + + return [ + { + "id": child.id, + "name": child.name, + "birth_time": child.birth_time, + "birth_weight": child.birth_weight, + "user_id": child.user_id, + "created_at": child.created_at, + } + for child in children + ] + + def update( + self, + child_id: int, + name: str | None = None, + birth_time: datetime | None = None, + birth_weight: float | None = None, + ) -> dict | None: + """Update a child record.""" + child = self.db.query(Child).filter(Child.id == child_id).first() + if not child: + return None + + if name is not None: + child.name = name # type: ignore[assignment] + if birth_time is not None: + child.birth_time = birth_time # type: ignore[assignment] + if birth_weight is not None: + child.birth_weight = birth_weight # type: ignore[assignment] + + self.db.commit() + self.db.refresh(child) + + return { + "id": child.id, + "name": child.name, + "birth_time": child.birth_time, + "birth_weight": child.birth_weight, + "user_id": child.user_id, + "created_at": child.created_at, + } + + def delete(self, child_id: int) -> bool: + """Delete a child record.""" + child = self.db.query(Child).filter(Child.id == child_id).first() + if not child: + return False + + self.db.delete(child) + self.db.commit() + return True diff --git a/src/baby_monitor/repositories/dependencies/get_child_repository.py b/src/baby_monitor/repositories/dependencies/get_child_repository.py new file mode 100644 index 0000000..a5d677b --- /dev/null +++ b/src/baby_monitor/repositories/dependencies/get_child_repository.py @@ -0,0 +1,15 @@ +"""Dependency for getting child repository instance.""" + +from typing import Annotated +from fastapi import Depends +from sqlalchemy.orm import Session + +from baby_monitor.repositories.dependencies.get_database import get_database +from baby_monitor.repositories.child.sqlite_child import SQLiteChildRepository + + +def get_child_repository( + db: Annotated[Session, Depends(get_database)], +) -> SQLiteChildRepository: + """Get child repository instance.""" + return SQLiteChildRepository(db) diff --git a/src/baby_monitor/repositories/dependencies/get_database.py b/src/baby_monitor/repositories/dependencies/get_database.py index 6f465e1..ec35bcf 100644 --- a/src/baby_monitor/repositories/dependencies/get_database.py +++ b/src/baby_monitor/repositories/dependencies/get_database.py @@ -77,6 +77,7 @@ def init_db() -> None: # Import models to register them with Base.metadata # This must be done before create_all() is called from baby_monitor.models.db.user import User # noqa: F401 + from baby_monitor.models.db.child import Child # noqa: F401 from baby_monitor.models.invitation import Invitation # noqa: F401 # Ensure data directory exists diff --git a/src/baby_monitor/repositories/interfaces/child_repository_interface.py b/src/baby_monitor/repositories/interfaces/child_repository_interface.py new file mode 100644 index 0000000..0f04844 --- /dev/null +++ b/src/baby_monitor/repositories/interfaces/child_repository_interface.py @@ -0,0 +1,36 @@ +"""Interface for child repository operations.""" + +from abc import ABC, abstractmethod +from datetime import datetime + + +class ChildRepositoryInterface(ABC): + """Interface for child data access operations.""" + + @abstractmethod + def create( + self, name: str, birth_time: datetime, birth_weight: float, user_id: int + ) -> dict: + """Create a new child record.""" + + @abstractmethod + def get_by_id(self, child_id: int) -> dict | None: + """Get a child by ID.""" + + @abstractmethod + def get_by_user_id(self, user_id: int) -> list[dict]: + """Get all children for a specific user.""" + + @abstractmethod + def update( + self, + child_id: int, + name: str | None = None, + birth_time: datetime | None = None, + birth_weight: float | None = None, + ) -> dict | None: + """Update a child record.""" + + @abstractmethod + def delete(self, child_id: int) -> bool: + """Delete a child record.""" diff --git a/src/baby_monitor/routers/child.py b/src/baby_monitor/routers/child.py new file mode 100644 index 0000000..718b461 --- /dev/null +++ b/src/baby_monitor/routers/child.py @@ -0,0 +1,90 @@ +"""Child management router.""" + +from typing import Annotated +from fastapi import APIRouter, HTTPException, Depends + +from baby_monitor.models.child import CreateChildRequest, ChildResponse +from baby_monitor.routers.auth import verify_token +from baby_monitor.repositories.child.sqlite_child import SQLiteChildRepository +from baby_monitor.repositories.dependencies.get_child_repository import ( + get_child_repository, +) + +router = APIRouter(prefix="/api/children", tags=["children"]) + + +@router.post("", response_model=ChildResponse, status_code=201) +def create_child( + request: CreateChildRequest, + user_id: Annotated[int, Depends(verify_token)], + child_repo: Annotated[SQLiteChildRepository, Depends(get_child_repository)], +) -> ChildResponse: + """Create a new child for the authenticated user.""" + child = child_repo.create( + name=request.name, + birth_time=request.birth_time, + birth_weight=request.birth_weight, + user_id=user_id, + ) + + return ChildResponse(**child) + + +@router.get("", response_model=list[ChildResponse]) +def get_user_children( + user_id: Annotated[int, Depends(verify_token)], + child_repo: Annotated[SQLiteChildRepository, Depends(get_child_repository)], +) -> list[ChildResponse]: + """Get all children for the authenticated user.""" + children = child_repo.get_by_user_id(user_id) + return [ChildResponse(**child) for child in children] + + +@router.get("/{child_id}", response_model=ChildResponse) +def get_child( + child_id: int, + user_id: Annotated[int, Depends(verify_token)], + child_repo: Annotated[SQLiteChildRepository, Depends(get_child_repository)], +) -> ChildResponse: + """Get a specific child by ID.""" + child = child_repo.get_by_id(child_id) + + if not child: + raise HTTPException(status_code=404, detail="Child not found") + + # Verify the child belongs to the authenticated user + if child["user_id"] != user_id: + raise HTTPException(status_code=403, detail="Access denied") + + return ChildResponse(**child) + + +@router.put("/{child_id}", response_model=ChildResponse) +def update_child( + child_id: int, + request: CreateChildRequest, + user_id: Annotated[int, Depends(verify_token)], + child_repo: Annotated[SQLiteChildRepository, Depends(get_child_repository)], +) -> ChildResponse: + """Update a child's information.""" + # First check if child exists and belongs to user + child = child_repo.get_by_id(child_id) + + if not child: + raise HTTPException(status_code=404, detail="Child not found") + + if child["user_id"] != user_id: + raise HTTPException(status_code=403, detail="Access denied") + + # Update the child + updated_child = child_repo.update( + child_id=child_id, + name=request.name, + birth_time=request.birth_time, + birth_weight=request.birth_weight, + ) + + if not updated_child: + raise HTTPException(status_code=500, detail="Update failed") + + return ChildResponse(**updated_child) diff --git a/src/baby_monitor/static/add-child.html b/src/baby_monitor/static/add-child.html new file mode 100644 index 0000000..7fde252 --- /dev/null +++ b/src/baby_monitor/static/add-child.html @@ -0,0 +1,322 @@ + + + + + + Add Child - Baby Monitor + + + +
+

👶 Add Child

+

Enter your child's information

+ +
+ +
+
+ + +
+ +
+ + +

+ Select the date and time when your child was born +

+
+ +
+ + +

Enter weight in grams (1 kg = 1000 grams)

+
+ + + +
+
+ + + + diff --git a/src/baby_monitor/static/index.html b/src/baby_monitor/static/index.html index da17c70..0bbdb36 100644 --- a/src/baby_monitor/static/index.html +++ b/src/baby_monitor/static/index.html @@ -129,6 +129,7 @@ @@ -160,6 +161,10 @@ burgerMenu.addEventListener("click", toggleMenu); menuBackdrop.addEventListener("click", closeMenu); + document.getElementById("menuAddChild").addEventListener("click", () => { + window.location.href = "/add-child.html"; + }); + document.getElementById("menuLogout").addEventListener("click", () => { closeMenu(); logout(); @@ -187,14 +192,29 @@ } }) .then((user) => { - // Show authenticated content - document.getElementById("content").innerHTML = ` + // Check if user has any children + return fetch("/api/children", { + headers: { + Authorization: `Bearer ${token}`, + }, + }) + .then((response) => response.json()) + .then((children) => { + // If no children, redirect to add-child page + if (children.length === 0) { + window.location.href = "/add-child.html"; + return; + } + + // Show authenticated content + document.getElementById("content").innerHTML = `

Welcome to Baby Monitor!

Logged in as: ${username}

Your session is active.

`; + }); }) .catch((error) => { console.error("Error:", error);