first commit

This commit is contained in:
matt committed 2026-10-15 03:34:44 -07:00
commit f51a0657f1
12 files changed
+327

No files matched your search

+2
View File
@@ -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.
+18
View File
@@ -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()
+14
View File
@@ -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
+20
View File
@@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Fitness Tracker Dashboard</title>
<style>
body {font-family: Arial, sans-serif; margin: 20px;}
.card {border: 1px solid #ddd; padding: 10px; margin-bottom: 10px;}
h2 {margin-top: 0;}
</style>
</head>
<body>
<h1>Fitness Tracker Dashboard</h1>
<div id="visits" class="card">
<h2>Visits</h2>
<ul id="visit-list"></ul>
</div>
<script src="app.js"></script>
</body>
</html>
+31
View File
@@ -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")
+50
View File
@@ -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")
+5
View File
@@ -0,0 +1,5 @@
fastapi
uvicorn[standard]
SQLAlchemy
pydantic
python-multipart
+1
View File
@@ -0,0 +1 @@
# Package init for routers
+35
View File
@@ -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
+46
View File
@@ -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
+52
View File
@@ -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
+53
View File
@@ -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