Compare commits

..

4 Commits

Author SHA1 Message Date
Lockeshor
810075a393 add README 2026-03-06 13:42:30 -05:00
Lockeshor
f42dc7c3a2 allala 2026-03-06 13:34:04 -05:00
Lockeshor
706b849515 refactor 2026-03-06 13:09:05 -05:00
LockeShor
7bb2d38f7b more.. fix? 2026-03-06 01:02:53 -05:00
4 changed files with 110 additions and 100 deletions

1
.gitignore vendored
View File

@@ -55,3 +55,4 @@ exports/
Scripts/
pool_checkin.sqlite3
pyvenv.cfg
Include/

108
README.md Normal file
View 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
View File

@@ -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")

View File

@@ -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