From ee6c7719cfe67cd746ea96a56be083f68d388691 Mon Sep 17 00:00:00 2001 From: matt Date: Fri, 16 Oct 2026 02:58:58 -0700 Subject: [PATCH] complete overhaul --- README.md | 47 +++++++ __init__.py | 1 - api/app.py | 131 ++++++++++++++++++++ api/database.py | 284 +++++++++++++++++++++++++++++++++++++++++++ database.py | 18 --- frontend/app.js | 88 -------------- frontend/index.html | 55 --------- main.py | 36 ------ models.py | 50 -------- requirements.txt | 5 - routers/__init__.py | 1 - routers/clients.py | 35 ------ routers/providers.py | 45 ------- routers/services.py | 53 -------- routers/visits.py | 53 -------- web/photos.php | 38 ++++++ web/providers.php | 137 +++++++++++++++++++++ web/test.html | 114 +++++++++++++++++ 18 files changed, 751 insertions(+), 440 deletions(-) delete mode 100644 __init__.py create mode 100644 api/app.py create mode 100644 api/database.py delete mode 100644 database.py delete mode 100644 frontend/app.js delete mode 100644 frontend/index.html delete mode 100644 main.py delete mode 100644 models.py delete mode 100644 requirements.txt delete mode 100644 routers/__init__.py delete mode 100644 routers/clients.py delete mode 100644 routers/providers.py delete mode 100644 routers/services.py delete mode 100644 routers/visits.py create mode 100644 web/photos.php create mode 100644 web/providers.php create mode 100644 web/test.html diff --git a/README.md b/README.md index 1e1c58f..a68e119 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,50 @@ 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. +I will build this with LAPP stack - Linux, Apache, PHP & Python. I will build the front-end in PHP, and the back-end will be a Python API with a database. + +The database will be a SQLite file handled by the Python service. There will need to be a Users table, a Providers table, and a Visits table. The Users table will contain the information and settings for the site users. The settings can be a JSON structure inside a single text entry. The Providers table will track the Providers info and rates. The rates will also be a JSON structure in a single text field. Any user can add a provider, and set it private or not. If not private, other users can add visits for the same provider. The Visits table will contain enties for User Visits to Providers. This will have date, time, location, provider, cost. There should be a initialize_db function that checks for the file and creates it if needed, including all tables. Here is a complete list of data for each table. + Users + - Name + - ID # this can be an int value automatically incremented by the database + - username # this will come from OIDC once the front-end starts getting built + - Age + - Photo # not sure how this will be implemented yet, but this will probably wind up being a txt field with a link or UUID + - Settings # things like dark mode maybe + Providers + - Name + - ID + - Phone number + - Age + - Photo + - Rates + - Private + Visits + - ID + - User # can be a list in case of joint sessions + - Provider # same applies here + - Cost + - Location + - Timestamp +Here is a list of functions to start with + get_user(name, id, username, fields) + This can search user by either name, id, or username depending on what is passed. Will return user data in JSON, fields is optional and will return only fields listed in this variable + get_provider(name, id, phone number, fields) + This will search for a provider similar to how the user search function works + get_visits(provider, user, date range) + This will be able to search by any single or combination of search fields passed, so you could find all visits a provider did with a specific user at a specific date or just all visits for a single user, etc. The date range will need to be a list with two dates in it. This will return a list JSON structures with visit info + add_visit(user, provider, cost, location, time) + This will add a visit for the specified user and will return simple JSON data with status and the visit ID + add_user(name, age, photo) + This will add a user and return a status and ID in JSON. + add_provider(name, age, photo, phone number, rates, private) + This will add a provider and return a status and ID in JSON. + update_provider(id, name, phone number, rates, private, photo) + This will allow for updating a provider and will return a status and ID as well. + update_user(id, name, photo, settings) + This will allow for updating a user and will return a status and ID as well. + update_visit(id, user, provider, cost, location, time) + In case changes need to be made to a recorded visit. + + +The API will be how the site interacts with the database. There will need to be subroutines for searching the database here, and that will depend on how the front-end works out. diff --git a/__init__.py b/__init__.py deleted file mode 100644 index 55f6e43..0000000 --- a/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# Package init for fitness diff --git a/api/app.py b/api/app.py new file mode 100644 index 0000000..807502f --- /dev/null +++ b/api/app.py @@ -0,0 +1,131 @@ +from flask import Flask, request, jsonify +from database import * + +app = Flask(__name__) + +# Initialise database on startup +initialize_db() + +# ----------------------------- Users ----------------------------- + +@app.route("/users", methods=["GET", "POST"]) +def users(): + if request.method == "GET": + # Optional query parameters + name = request.args.get("name") + id = request.args.get("id", type=int) + username = request.args.get("username") + fields = request.args.get("fields") + fields = fields.split(",") if fields else None + users = get_user(name=name, id=id, username=username, fields=fields) + return jsonify(users) + + if request.method == "POST": + data = request.get_json() + if not data: + return jsonify({"error": "Missing JSON body"}), 400 + name = data.get("name") + age = data.get("age") + photo = data.get("photo") + username = data.get("username") + if not all([name, age, username]): + return jsonify({"error": "name, age and username required"}), 400 + result = add_user(name, age, photo, username) + return jsonify(result), 201 + +@app.route("/users/", methods=["PUT"]) +def update_user_route(user_id: int): + data = request.get_json() + if not data: + return jsonify({"error": "Missing JSON body"}), 400 + name = data.get("name") + photo = data.get("photo") + settings = data.get("settings") + result = update_user(user_id, name=name, photo=photo, settings=settings) + return jsonify(result) + +# ----------------------------- Providers ----------------------------- + +@app.route("/providers", methods=["GET", "POST"]) +def providers(): + if request.method == "GET": + name = request.args.get("name") + id = request.args.get("id", type=int) + phone = request.args.get("phone") + fields = request.args.get("fields") + fields = fields.split(",") if fields else None + provs = get_provider(name=name, id=id, phone=phone, fields=fields) + return jsonify(provs) + + if request.method == "POST": + data = request.get_json() + if not data: + return jsonify({"error": "Missing JSON body"}), 400 + name = data.get("name") + age = data.get("age") + photo = data.get("photo") + phone = data.get("phone") + rates = data.get("rates", {}) + private = data.get("private", False) + if not all([name, age, phone]): + return jsonify({"error": "name, age and phone required"}), 400 + result = add_provider(name, age, photo, phone, rates, private) + return jsonify(result), 201 + +@app.route("/providers/", methods=["PUT"]) +def update_provider_route(prov_id: int): + data = request.get_json() + if not data: + return jsonify({"error": "Missing JSON body"}), 400 + name = data.get("name") + phone = data.get("phone") + rates = data.get("rates") + private = data.get("private") + photo = data.get("photo") + result = update_provider(prov_id, name=name, phone=phone, rates=rates, private=private, photo=photo) + return jsonify(result) + +# ----------------------------- Visits ----------------------------- + +@app.route("/visits", methods=["GET", "POST"]) +def visits(): + if request.method == "GET": + provider = request.args.get("provider", type=int) + user = request.args.get("user", type=int) + start = request.args.get("start") + end = request.args.get("end") + date_range = [start, end] if start and end else None + visits_list = get_visits(provider=provider, user=user, date_range=date_range) + return jsonify(visits_list) + + if request.method == "POST": + data = request.get_json() + if not data: + return jsonify({"error": "Missing JSON body"}), 400 + user_ids = data.get("user_ids", []) + provider_ids = data.get("provider_ids", []) + cost = data.get("cost") + location = data.get("location") + time = data.get("time") + if not (user_ids and provider_ids and time): + return jsonify({"error": "user_ids, provider_ids and time required"}), 400 + result = add_visit(user_ids, provider_ids, cost, location, time) + return jsonify(result), 201 + +@app.route("/visits/", methods=["PUT"]) +def update_visit_route(visit_id: int): + data = request.get_json() + if not data: + return jsonify({"error": "Missing JSON body"}), 400 + user_ids = data.get("user_ids") + provider_ids = data.get("provider_ids") + cost = data.get("cost") + location = data.get("location") + time = data.get("time") + result = update_visit(visit_id, user_ids=user_ids, provider_ids=provider_ids, + cost=cost, location=location, time=time) + return jsonify(result) + +# Run the app if this file is executed directly +if __name__ == "__main__": + app.run(host="0.0.0.0", port=5000, debug=False) diff --git a/api/database.py b/api/database.py new file mode 100644 index 0000000..7412cd2 --- /dev/null +++ b/api/database.py @@ -0,0 +1,284 @@ +import sqlite3 +import json +import os +from typing import List, Dict, Any, Optional + +# Path to the SQLite database file +DB_PATH = os.path.join(os.path.dirname(__file__), "fitness.db") + +# Ensure the database directory exists +os.makedirs(os.path.dirname(DB_PATH), exist_ok=True) + +# --------------------------------------------------------------------------- +# Helper functions +# --------------------------------------------------------------------------- + +def _get_connection() -> sqlite3.Connection: + """Return a new SQLite connection with row factory set to sqlite3.Row.""" + conn = sqlite3.connect(DB_PATH) + conn.row_factory = sqlite3.Row + return conn + +# --------------------------------------------------------------------------- +# Database initialization +# --------------------------------------------------------------------------- + +def initialize_db() -> None: + """Create the database file and tables if they do not already exist. + + This function is idempotent - calling it multiple times will not alter an + already-existing schema. + """ + with _get_connection() as conn: + cursor = conn.cursor() + cursor.execute( + """ + CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + username TEXT UNIQUE NOT NULL, + age INTEGER, + photo TEXT, + settings TEXT + ); + """ + ) + cursor.execute( + """ + CREATE TABLE IF NOT EXISTS providers ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + phone TEXT, + age INTEGER, + photo TEXT, + rates TEXT, + private INTEGER NOT NULL DEFAULT 0 + ); + """ + ) + cursor.execute( + """ + CREATE TABLE IF NOT EXISTS visits ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_ids TEXT NOT NULL, + provider_ids TEXT NOT NULL, + cost REAL, + location TEXT, + timestamp DATETIME NOT NULL + ); + """ + ) + conn.commit() + +# --------------------------------------------------------------------------- +# CRUD functions +# --------------------------------------------------------------------------- + +def _select(table: str, where_clause: str = "", params: tuple = (), fields: List[str] = None) -> List[Dict[str, Any]]: + """Internal helper to execute SELECT queries. + + Parameters + ---------- + table: str + Name of the table to query. + where_clause: str + Optional WHERE clause (without the leading "WHERE"). + params: tuple + Parameters for the query. + fields: List[str] | None + List of columns to retrieve. If None, all columns are returned. + """ + cols = ", ".join(fields) if fields else "*" + sql = f"SELECT {cols} FROM {table}" + if where_clause: + sql += f" WHERE {where_clause}" + with _get_connection() as conn: + cursor = conn.execute(sql, params) + rows = [dict(row) for row in cursor.fetchall()] + return rows + +# Users --------------------------------------------------------------------- + +def get_user(name: Optional[str] = None, id: Optional[int] = None, username: Optional[str] = None, fields: List[str] = None) -> List[Dict[str, Any]]: + where_clauses = [] + params = [] + if name: + where_clauses.append("name = ?") + params.append(name) + if id is not None: + where_clauses.append("id = ?") + params.append(id) + if username: + where_clauses.append("username = ?") + params.append(username) + where = " AND ".join(where_clauses) if where_clauses else "" + return _select("users", where, tuple(params), fields) + + +def add_user(name: str, age: int, photo: str, username: str) -> Dict[str, Any]: + """Insert a new user. + + The ``settings`` column is initialised to an empty JSON object. + """ + with _get_connection() as conn: + cursor = conn.execute( + "INSERT INTO users (name, username, age, photo, settings) VALUES (?, ?, ?, ?, ?)", + (name, username, age, photo, json.dumps({})), + ) + conn.commit() + return {"status": "ok", "id": cursor.lastrowid} + + +def update_user(id: int, name: Optional[str] = None, photo: Optional[str] = None, settings: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: + updates = [] + params = [] + if name is not None: + updates.append("name = ?") + params.append(name) + if photo is not None: + updates.append("photo = ?") + params.append(photo) + if settings is not None: + updates.append("settings = ?") + params.append(json.dumps(settings)) + if not updates: + return {"status": "no-op"} + params.append(id) + with _get_connection() as conn: + conn.execute(f"UPDATE users SET {', '.join(updates)} WHERE id = ?", tuple(params)) + conn.commit() + return {"status": "ok", "id": id} + +# Providers ----------------------------------------------------------------- + +def get_provider(name: Optional[str] = None, id: Optional[int] = None, phone: Optional[str] = None, fields: List[str] = None) -> List[Dict[str, Any]]: + where_clauses = [] + params = [] + if name: + where_clauses.append("name = ?") + params.append(name) + if id is not None: + where_clauses.append("id = ?") + params.append(id) + if phone: + where_clauses.append("phone = ?") + params.append(phone) + where = " AND ".join(where_clauses) if where_clauses else "" + return _select("providers", where, tuple(params), fields) + + +def add_provider(name: str, age: int, photo: str, phone: str, rates: Dict[str, Any], private: bool) -> Dict[str, Any]: + with _get_connection() as conn: + cursor = conn.execute( + "INSERT INTO providers (name, phone, age, photo, rates, private) VALUES (?, ?, ?, ?, ?, ?)", + (name, phone, age, photo, json.dumps(rates), int(private)), + ) + conn.commit() + return {"status": "ok", "id": cursor.lastrowid} + + +def update_provider(id: int, name: Optional[str] = None, phone: Optional[str] = None, rates: Optional[Dict[str, Any]] = None, private: Optional[bool] = None, photo: Optional[str] = None) -> Dict[str, Any]: + updates = [] + params = [] + if name is not None: + updates.append("name = ?") + params.append(name) + if phone is not None: + updates.append("phone = ?") + params.append(phone) + if rates is not None: + updates.append("rates = ?") + params.append(json.dumps(rates)) + if private is not None: + updates.append("private = ?") + params.append(int(private)) + if photo is not None: + updates.append("photo = ?") + params.append(photo) + if not updates: + return {"status": "no-op"} + params.append(id) + with _get_connection() as conn: + conn.execute(f"UPDATE providers SET {', '.join(updates)} WHERE id = ?", tuple(params)) + conn.commit() + return {"status": "ok", "id": id} + +# Visits --------------------------------------------------------------------- + +def _parse_user_provider_ids(ids_json: str) -> List[int]: + try: + return json.loads(ids_json) + except json.JSONDecodeError: + return [] + + +def get_visits(provider: Optional[int] = None, user: Optional[int] = None, date_range: Optional[List[str]] = None) -> List[Dict[str, Any]]: + """Return visits filtered by optional provider, user, and date range. + + SQLite does not have a native ``IN`` operator that works directly on a JSON + array column. We use ``EXISTS`` with ``json_each`` to test whether a value + appears in the stored JSON array. This keeps the query simple and works + across SQLite versions. + """ + where_clauses: List[str] = [] + params: List[Any] = [] + if provider is not None: + where_clauses.append("EXISTS (SELECT 1 FROM json_each(provider_ids) WHERE value = ?)") + params.append(provider) + if user is not None: + where_clauses.append("EXISTS (SELECT 1 FROM json_each(user_ids) WHERE value = ?)") + params.append(user) + if date_range: + where_clauses.append("timestamp BETWEEN ? AND ?") + params.extend(date_range) + where = " AND ".join(where_clauses) if where_clauses else "" + rows = _select("visits", where, tuple(params)) + # Convert JSON id fields back to lists + for row in rows: + row["user_ids"] = _parse_user_provider_ids(row["user_ids"]) + row["provider_ids"] = _parse_user_provider_ids(row["provider_ids"]) + return rows + + +def add_visit(user_ids: List[int], provider_ids: List[int], cost: float, location: str, time: str) -> Dict[str, Any]: + with _get_connection() as conn: + cursor = conn.execute( + "INSERT INTO visits (user_ids, provider_ids, cost, location, timestamp) VALUES (?, ?, ?, ?, ?)", + (json.dumps(user_ids), json.dumps(provider_ids), cost, location, time), + ) + conn.commit() + return {"status": "ok", "id": cursor.lastrowid} + + +def update_visit(id: int, user_ids: Optional[List[int]] = None, provider_ids: Optional[List[int]] = None, cost: Optional[float] = None, location: Optional[str] = None, time: Optional[str] = None) -> Dict[str, Any]: + updates = [] + params = [] + if user_ids is not None: + updates.append("user_ids = ?") + params.append(json.dumps(user_ids)) + if provider_ids is not None: + updates.append("provider_ids = ?") + params.append(json.dumps(provider_ids)) + if cost is not None: + updates.append("cost = ?") + params.append(cost) + if location is not None: + updates.append("location = ?") + params.append(location) + if time is not None: + updates.append("timestamp = ?") + params.append(time) + if not updates: + return {"status": "no-op"} + params.append(id) + with _get_connection() as conn: + conn.execute(f"UPDATE visits SET {', '.join(updates)} WHERE id = ?", tuple(params)) + conn.commit() + return {"status": "ok", "id": id} + +# Example usage (to be removed or guarded by ``if __name__ == '__main__'``) +if __name__ == "__main__": + initialize_db() + # Demo: add a user + res = add_user("Alice", 30, "alice.jpg", "alice123") + print("Added user:", res) diff --git a/database.py b/database.py deleted file mode 100644 index a221741..0000000 --- a/database.py +++ /dev/null @@ -1,18 +0,0 @@ -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 deleted file mode 100644 index 9084963..0000000 --- a/frontend/app.js +++ /dev/null @@ -1,88 +0,0 @@ -// --------------------------------------------------------------------- -// Helper: call the API relative to the current host (no hard‑coded host) -const api = async (method, endpoint, data) => { - const res = await fetch(`/${endpoint}`, { - method, - headers: { 'Content-Type': 'application/json' }, - body: data ? JSON.stringify(data) : null, - }); - if (!res.ok) throw await res.json(); - return res.json(); -}; - -// --------------------------------------------------------------------- -// Load Providers -const providersList = document.getElementById('providers-list'); -const providerForm = document.getElementById('provider-form'); -const providerSelect = document.querySelector('#service-form select[name="provider_id"]'); -const visitProviderSelect = document.querySelector('#visit-form select[name="provider_id"]'); -async function loadProviders() { - const providers = await api('GET', 'providers/'); - providersList.innerHTML = `
    ${providers.map(p => `
  • ${p.name} (${p.hourly_rate || 'n/a'})
  • `).join('')}
`; - const opts = providers.map(p => ``).join(''); - providerSelect.innerHTML = opts; - visitProviderSelect.innerHTML = opts; -} -providerForm.addEventListener('submit', async e => { - e.preventDefault(); - const data = Object.fromEntries(new FormData(e.target).entries()); - if (!data.hourly_rate) delete data.hourly_rate; - await api('POST', 'providers/', data); - await loadProviders(); -}); - -// --------------------------------------------------------------------- -// Load Clients -const clientsList = document.getElementById('clients-list'); -const clientForm = document.getElementById('client-form'); -const clientSelect = document.querySelector('#visit-form select[name="client_id"]'); -async function loadClients() { - const clients = await api('GET', 'clients/'); - clientsList.innerHTML = `
    ${clients.map(c => `
  • ${c.name}
  • `).join('')}
`; - clientSelect.innerHTML = clients.map(c => ``).join(''); -} -clientForm.addEventListener('submit', async e => { - e.preventDefault(); - const data = Object.fromEntries(new FormData(e.target).entries()); - await api('POST', 'clients/', data); - await loadClients(); -}); - -// --------------------------------------------------------------------- -// Load Services -const servicesList = document.getElementById('services-list'); -const serviceForm = document.getElementById('service-form'); -async function loadServices() { - const services = await api('GET', 'services/'); - servicesList.innerHTML = `
    ${services.map(s => `
  • ${s.name} @ $${s.price} (${s.duration_minutes || 'n/a'}min)
  • `).join('')}
`; -} -serviceForm.addEventListener('submit', async e => { - e.preventDefault(); - const data = Object.fromEntries(new FormData(e.target).entries()); - if (!data.duration_minutes) delete data.duration_minutes; - await api('POST', 'services/', data); - await loadServices(); -}); - -// --------------------------------------------------------------------- -// Load Visits -const visitsList = document.getElementById('visits-list'); -const visitForm = document.getElementById('visit-form'); -async function loadVisits() { - const visits = await api('GET', 'visits/'); - visitsList.innerHTML = `
    ${visits.map(v => `
  • ${new Date(v.start_time).toLocaleString()} – ${v.duration_minutes}min (Provider #${v.provider_id} • Client #${v.client_id})
  • `).join('')}
`; -} -visitForm.addEventListener('submit', async e => { - e.preventDefault(); - const data = Object.fromEntries(new FormData(e.target).entries()); - if (data.start_time) data.start_time = new Date(data.start_time).toISOString(); - await api('POST', 'visits/', data); - await loadVisits(); -}); - -// --------------------------------------------------------------------- -// Initial load -async function init() { - await Promise.all([loadProviders(), loadClients(), loadServices(), loadVisits()]); -} -init(); \ No newline at end of file diff --git a/frontend/index.html b/frontend/index.html deleted file mode 100644 index b178d51..0000000 --- a/frontend/index.html +++ /dev/null @@ -1,55 +0,0 @@ -@@ --

Fitness Tracker Dashboard

--
--

Visits

--
    --
    -- -+

    Fitness Tracker Dashboard

    -+ -+
    -+

    Providers

    -+
    -+
    -+
    -+
    -+ -+
    -+
    -+
    -+ -+
    -+

    Clients

    -+
    -+
    -+ -+
    -+
    -+
    -+ -+
    -+

    Services

    -+
    -+
    -+
    -+
    -+
    -+ -+
    -+
    -+
    -+ -+
    -+

    Visits

    -+
    -+
    -+
    -+
    -+
    -+ -+
    -+
    -+
    -+ -+ -*** End Patch diff --git a/main.py b/main.py deleted file mode 100644 index b9e8fc1..0000000 --- a/main.py +++ /dev/null @@ -1,36 +0,0 @@ -from fastapi import FastAPI -from fastapi.middleware.cors import CORSMiddleware -from fastapi.staticfiles import StaticFiles -from fastapi.responses import RedirectResponse - -import models, database -from routers import providers, clients, visits, services - -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 at /frontend - app.mount("/frontend", StaticFiles(directory="frontend"), name="frontend") - # Simple redirect from root to /frontend for convenience - @app.get("/") - def root(): - return RedirectResponse(url="/frontend/") - diff --git a/models.py b/models.py deleted file mode 100644 index 628d55c..0000000 --- a/models.py +++ /dev/null @@ -1,50 +0,0 @@ -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 deleted file mode 100644 index e4d4fdc..0000000 --- a/requirements.txt +++ /dev/null @@ -1,5 +0,0 @@ -fastapi -uvicorn[standard] -SQLAlchemy -pydantic -python-multipart diff --git a/routers/__init__.py b/routers/__init__.py deleted file mode 100644 index 4f5b20c..0000000 --- a/routers/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# Package init for routers diff --git a/routers/clients.py b/routers/clients.py deleted file mode 100644 index c2caa70..0000000 --- a/routers/clients.py +++ /dev/null @@ -1,35 +0,0 @@ -from fastapi import APIRouter, Depends, HTTPException, status -from sqlalchemy.orm import Session -import models -from database import get_db -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 deleted file mode 100644 index a731865..0000000 --- a/routers/providers.py +++ /dev/null @@ -1,45 +0,0 @@ -from fastapi import APIRouter, Depends, HTTPException, status -from sqlalchemy.orm import Session -import models -from database import get_db -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 deleted file mode 100644 index 8fb4482..0000000 --- a/routers/services.py +++ /dev/null @@ -1,53 +0,0 @@ -from fastapi import APIRouter, Depends, HTTPException, status -from sqlalchemy.orm import Session -import models -from database import get_db -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 deleted file mode 100644 index 38dcbe8..0000000 --- a/routers/visits.py +++ /dev/null @@ -1,53 +0,0 @@ -from fastapi import APIRouter, Depends, HTTPException, status -from sqlalchemy.orm import Session -import models -from database import get_db -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 - diff --git a/web/photos.php b/web/photos.php new file mode 100644 index 0000000..a90dd79 --- /dev/null +++ b/web/photos.php @@ -0,0 +1,38 @@ + + +
    + Drag photo here or click to choose +
    + + \ No newline at end of file diff --git a/web/providers.php b/web/providers.php new file mode 100644 index 0000000..16992cc --- /dev/null +++ b/web/providers.php @@ -0,0 +1,137 @@ += 400) { + return null; // or throw an exception + } + + return json_decode($response, true); +} + +// --- Fetch all providers ----------------------------------------------- +$providers = callApi('GET', '/providers'); +$providers = $providers ?? []; + +// ----------------------------------------------------------------------- +?> + + + + +Providers + + + + + +
    +

    Providers

    + + + + + + + + + + + + + + +
    IDNamePhoneAgePhotoRatesPrivate
    +
    + + + diff --git a/web/test.html b/web/test.html new file mode 100644 index 0000000..c117c95 --- /dev/null +++ b/web/test.html @@ -0,0 +1,114 @@ + + + + + Fitness Tracker + + + + +

    Providers

    + + + + +
    IDNamePhonePhoto
    + + + + + + + \ No newline at end of file