commit f51a0657f1e629dde0abb9e3e6ced6de6dd75903 Author: matt Date: Thu Oct 15 03:34:44 2026 -0700 first commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..8c7f965 --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +This app is intended to use with tracking personal fitness providers and visits to them. This will allow for tracking the providers, clients, and visits, and is intended to be used by the client for record-keeping. Providers will have photos and hourly rates. Visits will have duration and provider info. The client using the app should have a dashboard that shows a summary of all visits, and areas to view past visits, add new providers, and add visits. This will require a web front-end and a database back-end. + diff --git a/database.py b/database.py new file mode 100644 index 0000000..a221741 --- /dev/null +++ b/database.py @@ -0,0 +1,18 @@ +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker, declarative_base + +DATABASE_URL = "sqlite:///./fitness.db" +engine = create_engine( + DATABASE_URL, connect_args={"check_same_thread": False} +) +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) +Base = declarative_base() + +# Dependency + +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() diff --git a/frontend/app.js b/frontend/app.js new file mode 100644 index 0000000..d106170 --- /dev/null +++ b/frontend/app.js @@ -0,0 +1,14 @@ +// Simple fetch to populate visit list +const visitList = document.getElementById('visit-list'); +async function loadVisits() { + const resp = await fetch('http://localhost:8000/visits/'); + const data = await resp.json(); + visitList.innerHTML = ''; + data.forEach(v => { + const li = document.createElement('li'); + li.textContent = `${new Date(v.start_time).toLocaleString()} - ${v.duration_minutes} min`; + visitList.appendChild(li); + }); +} +loadVisits(); +// Could add intervals or form handling later \ No newline at end of file diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..a15cdc7 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,20 @@ + + + + + Fitness Tracker Dashboard + + + +

Fitness Tracker Dashboard

+
+

Visits

+ +
+ + + \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..b200809 --- /dev/null +++ b/main.py @@ -0,0 +1,31 @@ +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware +from fastapi.staticfiles import StaticFiles + +from . import models, database +from .routers import providers, clients, visits + +app = FastAPI(title="Fitness Tracker API") + +# Allow CORS for frontend dev +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# Include routers +app.include_router(providers.router, prefix="/providers", tags=["providers"]) +app.include_router(clients.router, prefix="/clients", tags=["clients"]) +app.include_router(visits.router, prefix="/visits", tags=["visits"]) +app.include_router(services.router, prefix="/services", tags=["services"]) + +# Create tables on startup +@app.on_event("startup") +async def startup(): + database.Base.metadata.create_all(bind=database.engine) + # Mount frontend static files + app.mount("/frontend", StaticFiles(directory="frontend"), name="frontend") + diff --git a/models.py b/models.py new file mode 100644 index 0000000..c5d8023 --- /dev/null +++ b/models.py @@ -0,0 +1,50 @@ +from sqlalchemy import Column, Integer, String, Float, DateTime, ForeignKey +from sqlalchemy.orm import relationship +from datetime import datetime + +from .database import Base + +class Provider(Base): + __tablename__ = "providers" + + id = Column(Integer, primary_key=True, index=True) + name = Column(String, unique=True, index=True, nullable=False) + photo_url = Column(String, nullable=True) + # Remove hourly_rate; pricing is per-service now + # hourly_rate = Column(Float, nullable=False) + created_at = Column(DateTime, default=datetime.utcnow) + + visits = relationship("Visit", back_populates="provider") + services = relationship("Service", back_populates="provider", cascade="all, delete-orphan") + +class Client(Base): + __tablename__ = "clients" + + id = Column(Integer, primary_key=True, index=True) + name = Column(String, unique=True, index=True, nullable=False) + created_at = Column(DateTime, default=datetime.utcnow) + + visits = relationship("Visit", back_populates="client") + +class Service(Base): + __tablename__ = "services" + + id = Column(Integer, primary_key=True, index=True) + provider_id = Column(Integer, ForeignKey("providers.id"), nullable=False) + name = Column(String, nullable=False) + price = Column(Float, nullable=False) + duration_minutes = Column(Integer, nullable=True) + + provider = relationship("Provider", back_populates="services") + +class Visit(Base): + __tablename__ = "visits" + + id = Column(Integer, primary_key=True, index=True) + provider_id = Column(Integer, ForeignKey("providers.id"), nullable=False) + client_id = Column(Integer, ForeignKey("clients.id"), nullable=False) + start_time = Column(DateTime, default=datetime.utcnow) + duration_minutes = Column(Integer, nullable=False) + provider = relationship("Provider", back_populates="visits") + client = relationship("Client", back_populates="visits") + diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..e4d4fdc --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +fastapi +uvicorn[standard] +SQLAlchemy +pydantic +python-multipart diff --git a/routers/__init__.py b/routers/__init__.py new file mode 100644 index 0000000..4f5b20c --- /dev/null +++ b/routers/__init__.py @@ -0,0 +1 @@ +# Package init for routers diff --git a/routers/clients.py b/routers/clients.py new file mode 100644 index 0000000..c49a605 --- /dev/null +++ b/routers/clients.py @@ -0,0 +1,35 @@ +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.orm import Session +from ..database import get_db +from .. import models +from typing import List +from pydantic import BaseModel +router = APIRouter() +class ClientCreate(BaseModel): + name: str +class ClientOut(ClientCreate): + id: int + class Config: + orm_mode = True +@router.post("/", response_model=ClientOut, status_code=status.HTTP_201_CREATED) +def create_client(client: ClientCreate, db: Session = Depends(get_db)): + db_client = models.Client(name=client.name) + db.add(db_client) + try: + db.commit() + db.refresh(db_client) + except Exception as e: + db.rollback() + raise HTTPException(status_code=400, detail=str(e)) + return db_client +@router.get("/", response_model=List[ClientOut]) +def list_clients(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)): + clients = db.query(models.Client).offset(skip).limit(limit).all() + return clients +@router.get("/{client_id}", response_model=ClientOut) +def get_client(client_id: int, db: Session = Depends(get_db)): + client = db.query(models.Client).filter(models.Client.id == client_id).first() + if not client: + raise HTTPException(status_code=404, detail="Client not found") + return client + diff --git a/routers/providers.py b/routers/providers.py new file mode 100644 index 0000000..3ee7ecc --- /dev/null +++ b/routers/providers.py @@ -0,0 +1,46 @@ +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.orm import Session +from ..database import get_db +from .. import models +from typing import List +from pydantic import BaseModel + +router = APIRouter() + +class ProviderCreate(BaseModel): + name: str + photo_url: str | None = None + # No hourly_rate; services will be added separately +class ProviderOut(ProviderCreate): + id: int + class Config: + orm_mode = True + +@router.post("/", response_model=ProviderOut, status_code=status.HTTP_201_CREATED) +def create_provider(provider: ProviderCreate, db: Session = Depends(get_db)): + db_provider = models.Provider( + name=provider.name, + photo_url=provider.photo_url, + ) + db.add(db_provider) + try: + db.commit() + db.refresh(db_provider) + except Exception as e: + db.rollback() + raise HTTPException(status_code=400, detail=str(e)) + return db_provider + +@router.get("/", response_model=List[ProviderOut]) +def list_providers(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)): + providers = db.query(models.Provider).offset(skip).limit(limit).all() + return providers + +@router.get("/{provider_id}", response_model=ProviderOut) +def get_provider(provider_id: int, db: Session = Depends(get_db)): + provider = db.query(models.Provider).filter(models.Provider.id == provider_id).first() + if not provider: + raise HTTPException(status_code=404, detail="Provider not found") + return provider + + diff --git a/routers/services.py b/routers/services.py new file mode 100644 index 0000000..050d0b8 --- /dev/null +++ b/routers/services.py @@ -0,0 +1,52 @@ +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.orm import Session +from ..database import get_db +from .. import models +from typing import List +from pydantic import BaseModel + +router = APIRouter() + +class ServiceCreate(BaseModel): + provider_id: int + name: str + price: float + duration_minutes: int | None = None + +class ServiceOut(ServiceCreate): + id: int + class Config: + orm_mode = True + +@router.post("/", response_model=ServiceOut, status_code=status.HTTP_201_CREATED) +def create_service(service: ServiceCreate, db: Session = Depends(get_db)): + # Verify provider exists + provider = db.query(models.Provider).filter(models.Provider.id == service.provider_id).first() + if not provider: + raise HTTPException(status_code=404, detail="Provider not found") + db_service = models.Service( + provider_id=service.provider_id, + name=service.name, + price=service.price, + duration_minutes=service.duration_minutes, + ) + db.add(db_service) + try: + db.commit() + db.refresh(db_service) + except Exception as e: + db.rollback() + raise HTTPException(status_code=400, detail=str(e)) + return db_service + +@router.get("/", response_model=List[ServiceOut]) +def list_services(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)): + services = db.query(models.Service).offset(skip).limit(limit).all() + return services + +@router.get("/{service_id}", response_model=ServiceOut) +def get_service(service_id: int, db: Session = Depends(get_db)): + service = db.query(models.Service).filter(models.Service.id == service_id).first() + if not service: + raise HTTPException(status_code=404, detail="Service not found") + return service diff --git a/routers/visits.py b/routers/visits.py new file mode 100644 index 0000000..b84e2aa --- /dev/null +++ b/routers/visits.py @@ -0,0 +1,53 @@ +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.orm import Session +from ..database import get_db +from .. import models +from typing import List +from pydantic import BaseModel +from datetime import datetime + +router = APIRouter() + +class VisitCreate(BaseModel): + provider_id: int + client_id: int + start_time: str | None = None + duration_minutes: int + +class VisitOut(VisitCreate): + id: int + + class Config: + orm_mode = True + +@router.post("/", response_model=VisitOut, status_code=status.HTTP_201_CREATED) +def create_visit(visit: VisitCreate, db: Session = Depends(get_db)): + # Convert ISO string to datetime + start_dt = datetime.fromisoformat(visit.start_time) if visit.start_time else None + db_visit = models.Visit( + provider_id=visit.provider_id, + client_id=visit.client_id, + start_time=start_dt, + duration_minutes=visit.duration_minutes, + ) + db.add(db_visit) + try: + db.commit() + db.refresh(db_visit) + except Exception as e: + db.rollback() + raise HTTPException(status_code=400, detail=str(e)) + return db_visit + +@router.get("/", response_model=List[VisitOut]) +def list_visits(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)): + visits = db.query(models.Visit).offset(skip).limit(limit).all() + return visits + +@router.get("/{visit_id}", response_model=VisitOut) +def get_visit(visit_id: int, db: Session = Depends(get_db)): + visit = db.query(models.Visit).filter(models.Visit.id == visit_id).first() + if not visit: + raise HTTPException(status_code=404, detail="Visit not found") + return visit +