Compare commits
4 Commits
7f03b24188
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
810075a393 | ||
|
|
f42dc7c3a2 | ||
|
|
706b849515 | ||
|
|
7bb2d38f7b |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -55,3 +55,4 @@ exports/
|
||||
Scripts/
|
||||
pool_checkin.sqlite3
|
||||
pyvenv.cfg
|
||||
Include/
|
||||
|
||||
108
README.md
Normal file
108
README.md
Normal file
@@ -0,0 +1,108 @@
|
||||
# Cary Swim Club Check-In
|
||||
|
||||
A lightweight Flask + SQLite app for managing pool member check-ins, families, and swim-test status.
|
||||
|
||||
Inspired by pooldues.com
|
||||
|
||||
## Features
|
||||
|
||||
- Family and individual member management
|
||||
- Check-in/check-out tracking
|
||||
- Swim-test tracking with an age-based rule (`DEFAULT_MIN_SWIM_TEST_AGE` in `main.py`)
|
||||
- Family guest pass tracking
|
||||
- Search-focused dashboards for active check-ins and family records
|
||||
- Admin utilities for backups, CSV export, DB maintenance, and bulk resets
|
||||
|
||||
## Tech Stack
|
||||
|
||||
- Python
|
||||
- Flask
|
||||
- SQLite
|
||||
- Static HTML/CSS/JavaScript front-end
|
||||
|
||||
## Project Structure
|
||||
|
||||
- `main.py`: Flask server, API routes, and SQLite schema initialization
|
||||
- `index.html`: Main dashboard/check-in experience
|
||||
- `family.html`: Family-specific detail and check-in page
|
||||
- `manage.html`: Management/admin interface
|
||||
- `pool_checkin.sqlite3`: Primary SQLite database
|
||||
- `backups/`: Generated SQLite backups
|
||||
- `exports/`: Generated CSV exports
|
||||
|
||||
## Getting Started
|
||||
|
||||
### 1. Create and activate a virtual environment (optional but recommended)
|
||||
|
||||
PowerShell:
|
||||
|
||||
```powershell
|
||||
python -m venv .venv
|
||||
.\.venv\Scripts\Activate.ps1
|
||||
```
|
||||
|
||||
### 2. Install dependencies
|
||||
|
||||
```powershell
|
||||
pip install flask
|
||||
```
|
||||
|
||||
### 3. Run the app
|
||||
|
||||
```powershell
|
||||
python main.py
|
||||
```
|
||||
|
||||
The app starts on Flask's default URL:
|
||||
|
||||
- `http://127.0.0.1:5000/`
|
||||
|
||||
On first run, required folders and database tables are created automatically.
|
||||
|
||||
## Main Routes
|
||||
|
||||
- `/`: Main check-in dashboard (`index.html`)
|
||||
- `/family/<family_id>`: Family page (`family.html`)
|
||||
- `/manage`: Management page (`manage.html`)
|
||||
|
||||
## API Overview
|
||||
|
||||
### Dashboard and listings
|
||||
|
||||
- `GET /api/dashboard`
|
||||
- `GET /api/checked-in`
|
||||
- `GET /api/individuals`
|
||||
- `GET /api/families`
|
||||
- `GET /api/families/<family_id>`
|
||||
|
||||
### Family and member updates
|
||||
|
||||
- `POST /api/families`
|
||||
- `DELETE /api/families/<family_id>`
|
||||
- `PATCH /api/families/<family_id>/guest-passes`
|
||||
- `POST /api/members`
|
||||
- `PATCH /api/members/<member_id>/checkin`
|
||||
- `PATCH /api/members/<member_id>/swim-test`
|
||||
|
||||
### Admin utilities
|
||||
|
||||
- `POST /api/admin/backup`
|
||||
- `POST /api/admin/export`
|
||||
- `POST /api/admin/reindex`
|
||||
- `POST /api/admin/vacuum`
|
||||
- `POST /api/admin/reset-checkins`
|
||||
- `POST /api/admin/reset-swim-tests`
|
||||
- `POST /api/admin/cleanup-orphan-members`
|
||||
|
||||
## Data Notes
|
||||
|
||||
- Phone numbers are normalized when families are created.
|
||||
- Swim-test requirement is based on age and `DEFAULT_MIN_SWIM_TEST_AGE` in `main.py`.
|
||||
- Exports are written as timestamped CSV files in `exports/`.
|
||||
- Backups are written as timestamped SQLite files in `backups/`.
|
||||
|
||||
## Operational Tips
|
||||
|
||||
- Keep regular DB backups using `POST /api/admin/backup`.
|
||||
- Run `POST /api/admin/vacuum` periodically if the database grows after lots of deletes.
|
||||
- Use `POST /api/admin/export` for reporting snapshots.
|
||||
94
main.py
94
main.py
@@ -79,11 +79,6 @@ def initialize_database() -> None:
|
||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS app_meta (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS members (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
first_name TEXT NOT NULL,
|
||||
@@ -103,60 +98,6 @@ def initialize_database() -> None:
|
||||
"""
|
||||
)
|
||||
|
||||
# Seed demo data once to make first run usable.
|
||||
total_rows = conn.execute("SELECT COUNT(*) AS count FROM members;").fetchone()["count"]
|
||||
if total_rows == 0:
|
||||
conn.executemany(
|
||||
"INSERT INTO families (family_name, primary_phone) VALUES (?, ?);",
|
||||
[
|
||||
("Garcia", "555-201-0141"),
|
||||
("Miller", "555-201-0187"),
|
||||
("Nguyen", "555-201-0179"),
|
||||
],
|
||||
)
|
||||
|
||||
family_map = {
|
||||
row["family_name"]: row["id"]
|
||||
for row in conn.execute("SELECT id, family_name FROM families;").fetchall()
|
||||
}
|
||||
|
||||
conn.executemany(
|
||||
"""
|
||||
INSERT INTO members
|
||||
(first_name, last_name, age, family_id, is_checked_in, swim_test_passed, can_use_deep_end)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?);
|
||||
""",
|
||||
[
|
||||
("Avery", "Wilson", 34, None, 0, 0, 1),
|
||||
("Noah", "Patel", 42, None, 0, 0, 1),
|
||||
("Emma", "James", 29, None, 0, 0, 1),
|
||||
("Luis", "Garcia", 41, family_map["Garcia"], 0, 0, 1),
|
||||
("Sofia", "Garcia", 38, family_map["Garcia"], 0, 0, 1),
|
||||
("Mateo", "Garcia", 11, family_map["Garcia"], 0, 0, 0),
|
||||
("Helen", "Miller", 37, family_map["Miller"], 0, 0, 1),
|
||||
("Kai", "Miller", 9, family_map["Miller"], 0, 0, 0),
|
||||
("Trang", "Nguyen", 35, family_map["Nguyen"], 0, 0, 1),
|
||||
("Liam", "Nguyen", 8, family_map["Nguyen"], 0, 0, 0),
|
||||
],
|
||||
)
|
||||
|
||||
schema_version = conn.execute(
|
||||
"SELECT value FROM app_meta WHERE key = 'schema_version';"
|
||||
).fetchone()
|
||||
current_schema_version = 0
|
||||
if schema_version is not None:
|
||||
try:
|
||||
current_schema_version = int(schema_version["value"])
|
||||
except (TypeError, ValueError):
|
||||
current_schema_version = 0
|
||||
|
||||
# One-time migration for older local databases created before defaults were reset.
|
||||
if current_schema_version < 2:
|
||||
conn.execute("UPDATE members SET is_checked_in = 0, swim_test_passed = 0;")
|
||||
conn.execute(
|
||||
"UPDATE members SET can_use_deep_end = CASE WHEN age >= 13 THEN 1 ELSE 0 END;"
|
||||
)
|
||||
|
||||
family_columns = {
|
||||
row["name"]
|
||||
for row in conn.execute("PRAGMA table_info(families);").fetchall()
|
||||
@@ -167,26 +108,6 @@ def initialize_database() -> None:
|
||||
"ADD COLUMN guest_passes INTEGER NOT NULL DEFAULT 0 CHECK(guest_passes >= 0);"
|
||||
)
|
||||
|
||||
if current_schema_version < 3:
|
||||
conn.execute(
|
||||
"INSERT INTO app_meta (key, value) VALUES ('schema_version', '3') "
|
||||
"ON CONFLICT(key) DO UPDATE SET value = excluded.value;"
|
||||
)
|
||||
|
||||
# Normalize older short phone records to full numbers when possible.
|
||||
old_phones = conn.execute(
|
||||
"SELECT id, primary_phone FROM families WHERE primary_phone IS NOT NULL;"
|
||||
).fetchall()
|
||||
for row in old_phones:
|
||||
phone = row["primary_phone"]
|
||||
digits = re.sub(r"\D", "", phone)
|
||||
if len(digits) == 7:
|
||||
normalized = normalize_phone_for_storage(f"555{digits}")
|
||||
conn.execute(
|
||||
"UPDATE families SET primary_phone = ? WHERE id = ?;",
|
||||
(normalized, row["id"]),
|
||||
)
|
||||
|
||||
|
||||
def to_member_payload(row: dict, min_swim_test_age: int) -> dict:
|
||||
requires_swim_test = row["age"] < min_swim_test_age
|
||||
@@ -203,21 +124,6 @@ def to_member_payload(row: dict, min_swim_test_age: int) -> dict:
|
||||
}
|
||||
|
||||
|
||||
def recalculate_deep_end(conn: sqlite3.Connection, member_id: int) -> None:
|
||||
member = conn.execute(
|
||||
"SELECT age, swim_test_passed FROM members WHERE id = ?;",
|
||||
(member_id,),
|
||||
).fetchone()
|
||||
if member is None:
|
||||
return
|
||||
|
||||
can_use = int(member["age"] >= 13 or bool(member["swim_test_passed"]))
|
||||
conn.execute(
|
||||
"UPDATE members SET can_use_deep_end = ? WHERE id = ?;",
|
||||
(can_use, member_id),
|
||||
)
|
||||
|
||||
|
||||
@app.route("/")
|
||||
def homepage():
|
||||
return send_file(BASE_DIR / "index.html")
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
home = C:\Users\Lockeshor\AppData\Local\Programs\Python\Python312
|
||||
include-system-site-packages = false
|
||||
version = 3.12.4
|
||||
executable = C:\Users\Lockeshor\AppData\Local\Programs\Python\Python312\python.exe
|
||||
command = C:\Users\Lockeshor\AppData\Local\Programs\Python\Python312\python.exe -m venv c:\Users\Lockeshor\Documents\Code\csc-checkin\csc-checkin.venv
|
||||
Reference in New Issue
Block a user