complete overhaul
This commit is contained in:
1 parent
0bc92cdb8b
commit
ee6c7719cf
18 files changed
+751
-440
No files matched your search
@@ -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.
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
# Package init for fitness
|
||||
+131
@@ -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/<int:user_id>", 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/<int:prov_id>", 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/<int:visit_id>", 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)
|
||||
+284
@@ -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)
|
||||
-18
@@ -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()
|
||||
@@ -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 = `<ul>${providers.map(p => `<li>${p.name} (${p.hourly_rate || 'n/a'})</li>`).join('')}</ul>`;
|
||||
const opts = providers.map(p => `<option value="${p.id}">${p.name}</option>`).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 = `<ul>${clients.map(c => `<li>${c.name}</li>`).join('')}</ul>`;
|
||||
clientSelect.innerHTML = clients.map(c => `<option value="${c.id}">${c.name}</option>`).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 = `<ul>${services.map(s => `<li>${s.name} @ $${s.price} (${s.duration_minutes || 'n/a'}min)</li>`).join('')}</ul>`;
|
||||
}
|
||||
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 = `<ul>${visits.map(v => `<li>${new Date(v.start_time).toLocaleString()} – ${v.duration_minutes}min (Provider #${v.provider_id} • Client #${v.client_id})</li>`).join('')}</ul>`;
|
||||
}
|
||||
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();
|
||||
@@ -1,55 +0,0 @@
|
||||
@@
|
||||
- <h1>Fitness Tracker Dashboard</h1>
|
||||
- <div id="visits" class="card">
|
||||
- <h2>Visits</h2>
|
||||
- <ul id="visit-list"></ul>
|
||||
- </div>
|
||||
- <script src="app.js"></script>
|
||||
+ <h1>Fitness Tracker Dashboard</h1>
|
||||
+
|
||||
+ <div class="card">
|
||||
+ <h2>Providers</h2>
|
||||
+ <form id="provider-form">
|
||||
+ <div><label>Name: <input name="name" required></label></div>
|
||||
+ <div><label>Photo URL: <input name="photo_url"></label></div>
|
||||
+ <div><label>Hourly Rate: <input name="hourly_rate" type="number" step="0.01"></label></div>
|
||||
+ <button type="submit">Add Provider</button>
|
||||
+ </form>
|
||||
+ <div class="list" id="providers-list"></div>
|
||||
+ </div>
|
||||
+
|
||||
+ <div class="card">
|
||||
+ <h2>Clients</h2>
|
||||
+ <form id="client-form">
|
||||
+ <div><label>Name: <input name="name" required></label></div>
|
||||
+ <button type="submit">Add Client</button>
|
||||
+ </form>
|
||||
+ <div class="list" id="clients-list"></div>
|
||||
+ </div>
|
||||
+
|
||||
+ <div class="card">
|
||||
+ <h2>Services</h2>
|
||||
+ <form id="service-form">
|
||||
+ <div><label>Provider: <select name="provider_id" required></select></label></div>
|
||||
+ <div><label>Name: <input name="name" required></label></div>
|
||||
+ <div><label>Price: <input name="price" type="number" step="0.01" required></label></div>
|
||||
+ <div><label>Duration (min): <input name="duration_minutes" type="number"></label></div>
|
||||
+ <button type="submit">Add Service</button>
|
||||
+ </form>
|
||||
+ <div class="list" id="services-list"></div>
|
||||
+ </div>
|
||||
+
|
||||
+ <div class="card">
|
||||
+ <h2>Visits</h2>
|
||||
+ <form id="visit-form">
|
||||
+ <div><label>Provider: <select name="provider_id" required></select></label></div>
|
||||
+ <div><label>Client: <select name="client_id" required></select></label></div>
|
||||
+ <div><label>Start Time: <input name="start_time" type="datetime-local"></label></div>
|
||||
+ <div><label>Duration (min): <input name="duration_minutes" type="number" required></label></div>
|
||||
+ <button type="submit">Add Visit</button>
|
||||
+ </form>
|
||||
+ <div class="list" id="visits-list"></div>
|
||||
+ </div>
|
||||
+
|
||||
+ <script src="app.js"></script>
|
||||
*** End Patch
|
||||
@@ -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/")
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
fastapi
|
||||
uvicorn[standard]
|
||||
SQLAlchemy
|
||||
pydantic
|
||||
python-multipart
|
||||
@@ -1 +0,0 @@
|
||||
# Package init for routers
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
<!-- index.html (served by PHP) -->
|
||||
<input type="file" id="photo" style="display:none;">
|
||||
<div id="drop" style="border:2px dashed #bbb; padding:20px;">
|
||||
Drag photo here or click to choose
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const drop = document.getElementById('drop');
|
||||
const photoInput = document.getElementById('photo');
|
||||
|
||||
drop.onclick = () => photoInput.click();
|
||||
|
||||
drop.addEventListener('dragover', e => { e.preventDefault(); drop.style.background='#eef'; });
|
||||
drop.addEventListener('dragleave', e => { e.preventDefault(); drop.style.background=''; });
|
||||
|
||||
drop.addEventListener('drop', e => {
|
||||
e.preventDefault();
|
||||
drop.style.background='';
|
||||
const file = e.dataTransfer.files[0];
|
||||
upload(file);
|
||||
});
|
||||
|
||||
photoInput.addEventListener('change', () => upload(photoInput.files[0]));
|
||||
|
||||
async function upload(file) {
|
||||
// 1️⃣ Send file to PHP, which will forward it to Flask
|
||||
const form = new FormData();
|
||||
form.append('photo', file);
|
||||
|
||||
const res = await fetch('/api/upload_photo', { // <-- PHP endpoint
|
||||
method: 'POST',
|
||||
body: form
|
||||
});
|
||||
|
||||
const json = await res.json();
|
||||
console.log('Uploaded:', json);
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,137 @@
|
||||
<?php
|
||||
// providers.php
|
||||
// Simple page that pulls provider data via the Flask API.
|
||||
// It expects the Flask API to be reachable at http://127.0.0.1:5000
|
||||
// The API endpoint for providers is /providers.
|
||||
|
||||
// --- Helper to call the API -------------------------------------------
|
||||
function callApi(string $method, string $path, $body = null)
|
||||
{
|
||||
$url = 'http://172.25.1.105:5000' . $path;
|
||||
$ch = curl_init($url);
|
||||
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
|
||||
|
||||
if ($body !== null) {
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
|
||||
}
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
if ($httpCode >= 400) {
|
||||
return null; // or throw an exception
|
||||
}
|
||||
|
||||
return json_decode($response, true);
|
||||
}
|
||||
|
||||
// --- Fetch all providers -----------------------------------------------
|
||||
$providers = callApi('GET', '/providers');
|
||||
$providers = $providers ?? [];
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Providers</title>
|
||||
|
||||
<link rel="stylesheet" href="https://cosmostat.matt-cloud.com/src/styles.css">
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; margin: 2rem; }
|
||||
#search { margin-bottom: 1rem; padding: 0.5rem; width: 300px; }
|
||||
table { width: 100%; border-collapse: collapse; }
|
||||
img { max-width: 60px; height: auto; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="card">
|
||||
<h1>Providers</h1>
|
||||
<input type="text" id="search" placeholder="Search providers...">
|
||||
<table id="providers-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th data-field="id">ID</th>
|
||||
<th data-field="name">Name</th>
|
||||
<th data-field="phone">Phone</th>
|
||||
<th data-field="age">Age</th>
|
||||
<th data-field="photo">Photo</th>
|
||||
<th data-field="rates">Rates</th>
|
||||
<th data-field="private">Private</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<script>
|
||||
const providers = <?php echo json_encode($providers); ?>;
|
||||
let currentSort = { field: null, asc: true };
|
||||
|
||||
function renderTable(data) {
|
||||
const tbody = document.querySelector('#providers-table tbody');
|
||||
tbody.innerHTML = '';
|
||||
data.forEach(p => {
|
||||
const tr = document.createElement('tr');
|
||||
tr.innerHTML = `
|
||||
<td>${p.id}</td>
|
||||
<td>${p.name}</td>
|
||||
<td>${p.phone || ''}</td>
|
||||
<td>${p.age || ''}</td>
|
||||
<td>${p.photo ? `<img src="${p.photo}" alt="Photo">` : ''}</td>
|
||||
<td>${p.rates}</td>
|
||||
<td>${p.private ? 'Yes' : 'No'}</td>
|
||||
`;
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
}
|
||||
|
||||
function sortData(field) {
|
||||
if (currentSort.field === field) {
|
||||
currentSort.asc = !currentSort.asc;
|
||||
} else {
|
||||
currentSort.field = field;
|
||||
currentSort.asc = true;
|
||||
}
|
||||
const sorted = providers.slice().sort((a, b) => {
|
||||
let aVal = a[field];
|
||||
let bVal = b[field];
|
||||
if (['id', 'age', 'private'].includes(field)) {
|
||||
aVal = Number(aVal);
|
||||
bVal = Number(bVal);
|
||||
} else {
|
||||
aVal = (aVal || '').toString().toLowerCase();
|
||||
bVal = (bVal || '').toString().toLowerCase();
|
||||
}
|
||||
if (aVal < bVal) return currentSort.asc ? -1 : 1;
|
||||
if (aVal > bVal) return currentSort.asc ? 1 : -1;
|
||||
return 0;
|
||||
});
|
||||
renderTable(sorted);
|
||||
}
|
||||
|
||||
renderTable(providers);
|
||||
|
||||
document.querySelectorAll('#providers-table th').forEach(th => {
|
||||
th.addEventListener('click', () => {
|
||||
const field = th.getAttribute('data-field');
|
||||
sortData(field);
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById('search').addEventListener('input', e => {
|
||||
const query = e.target.value.toLowerCase();
|
||||
const filtered = providers.filter(p => {
|
||||
return Object.values(p).some(v => {
|
||||
if (!v) return false;
|
||||
return String(v).toLowerCase().includes(query);
|
||||
});
|
||||
});
|
||||
renderTable(filtered);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Fitness Tracker</title>
|
||||
<style>
|
||||
table { width: 100%; border-collapse: collapse; }
|
||||
th, td { border: 1px solid #ddd; padding: 8px; }
|
||||
th { background:#f0f0f0; }
|
||||
#drop-zone { border: 2px dashed #aaa; padding: 1rem; text-align:center; cursor:pointer; }
|
||||
#drop-zone.hover { background:#e0e0ff; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h1>Providers</h1>
|
||||
<button id="add-provider-btn">Add Provider</button>
|
||||
<table id="providers-table">
|
||||
<thead><tr><th>ID</th><th>Name</th><th>Phone</th><th>Photo</th></tr></thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
|
||||
<!-- Add Provider Modal -->
|
||||
<div id="modal" style="display:none; position:fixed; top:0; left:0; width:100%; height:100%; background:rgba(0,0,0,0.5);">
|
||||
<div style="background:#fff; margin:5% auto; padding:1rem; width:300px; position:relative;">
|
||||
<h2>Add Provider</h2>
|
||||
<label>Name: <input type="text" id="p-name"></label><br>
|
||||
<label>Age: <input type="number" id="p-age"></label><br>
|
||||
<label>Phone: <input type="text" id="p-phone"></label><br>
|
||||
<label>Rates: <input type="text" id="p-rates" placeholder='{"hourly":70}'></label><br>
|
||||
<label>Private: <input type="checkbox" id="p-private"></label><br>
|
||||
|
||||
<div id="drop-zone">Drag photo or click to upload</div>
|
||||
<input type="file" id="p-file" style="display:none;">
|
||||
<br>
|
||||
<button id="save-provider">Save</button>
|
||||
<button id="cancel-provider">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const API = 'http://localhost:5000';
|
||||
|
||||
// ----------- Fetch & render providers -----------------
|
||||
async function loadProviders() {
|
||||
const res = await fetch(`${API}/providers`);
|
||||
const data = await res.json();
|
||||
const tbody = document.querySelector('#providers-table tbody');
|
||||
tbody.innerHTML = '';
|
||||
data.forEach(p => {
|
||||
const tr = document.createElement('tr');
|
||||
tr.innerHTML = `
|
||||
<td>${p.id}</td>
|
||||
<td>${p.name}</td>
|
||||
<td>${p.phone}</td>
|
||||
<td><img src="${p.photo}" width="50"></td>
|
||||
`;
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
}
|
||||
loadProviders();
|
||||
|
||||
// ----------- Modal handling -----------------
|
||||
const modal = document.getElementById('modal');
|
||||
const addBtn = document.getElementById('add-provider-btn');
|
||||
const cancelBtn = document.getElementById('cancel-provider');
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const fileInput = document.getElementById('p-file');
|
||||
let uploadedPhotoUrl = null;
|
||||
|
||||
addBtn.onclick = () => { modal.style.display='block'; }
|
||||
cancelBtn.onclick = () => { modal.style.display='none'; }
|
||||
|
||||
dropZone.onclick = () => fileInput.click();
|
||||
|
||||
fileInput.onchange = () => uploadFile(fileInput.files[0]);
|
||||
|
||||
dropZone.addEventListener('dragover', e => { e.preventDefault(); dropZone.classList.add('hover'); });
|
||||
dropZone.addEventListener('dragleave', e => { e.preventDefault(); dropZone.classList.remove('hover'); });
|
||||
dropZone.addEventListener('drop', e => {
|
||||
e.preventDefault(); dropZone.classList.remove('hover');
|
||||
uploadFile(e.dataTransfer.files[0]);
|
||||
});
|
||||
|
||||
async function uploadFile(file) {
|
||||
const formData = new FormData();
|
||||
formData.append('photo', file);
|
||||
const resp = await fetch(`${API}/upload_photo`, { method: 'POST', body: formData });
|
||||
uploadedPhotoUrl = await resp.text(); // /uploads/xxxx.jpg
|
||||
dropZone.textContent = '✓ Uploaded';
|
||||
}
|
||||
|
||||
// ----------- Save provider -----------------
|
||||
const saveBtn = document.getElementById('save-provider');
|
||||
saveBtn.onclick = async () => {
|
||||
const payload = {
|
||||
name: document.getElementById('p-name').value,
|
||||
age: parseInt(document.getElementById('p-age').value, 10),
|
||||
phone: document.getElementById('p-phone').value,
|
||||
rates: JSON.parse(document.getElementById('p-rates').value),
|
||||
private: document.getElementById('p-private').checked,
|
||||
photo: uploadedPhotoUrl
|
||||
};
|
||||
await fetch(`${API}/providers`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
modal.style.display='none';
|
||||
loadProviders(); // refresh table
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in new issue
Block a user