first commit
This commit is contained in:
commit
f51a0657f1
12 files changed
+327
No files matched your search
@@ -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
@@ -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()
|
||||||
@@ -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
|
||||||
@@ -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>
|
||||||
@@ -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")
|
||||||
|
|
||||||
@@ -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")
|
||||||
|
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
fastapi
|
||||||
|
uvicorn[standard]
|
||||||
|
SQLAlchemy
|
||||||
|
pydantic
|
||||||
|
python-multipart
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
# Package init for routers
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
|
||||||
Reference in new issue
Block a user