Clean things up a bit

This commit is contained in:
2026-06-15 18:57:57 -04:00
parent d46c4c3fe7
commit 08f92c068e
2 changed files with 7 additions and 208 deletions

184
app.py
View File

@@ -1,15 +1,5 @@
#!/usr/bin/env python3
"""
Minimal Time Clock Application
"""
import sqlite3 import sqlite3
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
LEGACY_TIMEZONE = timezone.utc
from flask import ( from flask import (
Flask, Flask,
request, request,
@@ -17,45 +7,26 @@ from flask import (
jsonify jsonify
) )
# ----------------------------------------------------------------------------- LEGACY_TIMEZONE = timezone.utc
# Flask Configuration
# -----------------------------------------------------------------------------
app = Flask(__name__) app = Flask(__name__)
DATABASE = "database/timeclock.db" DATABASE = "database/timeclock.db"
# -----------------------------------------------------------------------------
# Database Helpers
# -----------------------------------------------------------------------------
def get_db_connection(): def get_db_connection():
"""
Create and return a sqlite3 database connection.
"""
conn = sqlite3.connect(DATABASE) conn = sqlite3.connect(DATABASE)
conn.row_factory = sqlite3.Row conn.row_factory = sqlite3.Row
return conn return conn
def initialize_database(): def initialize_database():
"""
Create database tables if they do not already exist.
"""
conn = get_db_connection() conn = get_db_connection()
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute(""" cursor.execute("""
CREATE TABLE IF NOT EXISTS users ( CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL name TEXT NOT NULL
) )
""") """)
cursor.execute(""" cursor.execute("""
CREATE TABLE IF NOT EXISTS entries ( CREATE TABLE IF NOT EXISTS entries (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -65,161 +36,76 @@ def initialize_database():
FOREIGN KEY(user_id) REFERENCES users(id) FOREIGN KEY(user_id) REFERENCES users(id)
) )
""") """)
conn.commit() conn.commit()
cursor.execute("SELECT COUNT(*) AS count FROM users") cursor.execute("SELECT COUNT(*) AS count FROM users")
result = cursor.fetchone() result = cursor.fetchone()
if result["count"] == 0: if result["count"] == 0:
cursor.execute("INSERT INTO users (name) VALUES ('Alice')") cursor.execute("INSERT INTO users (name) VALUES ('Alice')")
cursor.execute("INSERT INTO users (name) VALUES ('Bob')") cursor.execute("INSERT INTO users (name) VALUES ('Bob')")
cursor.execute("INSERT INTO users (name) VALUES ('Charlie')") cursor.execute("INSERT INTO users (name) VALUES ('Charlie')")
conn.commit()
conn.commit()
conn.close() conn.close()
# -----------------------------------------------------------------------------
# Time Helpers
# -----------------------------------------------------------------------------
def parse_iso_datetime(value): def parse_iso_datetime(value):
"""
Parse timestamps into timezone-aware datetime objects.
Handles both:
2026-05-29 08:00:00
and:
2026-05-29T08:00:00-04:00
"""
#
# Support legacy sqlite format:
# 2026-05-29 08:00:00
#
normalized = value.replace(" ", "T") normalized = value.replace(" ", "T")
dt = datetime.fromisoformat(normalized) dt = datetime.fromisoformat(normalized)
#
# If timestamp is naive, assume legacy timezone.
#
if dt.tzinfo is None: if dt.tzinfo is None:
dt = dt.replace(tzinfo=LEGACY_TIMEZONE) dt = dt.replace(tzinfo=LEGACY_TIMEZONE)
return dt return dt
def utc_now(): def utc_now():
"""
Return current UTC timestamp.
"""
return datetime.now(timezone.utc) return datetime.now(timezone.utc)
# -----------------------------------------------------------------------------
# Utility Functions
# -----------------------------------------------------------------------------
def create_entry(user_id, entry_type, client_timestamp=None): def create_entry(user_id, entry_type, client_timestamp=None):
"""
Create a clock in/out entry for the specified user.
"""
conn = get_db_connection() conn = get_db_connection()
cursor = conn.cursor() cursor = conn.cursor()
if client_timestamp: if client_timestamp:
timestamp = parse_iso_datetime(client_timestamp) timestamp = parse_iso_datetime(client_timestamp)
else: else:
timestamp = utc_now() timestamp = utc_now()
cursor.execute(""" cursor.execute("""
INSERT INTO entries (user_id, entrytype, ts) INSERT INTO entries (user_id, entrytype, ts)
VALUES (?, ?, ?) VALUES (?, ?, ?)
""", ( """, (user_id, entry_type, timestamp.isoformat()))
user_id,
entry_type,
timestamp.isoformat()
))
conn.commit() conn.commit()
conn.close() conn.close()
def generate_report(user_id, begin_date, end_date): def generate_report(user_id, begin_date, end_date):
"""
Generate total worked hours for a user between two dates.
"""
conn = get_db_connection() conn = get_db_connection()
cursor = conn.cursor() cursor = conn.cursor()
begin_dt = parse_iso_datetime(begin_date) begin_dt = parse_iso_datetime(begin_date)
end_dt = parse_iso_datetime(end_date) end_dt = parse_iso_datetime(end_date)
cursor.execute(""" cursor.execute("""
SELECT entrytype, ts SELECT entrytype, ts
FROM entries FROM entries
WHERE user_id = ? WHERE user_id = ?
ORDER BY ts ASC ORDER BY ts ASC
""", (user_id,)) """, (user_id,))
rows = cursor.fetchall() rows = cursor.fetchall()
conn.close() conn.close()
total_seconds = 0 total_seconds = 0
clock_in_time = None clock_in_time = None
for row in rows: for row in rows:
timestamp = parse_iso_datetime(row["ts"]) timestamp = parse_iso_datetime(row["ts"])
if timestamp < begin_dt or timestamp > end_dt: if timestamp < begin_dt or timestamp > end_dt:
continue continue
if row["entrytype"] == "in": if row["entrytype"] == "in":
clock_in_time = timestamp clock_in_time = timestamp
elif row["entrytype"] == "out" and clock_in_time is not None: elif row["entrytype"] == "out" and clock_in_time is not None:
delta = timestamp - clock_in_time delta = timestamp - clock_in_time
total_seconds += delta.total_seconds() total_seconds += delta.total_seconds()
clock_in_time = None clock_in_time = None
total_hours = total_seconds / 3600.0 total_hours = total_seconds / 3600.0
return round(total_hours, 2) return round(total_hours, 2)
# -----------------------------------------------------------------------------
# Web UI Routes
# -----------------------------------------------------------------------------
@app.route("/", methods=["GET", "POST"]) @app.route("/", methods=["GET", "POST"])
def index(): def index():
"""
Main application page.
"""
conn = get_db_connection() conn = get_db_connection()
users = conn.execute(""" users = conn.execute("""
SELECT id, name SELECT id, name
FROM users FROM users
ORDER BY name ORDER BY name
""").fetchall() """).fetchall()
clocked_in_users = conn.execute(""" clocked_in_users = conn.execute("""
SELECT u.id, u.name, last_entry.ts SELECT u.id, u.name, last_entry.ts
FROM users u FROM users u
@@ -238,94 +124,59 @@ def index():
WHERE last_entry.entrytype = 'in' WHERE last_entry.entrytype = 'in'
ORDER BY u.name ORDER BY u.name
""").fetchall() """).fetchall()
conn.close() conn.close()
clocked_in_list = [] clocked_in_list = []
for user in clocked_in_users: for user in clocked_in_users:
ts = parse_iso_datetime(user["ts"]) ts = parse_iso_datetime(user["ts"])
clocked_in_list.append({ clocked_in_list.append({
"id": user["id"], "id": user["id"],
"name": user["name"], "name": user["name"],
"since": ts.isoformat() "since": ts.isoformat()
}) })
report_hours = None report_hours = None
all_user_reports = [] all_user_reports = []
selected_user_id = None selected_user_id = None
now = utc_now() now = utc_now()
default_begin = now - timedelta(days=7) default_begin = now - timedelta(days=7)
default_end = now default_end = now
if request.method == "POST": if request.method == "POST":
selected_user_id = request.form.get("user_id") selected_user_id = request.form.get("user_id")
action = request.form.get("action") action = request.form.get("action")
client_timestamp = request.form.get("client_timestamp") client_timestamp = request.form.get("client_timestamp")
if action == "clock_in": if action == "clock_in":
create_entry( create_entry(
selected_user_id, selected_user_id,
"in", "in",
client_timestamp client_timestamp
) )
elif action == "clock_out": elif action == "clock_out":
create_entry( create_entry(
selected_user_id, selected_user_id,
"out", "out",
client_timestamp client_timestamp
) )
elif action == "report": elif action == "report":
begin_date = request.form.get("begin_date") begin_date = request.form.get("begin_date")
end_date = request.form.get("end_date") end_date = request.form.get("end_date")
report_hours = generate_report( report_hours = generate_report(
selected_user_id, selected_user_id,
begin_date, begin_date,
end_date end_date
) )
default_begin = parse_iso_datetime(begin_date) default_begin = parse_iso_datetime(begin_date)
default_end = parse_iso_datetime(end_date) default_end = parse_iso_datetime(end_date)
else: else:
#
# Automatically generate last 7 day report
# for ALL users
#
for user in users: for user in users:
hours = generate_report( hours = generate_report(
user["id"], user["id"],
default_begin.isoformat(), default_begin.isoformat(),
default_end.isoformat() default_end.isoformat()
) )
all_user_reports.append({ all_user_reports.append({
"id": user["id"], "id": user["id"],
"name": user["name"], "name": user["name"],
"hours": hours "hours": hours
}) })
return render_template( return render_template(
"index.html", "index.html",
users=users, users=users,
@@ -337,78 +188,51 @@ def index():
selected_user_id=selected_user_id selected_user_id=selected_user_id
) )
# -----------------------------------------------------------------------------
# CRUD API
# -----------------------------------------------------------------------------
@app.route("/api/users", methods=["GET"]) @app.route("/api/users", methods=["GET"])
def api_get_users(): def api_get_users():
conn = get_db_connection() conn = get_db_connection()
users = conn.execute(""" users = conn.execute("""
SELECT id, name SELECT id, name
FROM users FROM users
""").fetchall() """).fetchall()
conn.close() conn.close()
return jsonify([dict(user) for user in users]) return jsonify([dict(user) for user in users])
@app.route("/api/entries", methods=["GET"]) @app.route("/api/entries", methods=["GET"])
def api_get_entries(): def api_get_entries():
conn = get_db_connection() conn = get_db_connection()
entries = conn.execute(""" entries = conn.execute("""
SELECT id, user_id, entrytype, ts SELECT id, user_id, entrytype, ts
FROM entries FROM entries
ORDER BY ts DESC ORDER BY ts DESC
""").fetchall() """).fetchall()
conn.close() conn.close()
return jsonify([dict(entry) for entry in entries]) return jsonify([dict(entry) for entry in entries])
@app.route("/api/entries", methods=["POST"]) @app.route("/api/entries", methods=["POST"])
def api_create_entry(): def api_create_entry():
data = request.get_json() data = request.get_json()
user_id = data.get("user_id") user_id = data.get("user_id")
entry_type = data.get("entrytype") entry_type = data.get("entrytype")
client_timestamp = data.get("timestamp") client_timestamp = data.get("timestamp")
if entry_type not in ("in", "out"): if entry_type not in ("in", "out"):
return jsonify({ return jsonify({
"error": "Invalid entrytype" "error": "Invalid entrytype"
}), 400 }), 400
create_entry( create_entry(
user_id, user_id,
entry_type, entry_type,
client_timestamp client_timestamp
) )
return jsonify({ return jsonify({
"status": "success" "status": "success"
}), 201 }), 201
# -----------------------------------------------------------------------------
# Application Entry Point
# -----------------------------------------------------------------------------
if __name__ == "__main__": if __name__ == "__main__":
initialize_database() initialize_database()
app.run( app.run(
host="0.0.0.0", host="0.0.0.0",
port=5000, port=5000,
debug=False debug=False
) )

View File

@@ -82,7 +82,6 @@
{{ user.name }} {{ user.name }}
</option> </option>
{% endfor %} {% endfor %}
</select> </select>
</td> </td>
</tr> </tr>
@@ -180,9 +179,7 @@
<script> <script>
function localDatetimeValue(date) { function localDatetimeValue(date) {
const pad = (v) => String(v).padStart(2, "0"); const pad = (v) => String(v).padStart(2, "0");
return ( return (
date.getFullYear() + "-" + date.getFullYear() + "-" +
pad(date.getMonth() + 1) + "-" + pad(date.getMonth() + 1) + "-" +
@@ -193,43 +190,21 @@ function localDatetimeValue(date) {
} }
function setCurrentTimestamp() { function setCurrentTimestamp() {
document.getElementById("client_timestamp").value = new Date().toISOString();
document.getElementById("client_timestamp").value =
new Date().toISOString();
} }
window.addEventListener("load", () => { window.addEventListener("load", () => {
//
// Populate default report range
//
const now = new Date(); const now = new Date();
const weekAgo = new Date(); const weekAgo = new Date();
weekAgo.setDate(now.getDate() - 7); weekAgo.setDate(now.getDate() - 7);
document.getElementById("begin_date").value = localDatetimeValue(weekAgo);
document.getElementById("begin_date").value = document.getElementById("end_date").value = localDatetimeValue(now);
localDatetimeValue(weekAgo);
document.getElementById("end_date").value =
localDatetimeValue(now);
//
// Convert displayed timestamps to local timezone
//
document.querySelectorAll(".tztime").forEach((el) => { document.querySelectorAll(".tztime").forEach((el) => {
const ts = el.dataset.ts; const ts = el.dataset.ts;
const d = new Date(ts); const d = new Date(ts);
el.textContent = d.toLocaleString(); el.textContent = d.toLocaleString();
}); });
}); });
</script> </script>
</body> </body>