Files
timeclock/app.py
2026-05-19 18:14:39 -04:00

299 lines
6.8 KiB
Python

#!/usr/bin/env python3
"""
Minimal Time Clock Application
Requirements:
- Python 3
- Flask
- sqlite3
- Linux compatible
- No JavaScript
- Simple HTML UI
Features:
- User selection
- Clock in
- Clock out
- Report generation
- CRUD API
- Automatic database initialization
"""
import sqlite3
from datetime import datetime, timedelta
from flask import Flask, request, redirect, url_for, render_template, jsonify
# -----------------------------------------------------------------------------
# Flask Configuration
# -----------------------------------------------------------------------------
app = Flask(__name__)
DATABASE = "database/timeclock.db"
# -----------------------------------------------------------------------------
# Database Helpers
# -----------------------------------------------------------------------------
def get_db_connection():
"""
Create and return a sqlite3 database connection.
"""
conn = sqlite3.connect(DATABASE)
conn.row_factory = sqlite3.Row
return conn
def initialize_database():
"""
Create database tables if they do not already exist.
"""
conn = get_db_connection()
cursor = conn.cursor()
# Users table
cursor.execute("""
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL
)
""")
# Entries table
#
# user_id:
# References users.id
#
# entrytype:
# Must be either 'in' or 'out'
#
# ts:
# Timestamp stored as ISO datetime string
#
cursor.execute("""
CREATE TABLE IF NOT EXISTS entries (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
entrytype TEXT NOT NULL CHECK(entrytype IN ('in', 'out')),
ts DATETIME NOT NULL,
FOREIGN KEY(user_id) REFERENCES users(id)
)
""")
conn.commit()
# Add sample users if table is empty
cursor.execute("SELECT COUNT(*) AS count FROM users")
result = cursor.fetchone()
if result["count"] == 0:
cursor.execute("INSERT INTO users (name) VALUES ('Alice')")
cursor.execute("INSERT INTO users (name) VALUES ('Bob')")
cursor.execute("INSERT INTO users (name) VALUES ('Charlie')")
conn.commit()
conn.close()
# -----------------------------------------------------------------------------
# Utility Functions
# -----------------------------------------------------------------------------
def create_entry(user_id, entry_type):
"""
Create a clock in/out entry for the specified user.
"""
conn = get_db_connection()
cursor = conn.cursor()
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
cursor.execute("""
INSERT INTO entries (user_id, entrytype, ts)
VALUES (?, ?, ?)
""", (user_id, entry_type, timestamp))
conn.commit()
conn.close()
def generate_report(user_id, begin_date, end_date):
"""
Generate total worked hours for a user between two dates.
Assumptions:
- Entries alternate correctly between 'in' and 'out'
- Missing pairs are ignored
"""
conn = get_db_connection()
cursor = conn.cursor()
# Inclusive date range
begin_dt = datetime.strptime(begin_date, "%Y-%m-%dT%H:%M")
end_dt = datetime.strptime(end_date, "%Y-%m-%dT%H:%M")
# Include entire ending day
end_dt = end_dt + timedelta(days=1)
cursor.execute("""
SELECT entrytype, ts
FROM entries
WHERE user_id = ?
AND ts >= ?
AND ts < ?
ORDER BY ts ASC
""", (
user_id,
begin_dt.strftime("%Y-%m-%d %H:%M"),
end_dt.strftime("%Y-%m-%d %H:%M")
))
rows = cursor.fetchall()
conn.close()
total_seconds = 0
clock_in_time = None
for row in rows:
entry_type = row["entrytype"]
timestamp = datetime.strptime(row["ts"], "%Y-%m-%d %H:%M:%S")
if entry_type == "in":
clock_in_time = timestamp
elif entry_type == "out" and clock_in_time is not None:
delta = timestamp - clock_in_time
total_seconds += delta.total_seconds()
clock_in_time = None
total_hours = total_seconds / 3600.0
return round(total_hours, 2)
# -----------------------------------------------------------------------------
# Web UI Routes
# -----------------------------------------------------------------------------
@app.route("/", methods=["GET", "POST"])
def index():
"""
Main application page.
"""
conn = get_db_connection()
users = conn.execute("""
SELECT id, name
FROM users
ORDER BY name
""").fetchall()
conn.close()
report_hours = None
if request.method == "POST":
user_id = request.form.get("user_id")
action = request.form.get("action")
if action == "clock_in":
create_entry(user_id, "in")
elif action == "clock_out":
create_entry(user_id, "out")
elif action == "report":
begin_date = request.form.get("begin_date")
end_date = request.form.get("end_date")
report_hours = generate_report(
user_id,
begin_date,
end_date
)
return render_template(
"index.html",
users=users,
report_hours=report_hours
)
# -----------------------------------------------------------------------------
# CRUD API
# -----------------------------------------------------------------------------
@app.route("/api/users", methods=["GET"])
def api_get_users():
"""
Return all users.
"""
conn = get_db_connection()
users = conn.execute("""
SELECT id, name
FROM users
""").fetchall()
conn.close()
return jsonify([dict(user) for user in users])
@app.route("/api/entries", methods=["GET"])
def api_get_entries():
"""
Return all entries.
"""
conn = get_db_connection()
entries = conn.execute("""
SELECT id, user_id, entrytype, ts
FROM entries
ORDER BY ts DESC
""").fetchall()
conn.close()
return jsonify([dict(entry) for entry in entries])
@app.route("/api/entries", methods=["POST"])
def api_create_entry():
"""
Create a clock entry.
"""
data = request.get_json()
user_id = data.get("user_id")
entry_type = data.get("entrytype")
if entry_type not in ("in", "out"):
return jsonify({"error": "Invalid entrytype"}), 400
create_entry(user_id, entry_type)
return jsonify({"status": "success"}), 201
# -----------------------------------------------------------------------------
# Application Entry Point
# -----------------------------------------------------------------------------
if __name__ == "__main__":
initialize_database()
app.run(
host="0.0.0.0",
port=5000,
debug=False
)