From 7f9783b2fcc249eae43b00ae7cbef0308e70b93f Mon Sep 17 00:00:00 2001 From: Jens Luedicke Date: Fri, 27 Jun 2025 15:14:57 +0200 Subject: [PATCH] Improve logging features (edit and delete). --- app.py | 130 +++++++++++++++++++--- requirements.txt | 3 +- static/css/style.css | 126 +++++++++++++++++++-- templates/contact.html | 22 ---- templates/history.html | 234 +++++++++++++++++++++++++++++++++++++++ templates/index.html | 216 +++++++++++++++++++++++++++++++++++- templates/layout.html | 7 +- templates/timetrack.html | 230 ++++++++++++++++++++++++++++++++++---- 8 files changed, 888 insertions(+), 80 deletions(-) delete mode 100644 templates/contact.html create mode 100644 templates/history.html diff --git a/app.py b/app.py index bb61497..9429bc9 100644 --- a/app.py +++ b/app.py @@ -1,7 +1,8 @@ from flask import Flask, render_template, request, redirect, url_for, jsonify, flash from models import db, TimeEntry, WorkConfig -from datetime import datetime +from datetime import datetime, time import os +from sqlalchemy import func app = Flask(__name__) app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///timetrack.db' @@ -13,13 +14,22 @@ db.init_app(app) @app.route('/') def home(): - # Get the latest time entry that doesn't have a departure time + # Get active time entry (if any) active_entry = TimeEntry.query.filter_by(departure_time=None).first() - # Get all completed time entries, ordered by most recent first - history = TimeEntry.query.filter(TimeEntry.departure_time.isnot(None)).order_by(TimeEntry.arrival_time.desc()).all() + # Get today's date + today = datetime.now().date() - return render_template('index.html', title='Home', active_entry=active_entry, history=history) + # Get time entries for today only + today_start = datetime.combine(today, time.min) + today_end = datetime.combine(today, time.max) + + today_entries = TimeEntry.query.filter( + TimeEntry.arrival_time >= today_start, + TimeEntry.arrival_time <= today_end + ).order_by(TimeEntry.arrival_time.desc()).all() + + return render_template('index.html', title='Home', active_entry=active_entry, history=today_entries) @app.route('/about') def about(): @@ -30,10 +40,6 @@ def contact(): # redacted return render_template('contact.html', title='Contact') -@app.route('/thank-you') -def thank_you(): - return render_template('thank_you.html', title='Thank You') - # We can keep this route as a redirect to home for backward compatibility @app.route('/timetrack') def timetrack(): @@ -65,10 +71,14 @@ def leave(entry_id): final_break_duration = int((departure_time - entry.pause_start_time).total_seconds()) entry.total_break_duration += final_break_duration entry.is_paused = False + entry.pause_start_time = None - # Calculate duration in seconds (excluding breaks) - raw_duration = (departure_time - entry.arrival_time).total_seconds() - entry.duration = int(raw_duration - entry.total_break_duration) + # Calculate work duration considering breaks + entry.duration, effective_break = calculate_work_duration( + entry.arrival_time, + departure_time, + entry.total_break_duration + ) db.session.commit() @@ -77,7 +87,8 @@ def leave(entry_id): 'arrival_time': entry.arrival_time.strftime('%Y-%m-%d %H:%M:%S'), 'departure_time': entry.departure_time.strftime('%Y-%m-%d %H:%M:%S'), 'duration': entry.duration, - 'total_break_duration': entry.total_break_duration + 'total_break_duration': entry.total_break_duration, + 'effective_break_duration': effective_break }) # Add this new route to handle pausing/resuming @@ -152,5 +163,98 @@ def create_tables(): if 'is_paused' not in columns or 'pause_start_time' not in columns or 'total_break_duration' not in columns: print("WARNING: Database schema is outdated. Please run migrate_db.py to update it.") +@app.route('/api/delete/', methods=['DELETE']) +def delete_entry(entry_id): + entry = TimeEntry.query.get_or_404(entry_id) + db.session.delete(entry) + db.session.commit() + return jsonify({'success': True, 'message': 'Entry deleted successfully'}) + +@app.route('/api/update/', methods=['PUT']) +def update_entry(entry_id): + entry = TimeEntry.query.get_or_404(entry_id) + data = request.json + + if 'arrival_time' in data: + try: + entry.arrival_time = datetime.strptime(data['arrival_time'], '%Y-%m-%d %H:%M:%S') + except ValueError: + return jsonify({'success': False, 'message': 'Invalid arrival time format'}), 400 + + if 'departure_time' in data and data['departure_time']: + try: + entry.departure_time = datetime.strptime(data['departure_time'], '%Y-%m-%d %H:%M:%S') + # Recalculate duration if both times are present + if entry.arrival_time and entry.departure_time: + # Calculate work duration considering breaks + entry.duration, _ = calculate_work_duration( + entry.arrival_time, + entry.departure_time, + entry.total_break_duration + ) + except ValueError: + return jsonify({'success': False, 'message': 'Invalid departure time format'}), 400 + + db.session.commit() + return jsonify({ + 'success': True, + 'message': 'Entry updated successfully', + 'entry': { + 'id': entry.id, + 'arrival_time': entry.arrival_time.strftime('%Y-%m-%d %H:%M:%S'), + 'departure_time': entry.departure_time.strftime('%Y-%m-%d %H:%M:%S') if entry.departure_time else None, + 'duration': entry.duration, + 'is_paused': entry.is_paused, + 'total_break_duration': entry.total_break_duration + } + }) + +@app.route('/history') +def history(): + # Get all time entries, ordered by most recent first + all_entries = TimeEntry.query.order_by(TimeEntry.arrival_time.desc()).all() + + return render_template('history.html', title='Time Entry History', entries=all_entries) + +def calculate_work_duration(arrival_time, departure_time, total_break_duration): + """ + Calculate work duration considering both configured and actual break times. + + Args: + arrival_time: Datetime of arrival + departure_time: Datetime of departure + total_break_duration: Actual logged break duration in seconds + + Returns: + tuple: (work_duration_in_seconds, effective_break_duration_in_seconds) + """ + # Calculate raw duration + raw_duration = (departure_time - arrival_time).total_seconds() + + # Get work configuration for break rules + config = WorkConfig.query.order_by(WorkConfig.id.desc()).first() + if not config: + config = WorkConfig() # Use default values if no config exists + + # Calculate mandatory breaks based on work duration + work_hours = raw_duration / 3600 # Convert seconds to hours + configured_break_seconds = 0 + + # Apply primary break if work duration exceeds threshold + if work_hours > config.break_threshold_hours: + configured_break_seconds += config.mandatory_break_minutes * 60 + + # Apply additional break if work duration exceeds additional threshold + if work_hours > config.additional_break_threshold_hours: + configured_break_seconds += config.additional_break_minutes * 60 + + # Use the greater of configured breaks or actual logged breaks + effective_break_duration = max(configured_break_seconds, total_break_duration) + + # Calculate final work duration + work_duration = int(raw_duration - effective_break_duration) + + return work_duration, effective_break_duration + if __name__ == '__main__': app.run(debug=True) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 2ddd91d..ade5b39 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,4 +5,5 @@ MarkupSafe==2.0.1 itsdangerous==2.0.1 click==8.0.1 Flask-SQLAlchemy==2.5.1 -SQLAlchemy==1.4.23 \ No newline at end of file +SQLAlchemy==1.4.23 +python-dotenv==0.19.0 \ No newline at end of file diff --git a/static/css/style.css b/static/css/style.css index 380efa4..7bd9747 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -69,39 +69,51 @@ main { } .form-group { - margin-bottom: 1rem; + margin-bottom: 15px; } .form-group label { display: block; - margin-bottom: 0.5rem; + margin-bottom: 5px; + font-weight: bold; } .form-group input, .form-group textarea { width: 100%; - padding: 0.5rem; + padding: 8px; border: 1px solid #ddd; - border-radius: 3px; + border-radius: 4px; } button { background-color: #4CAF50; color: white; border: none; - padding: 0.5rem 1rem; + padding: 8px 16px; cursor: pointer; - border-radius: 3px; + border-radius: 4px; } .btn { - display: inline-block; + padding: 8px 16px; + border: none; + border-radius: 4px; + cursor: pointer; background-color: #4CAF50; color: white; - padding: 0.5rem 1rem; - text-decoration: none; - border-radius: 3px; - margin-top: 1rem; +} + +.btn:hover { + background-color: #45a049; +} + +.btn-danger { + background-color: #f44336; +} + +.btn-danger:hover { + background-color: #d32f2f; } footer { @@ -278,7 +290,8 @@ footer { .config-form small { display: block; color: #666; - margin-top: 0.25rem; + margin-top: 4px; + font-style: italic; } .btn-primary { @@ -311,4 +324,93 @@ footer { background-color: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; +} + +/* Modal styles */ +.modal { + display: none; + position: fixed; + z-index: 1000; + left: 0; + top: 0; + width: 100%; + height: 100%; + background-color: rgba(0,0,0,0.5); +} + +.modal-content { + background-color: #fff; + margin: 10% auto; + padding: 20px; + border-radius: 5px; + width: 80%; + max-width: 500px; + box-shadow: 0 4px 8px rgba(0,0,0,0.2); +} + +.close { + color: #aaa; + float: right; + font-size: 28px; + font-weight: bold; + cursor: pointer; +} + +.close:hover, +.close:focus { + color: #000; + text-decoration: none; +} + +.modal-actions { + display: flex; + justify-content: flex-end; + gap: 10px; + margin-top: 20px; +} + +.edit-entry-btn, .delete-entry-btn { + padding: 5px 10px; + margin-right: 5px; + border: none; + border-radius: 3px; + cursor: pointer; +} + +.edit-entry-btn { + background-color: #2196F3; + color: white; +} + +.edit-entry-btn:hover { + background-color: #0b7dda; +} + +.delete-entry-btn { + background-color: #f44336; + color: white; +} + +.delete-entry-btn:hover { + background-color: #d32f2f; +} + +input[type="date"], input[type="time"] { + font-family: monospace; + padding: 8px; + width: 100%; +} + +/* Force consistent date format display */ +input[type="date"]::-webkit-datetime-edit, +input[type="time"]::-webkit-datetime-edit { + font-family: monospace; +} + +/* Add some styling to the format hints */ +.form-group small { + display: block; + color: #666; + margin-top: 4px; + font-style: italic; } \ No newline at end of file diff --git a/templates/contact.html b/templates/contact.html deleted file mode 100644 index 6df6dc5..0000000 --- a/templates/contact.html +++ /dev/null @@ -1,22 +0,0 @@ -{% extends "layout.html" %} - -{% block content %} -
-

Contact Us

-
-
- - -
-
- - -
-
- - -
- -
-
-{% endblock %} \ No newline at end of file diff --git a/templates/history.html b/templates/history.html new file mode 100644 index 0000000..20234e0 --- /dev/null +++ b/templates/history.html @@ -0,0 +1,234 @@ +{% extends "layout.html" %} + +{% block content %} +
+

Complete Time Entry History

+ +
+ {% if entries %} + + + + + + + + + + + + + {% for entry in entries %} + + + + + + + + + {% endfor %} + +
DateArrivalDepartureWork DurationBreak DurationActions
{{ entry.arrival_time.strftime('%Y-%m-%d') }}{{ entry.arrival_time.strftime('%H:%M:%S') }}{{ entry.departure_time.strftime('%H:%M:%S') if entry.departure_time else 'Active' }}{{ '%d:%02d:%02d'|format(entry.duration//3600, (entry.duration%3600)//60, entry.duration%60) if entry.duration is not none else 'In progress' }}{{ '%d:%02d:%02d'|format(entry.total_break_duration//3600, (entry.total_break_duration%3600)//60, entry.total_break_duration%60) if entry.total_break_duration is not none else '00:00:00' }} + + +
+ {% else %} +

No time entries recorded yet.

+ {% endif %} +
+
+ + + + + + + + +{% endblock %} \ No newline at end of file diff --git a/templates/index.html b/templates/index.html index 75b84f9..bf84df1 100644 --- a/templates/index.html +++ b/templates/index.html @@ -53,16 +53,21 @@ Departure Work Duration Break Duration + Actions {% for entry in history %} - + {{ entry.arrival_time.strftime('%Y-%m-%d') }} {{ entry.arrival_time.strftime('%H:%M:%S') }} - {{ entry.departure_time.strftime('%H:%M:%S') }} - {{ '%d:%02d:%02d'|format(entry.duration//3600, (entry.duration%3600)//60, entry.duration%60) }} - {{ '%d:%02d:%02d'|format(entry.total_break_duration//3600, (entry.total_break_duration%3600)//60, entry.total_break_duration%60) }} + {{ entry.departure_time.strftime('%H:%M:%S') if entry.departure_time else 'Active' }} + {{ '%d:%02d:%02d'|format(entry.duration//3600, (entry.duration%3600)//60, entry.duration%60) if entry.duration is not none else 'In progress' }} + {{ '%d:%02d:%02d'|format(entry.total_break_duration//3600, (entry.total_break_duration%3600)//60, entry.total_break_duration%60) if entry.total_break_duration is not none else '00:00:00' }} + + + + {% endfor %} @@ -91,4 +96,207 @@

No complicated setup or configuration needed. Start tracking right away!

+ + + + + + + + + {% endblock %} \ No newline at end of file diff --git a/templates/layout.html b/templates/layout.html index e9bbc38..aee3363 100644 --- a/templates/layout.html +++ b/templates/layout.html @@ -11,10 +11,9 @@ @@ -24,7 +23,7 @@
-

© 2023 TimeTrack

+

© 2025 TimeTrack

diff --git a/templates/timetrack.html b/templates/timetrack.html index 43aac57..c325b58 100644 --- a/templates/timetrack.html +++ b/templates/timetrack.html @@ -22,30 +22,212 @@

Time Entry History

- {% if history %} - - - - - - - - - - - {% for entry in history %} - - - - - - - {% endfor %} - -
DateArrivalDepartureDuration
{{ entry.arrival_time.strftime('%Y-%m-%d') }}{{ entry.arrival_time.strftime('%H:%M:%S') }}{{ entry.departure_time.strftime('%H:%M:%S') }}{{ '%d:%02d:%02d'|format(entry.duration//3600, (entry.duration%3600)//60, entry.duration%60) }}
- {% else %} -

No time entries recorded yet.

- {% endif %} + + + + + + + + + + + + + {% for entry in history %} + + + + + + + + {% endfor %} + +
DateArrivalDepartureDurationActions
{{ entry.arrival_time.strftime('%Y-%m-%d') }}{{ entry.arrival_time.strftime('%H:%M:%S') }}{{ entry.departure_time.strftime('%H:%M:%S') if entry.departure_time else 'Active' }}{{ (entry.duration // 3600)|string + 'h ' + ((entry.duration % 3600) // 60)|string + 'm' if entry.duration else 'N/A' }} + + +
+ + + + + +
+ + {% endblock %} \ No newline at end of file