From dbd8b6e6accd76965d8146299177fa2b3e4dd919 Mon Sep 17 00:00:00 2001 From: Andrew Kesterson Date: Tue, 16 Jun 2026 12:16:41 -0400 Subject: [PATCH] Qwen broke several things with the last features. Fix those. Handle user timezones properly. Use a textarea for the comments instead of a text input. --- app.py | 245 ++++++++++++++++++++++--------------------- templates/index.html | 46 ++++++-- 2 files changed, 163 insertions(+), 128 deletions(-) diff --git a/app.py b/app.py index 40a3538..591f05a 100644 --- a/app.py +++ b/app.py @@ -1,18 +1,33 @@ import sqlite3 from datetime import datetime, timedelta, timezone +from zoneinfo import ZoneInfo from flask import ( Flask, request, render_template, jsonify ) +import logging -LEGACY_TIMEZONE = timezone.utc +LEGACY_TIMEZONE = ZoneInfo("America/New_York") app = Flask(__name__) DATABASE = "database/timeclock.db" +def is_timezone_aware(dt): + return dt.tzinfo is not None and dt.tzinfo.utcoffset(dt) is not None + +def make_timezone_aware(dt, tzstr): + if tzstr: + tzval = datetime.strptime(tzstr.replace(":", ""), "%z").tzinfo + else: + tzval = LEGACY_TIMEZONE + if not is_timezone_aware(dt): + return dt.replace(tzinfo=tzval) + else: + return dt + def get_db_connection(): conn = sqlite3.connect(DATABASE) conn.row_factory = sqlite3.Row @@ -51,20 +66,34 @@ def initialize_database(): def parse_iso_datetime(value): normalized = value.replace(" ", "T") dt = datetime.fromisoformat(normalized) - if dt.tzinfo is None: - dt = dt.replace(tzinfo=LEGACY_TIMEZONE) return dt -def utc_now(): - return datetime.now(timezone.utc) +def est_now(): + return datetime.now(ZoneInfo("America/New_York")) -def create_entry(user_id, entry_type, client_timestamp=None, comments=None): +def pay_hours(user_id, begin_date, end_date): + conn = get_db_connection() + conn.set_trace_callback(logging.error) + cursor = conn.cursor() + begin_dt = parse_iso_datetime(begin_date) + end_dt = parse_iso_datetime(end_date) + cursor.execute(""" + UPDATE entries + SET paid = TRUE + WHERE user_id = ? + AND datetime(ts) BETWEEN datetime(?) AND datetime(?) + """, (user_id, begin_dt.isoformat(), end_dt.isoformat())) + rows = cursor.fetchall() + conn.commit() + conn.close() + +def create_entry(user_id, entry_type, client_timestamp=None, comments=None, client_timezone=None): conn = get_db_connection() cursor = conn.cursor() if client_timestamp: - timestamp = parse_iso_datetime(client_timestamp) + timestamp = make_timezone_aware(parse_iso_datetime(client_timestamp), client_timezone) else: - timestamp = utc_now() + timestamp = est_now() if comments: cursor.execute(""" INSERT INTO entries (user_id, entrytype, ts, paid, comments) @@ -78,46 +107,125 @@ def create_entry(user_id, entry_type, client_timestamp=None, comments=None): conn.commit() conn.close() -def generate_report(user_id, begin_date, end_date): +def generate_report(user_id, begin_date, end_date, client_timezone): + logging.error(f"user_id={user_id}, begin_date={begin_date}, end_date={end_date}") conn = get_db_connection() cursor = conn.cursor() - begin_dt = parse_iso_datetime(begin_date) - end_dt = parse_iso_datetime(end_date) + begin_dt = make_timezone_aware(parse_iso_datetime(begin_date), client_timezone) + end_dt = make_timezone_aware(parse_iso_datetime(end_date), client_timezone) + logging.error(f"begin_dt={begin_dt}, end_dt={end_dt}") cursor.execute(""" SELECT entrytype, ts, paid, comments FROM entries WHERE user_id = ? - AND ts BETWEEN ? AND ? + AND datetime(ts) BETWEEN datetime(?) AND datetime(?) ORDER BY ts ASC - """, (user_id, begin_dt, end_dt)) + """, (user_id, begin_dt.isoformat(), end_dt.isoformat())) rows = cursor.fetchall() conn.close() total_seconds = 0 + clock_in_time = None paid_seconds = 0 actions = [] for row in rows: timestamp = parse_iso_datetime(row["ts"]) if timestamp < begin_dt or timestamp > end_dt: continue - delta = end_dt - timestamp - if row["paid"]: - paid_seconds += delta.total_seconds() - else: + if row["entrytype"] == "in": + clock_in_time = timestamp + elif row["entrytype"] == "out" and clock_in_time is not None: + delta = timestamp - clock_in_time + if row["paid"]: + paid_seconds += delta.total_seconds() total_seconds += delta.total_seconds() if row["comments"]: - actions.append(row["comments"]) + if row["paid"]: + actions.append("(PAID) {}".format(row["comments"])) + else: + actions.append(row["comments"]) total_hours = total_seconds / 3600.0 paid_hours = paid_seconds / 3600.0 return round(total_hours, 2), round(paid_hours, 2), actions @app.route("/", methods=["GET", "POST"]) def index(): + report_hours = None + user_report = {} + all_user_reports = [] + selected_user_id = None + now = est_now() + default_begin = now - timedelta(days=7) + default_end = now conn = get_db_connection() users = conn.execute(""" SELECT id, name FROM users ORDER BY name """).fetchall() + conn.close() + if request.method == "POST": + selected_user_id = request.form.get("user_id") + action = request.form.get("action") + client_timestamp = request.form.get("client_timestamp") + client_timezone = request.form.get("client_timezone") + comments = request.form.get("comments") + if action == "clock_in": + create_entry( + selected_user_id, + "in", + client_timestamp, + comments, + client_timezone + ) + elif action == "clock_out": + create_entry( + selected_user_id, + "out", + client_timestamp, + comments, + client_timezone + ) + elif action == "pay": + begin_date = request.form.get("begin_date") + end_date = request.form.get("end_date") + pay_hours( + selected_user_id, + begin_date, + end_date, + client_timezone + ) + elif action == "report": + begin_date = request.form.get("begin_date") + end_date = request.form.get("end_date") + client_timezone = request.form.get("client_timezone") + report_hours, paid_hours, actions = generate_report( + selected_user_id, + begin_date, + end_date, + client_timezone + ) + user_report = { + "total_hours": report_hours, + "paid_hours": paid_hours, + "actions": actions + } + default_begin = parse_iso_datetime(begin_date) + default_end = parse_iso_datetime(end_date) + for user in users: + total_hours, paid_hours, actions = generate_report( + user["id"], + default_begin.isoformat(), + default_end.isoformat(), + request.form.get("client_timezone") + ) + all_user_reports.append({ + "id": user["id"], + "name": user["name"], + "total_hours": total_hours, + "paid_hours": paid_hours, + "actions": actions + }) + conn = get_db_connection() clocked_in_users = conn.execute(""" SELECT u.id, u.name, last_entry.ts FROM users u @@ -145,65 +253,10 @@ def index(): "name": user["name"], "since": ts.isoformat() }) - report_hours = None - all_user_reports = [] - selected_user_id = None - now = utc_now() - default_begin = now - timedelta(days=7) - default_end = now - if request.method == "POST": - selected_user_id = request.form.get("user_id") - action = request.form.get("action") - client_timestamp = request.form.get("client_timestamp") - comments = request.form.get("comments") - if action == "clock_in": - create_entry( - selected_user_id, - "in", - client_timestamp, - comments - ) - elif action == "clock_out": - create_entry( - selected_user_id, - "out", - client_timestamp, - comments - ) - elif action == "pay": - # Update the paid column for all entries for the selected user to True - create_entry( - selected_user_id, - "pay" - ) - elif action == "report": - begin_date = request.form.get("begin_date") - end_date = request.form.get("end_date") - report_hours, paid_hours, actions = generate_report( - selected_user_id, - begin_date, - end_date - ) - default_begin = parse_iso_datetime(begin_date) - default_end = parse_iso_datetime(end_date) - else: - for user in users: - total_hours, paid_hours, actions = generate_report( - user["id"], - default_begin.isoformat(), - default_end.isoformat() - ) - all_user_reports.append({ - "id": user["id"], - "name": user["name"], - "total_hours": total_hours, - "paid_hours": paid_hours, - "actions": actions - }) return render_template( "index.html", users=users, - report_hours=report_hours, + user_report=user_report, all_user_reports=all_user_reports, clocked_in_users=clocked_in_list, default_begin=default_begin.isoformat(timespec="minutes"), @@ -211,50 +264,6 @@ def index(): selected_user_id=selected_user_id ) -@app.route("/api/users", methods=["GET"]) -def api_get_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(): - conn = get_db_connection() - entries = conn.execute(""" - SELECT id, user_id, entrytype, ts, paid, comments - 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(): - data = request.get_json() - user_id = data.get("user_id") - entry_type = data.get("entrytype") - client_timestamp = data.get("timestamp") - paid = data.get("paid", False) - comments = data.get("comments", "") - if entry_type not in ("in", "out", "pay"): - return jsonify({ - "error": "Invalid entrytype" - }), 400 - create_entry( - user_id, - entry_type, - client_timestamp, - paid, - comments - ) - return jsonify({ - "status": "success" - }), 201 - if __name__ == "__main__": initialize_database() app.run( diff --git a/templates/index.html b/templates/index.html index 8342746..e3f67b2 100644 --- a/templates/index.html +++ b/templates/index.html @@ -68,6 +68,12 @@
+ + + value=""> @@ -161,12 +166,11 @@ Comments - + rows="5" cols="72" + > @@ -190,7 +194,7 @@
-{% if report_hours is not none %} +{% if user_report is not none %}

Custom Report

@@ -199,11 +203,19 @@ Total Hours Worked Paid Hours + Actions - {{ report_hours.total_hours }} - {{ report_hours.paid_hours }} + {{ user_report.total_hours }} + {{ user_report.paid_hours }} + + + @@ -212,6 +224,19 @@