commit a8d1f33874f44295eff63fec26c39d72a781200a Author: Jens Luedicke Date: Fri Jun 27 15:14:57 2025 +0200 Initial commit. diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..1328b9b --- /dev/null +++ b/Pipfile @@ -0,0 +1,19 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +flask = "==2.0.1" +werkzeug = "==2.0.1" +jinja2 = "==3.0.1" +markupsafe = "==2.0.1" +itsdangerous = "==2.0.1" +click = "==8.0.1" +flask-sqlalchemy = "==2.5.1" +sqlalchemy = "==1.4.23" + +[dev-packages] + +[requires] +python_version = "3.12" diff --git a/app.py b/app.py new file mode 100644 index 0000000..bb61497 --- /dev/null +++ b/app.py @@ -0,0 +1,156 @@ +from flask import Flask, render_template, request, redirect, url_for, jsonify, flash +from models import db, TimeEntry, WorkConfig +from datetime import datetime +import os + +app = Flask(__name__) +app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///timetrack.db' +app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False +app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'dev_key_for_timetrack') # Add secret key for flash messages + +# Initialize the database with the app +db.init_app(app) + +@app.route('/') +def home(): + # Get the latest time entry that doesn't have a departure time + 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() + + return render_template('index.html', title='Home', active_entry=active_entry, history=history) + +@app.route('/about') +def about(): + return render_template('about.html', title='About') + +@app.route('/contact', methods=['GET', 'POST']) +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(): + return redirect(url_for('home')) + +@app.route('/api/arrive', methods=['POST']) +def arrive(): + # Create a new time entry with arrival time + new_entry = TimeEntry(arrival_time=datetime.now()) + db.session.add(new_entry) + db.session.commit() + + return jsonify({ + 'id': new_entry.id, + 'arrival_time': new_entry.arrival_time.strftime('%Y-%m-%d %H:%M:%S') + }) + +@app.route('/api/leave/', methods=['POST']) +def leave(entry_id): + # Find the time entry + entry = TimeEntry.query.get_or_404(entry_id) + + # Set the departure time + departure_time = datetime.now() + entry.departure_time = departure_time + + # If currently paused, add the final break duration + if entry.is_paused and entry.pause_start_time: + final_break_duration = int((departure_time - entry.pause_start_time).total_seconds()) + entry.total_break_duration += final_break_duration + entry.is_paused = False + + # Calculate duration in seconds (excluding breaks) + raw_duration = (departure_time - entry.arrival_time).total_seconds() + entry.duration = int(raw_duration - entry.total_break_duration) + + db.session.commit() + + return jsonify({ + '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'), + 'duration': entry.duration, + 'total_break_duration': entry.total_break_duration + }) + +# Add this new route to handle pausing/resuming +@app.route('/api/toggle-pause/', methods=['POST']) +def toggle_pause(entry_id): + # Find the time entry + entry = TimeEntry.query.get_or_404(entry_id) + + now = datetime.now() + + if entry.is_paused: + # Resuming work - calculate break duration + break_duration = int((now - entry.pause_start_time).total_seconds()) + entry.total_break_duration += break_duration + entry.is_paused = False + entry.pause_start_time = None + + message = "Work resumed" + else: + # Pausing work + entry.is_paused = True + entry.pause_start_time = now + + message = "Work paused" + + db.session.commit() + + return jsonify({ + 'id': entry.id, + 'is_paused': entry.is_paused, + 'total_break_duration': entry.total_break_duration, + 'message': message + }) + +@app.route('/config', methods=['GET', 'POST']) +def config(): + # Get current configuration or create default if none exists + config = WorkConfig.query.order_by(WorkConfig.id.desc()).first() + if not config: + config = WorkConfig() + db.session.add(config) + db.session.commit() + + if request.method == 'POST': + try: + # Update configuration with form data + config.work_hours_per_day = float(request.form.get('work_hours_per_day', 8.0)) + config.mandatory_break_minutes = int(request.form.get('mandatory_break_minutes', 30)) + config.break_threshold_hours = float(request.form.get('break_threshold_hours', 6.0)) + config.additional_break_minutes = int(request.form.get('additional_break_minutes', 15)) + config.additional_break_threshold_hours = float(request.form.get('additional_break_threshold_hours', 9.0)) + + db.session.commit() + flash('Configuration updated successfully!', 'success') + return redirect(url_for('config')) + except ValueError: + flash('Please enter valid numbers for all fields', 'error') + + return render_template('config.html', title='Configuration', config=config) + +# Create the database tables before first request +@app.before_first_request +def create_tables(): + # This will only create tables that don't exist yet + db.create_all() + + # Check if we need to add new columns + from sqlalchemy import inspect + inspector = inspect(db.engine) + columns = [column['name'] for column in inspector.get_columns('time_entry')] + + 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.") + +if __name__ == '__main__': + app.run(debug=True) \ No newline at end of file diff --git a/migrate_db.py b/migrate_db.py new file mode 100644 index 0000000..e78e85e --- /dev/null +++ b/migrate_db.py @@ -0,0 +1,78 @@ +from app import app, db +import sqlite3 +import os + +def migrate_database(): + db_path = 'timetrack.db' + + # Check if database exists + if not os.path.exists(db_path): + print("Database doesn't exist. Creating new database.") + with app.app_context(): + db.create_all() + return + + print("Migrating existing database...") + + # Connect to the database + conn = sqlite3.connect(db_path) + cursor = conn.cursor() + + # Check if the time_entry columns already exist + cursor.execute("PRAGMA table_info(time_entry)") + time_entry_columns = [column[1] for column in cursor.fetchall()] + + # Add new columns to time_entry if they don't exist + if 'is_paused' not in time_entry_columns: + print("Adding is_paused column to time_entry...") + cursor.execute("ALTER TABLE time_entry ADD COLUMN is_paused BOOLEAN DEFAULT 0") + + if 'pause_start_time' not in time_entry_columns: + print("Adding pause_start_time column to time_entry...") + cursor.execute("ALTER TABLE time_entry ADD COLUMN pause_start_time TIMESTAMP") + + if 'total_break_duration' not in time_entry_columns: + print("Adding total_break_duration column to time_entry...") + cursor.execute("ALTER TABLE time_entry ADD COLUMN total_break_duration INTEGER DEFAULT 0") + + # Check if the work_config table exists + cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='work_config'") + if not cursor.fetchone(): + print("Creating work_config table...") + cursor.execute(""" + CREATE TABLE work_config ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + work_hours_per_day FLOAT DEFAULT 8.0, + mandatory_break_minutes INTEGER DEFAULT 30, + break_threshold_hours FLOAT DEFAULT 6.0, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + """) + # Insert default config + cursor.execute(""" + INSERT INTO work_config (work_hours_per_day, mandatory_break_minutes, break_threshold_hours) + VALUES (8.0, 30, 6.0) + """) + else: + # Check if the work_config columns already exist + cursor.execute("PRAGMA table_info(work_config)") + work_config_columns = [column[1] for column in cursor.fetchall()] + + # Add new columns to work_config if they don't exist + if 'additional_break_minutes' not in work_config_columns: + print("Adding additional_break_minutes column to work_config...") + cursor.execute("ALTER TABLE work_config ADD COLUMN additional_break_minutes INTEGER DEFAULT 15") + + if 'additional_break_threshold_hours' not in work_config_columns: + print("Adding additional_break_threshold_hours column to work_config...") + cursor.execute("ALTER TABLE work_config ADD COLUMN additional_break_threshold_hours FLOAT DEFAULT 9.0") + + # Commit changes and close connection + conn.commit() + conn.close() + + print("Database migration completed successfully!") + +if __name__ == "__main__": + migrate_database() \ No newline at end of file diff --git a/models.py b/models.py new file mode 100644 index 0000000..1e92c1b --- /dev/null +++ b/models.py @@ -0,0 +1,29 @@ +from flask_sqlalchemy import SQLAlchemy +from datetime import datetime + +db = SQLAlchemy() + +class TimeEntry(db.Model): + id = db.Column(db.Integer, primary_key=True) + arrival_time = db.Column(db.DateTime, nullable=False) + departure_time = db.Column(db.DateTime, nullable=True) + duration = db.Column(db.Integer, nullable=True) # Duration in seconds + is_paused = db.Column(db.Boolean, default=False) + pause_start_time = db.Column(db.DateTime, nullable=True) + total_break_duration = db.Column(db.Integer, default=0) # Total break duration in seconds + + def __repr__(self): + return f'' + +class WorkConfig(db.Model): + id = db.Column(db.Integer, primary_key=True) + work_hours_per_day = db.Column(db.Float, default=8.0) # Default 8 hours + mandatory_break_minutes = db.Column(db.Integer, default=30) # Default 30 minutes + break_threshold_hours = db.Column(db.Float, default=6.0) # Work hours that trigger mandatory break + additional_break_minutes = db.Column(db.Integer, default=15) # Default 15 minutes for additional break + additional_break_threshold_hours = db.Column(db.Float, default=9.0) # Work hours that trigger additional break + created_at = db.Column(db.DateTime, default=datetime.now) + updated_at = db.Column(db.DateTime, default=datetime.now, onupdate=datetime.now) + + def __repr__(self): + return f'' \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..2ddd91d --- /dev/null +++ b/requirements.txt @@ -0,0 +1,8 @@ +Flask==2.0.1 +Werkzeug==2.0.1 +Jinja2==3.0.1 +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 diff --git a/static/css/style.css b/static/css/style.css new file mode 100644 index 0000000..380efa4 --- /dev/null +++ b/static/css/style.css @@ -0,0 +1,314 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: Arial, sans-serif; + line-height: 1.6; + color: #333; +} + +header { + background-color: #4CAF50; + padding: 1rem; +} + +nav ul { + display: flex; + list-style: none; + justify-content: center; +} + +nav ul li { + margin: 0 1rem; +} + +nav ul li a { + color: white; + text-decoration: none; +} + +main { + max-width: 1200px; + margin: 2rem auto; + padding: 0 1rem; +} + +.hero { + text-align: center; + padding: 2rem 0; + background-color: #f9f9f9; + border-radius: 5px; + margin-bottom: 2rem; +} + +.features { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: 2rem; + margin: 2rem 0; +} + +.feature { + background-color: #f9f9f9; + padding: 1.5rem; + border-radius: 5px; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); +} + +.feature h3 { + color: #4CAF50; + margin-top: 0; +} + +.about, .contact, .thank-you { + max-width: 800px; + margin: 0 auto; +} + +.form-group { + margin-bottom: 1rem; +} + +.form-group label { + display: block; + margin-bottom: 0.5rem; +} + +.form-group input, +.form-group textarea { + width: 100%; + padding: 0.5rem; + border: 1px solid #ddd; + border-radius: 3px; +} + +button { + background-color: #4CAF50; + color: white; + border: none; + padding: 0.5rem 1rem; + cursor: pointer; + border-radius: 3px; +} + +.btn { + display: inline-block; + background-color: #4CAF50; + color: white; + padding: 0.5rem 1rem; + text-decoration: none; + border-radius: 3px; + margin-top: 1rem; +} + +footer { + text-align: center; + padding: 1rem; + background-color: #f4f4f4; + margin-top: 2rem; +} + +/* Time tracking specific styles */ +.timetrack-container { + max-width: 800px; + margin: 0 auto 3rem auto; +} + +.timer-section { + background-color: #f5f5f5; + padding: 2rem; + border-radius: 5px; + margin-bottom: 2rem; + text-align: center; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); +} + +#timer { + font-size: 3rem; + font-weight: bold; + margin: 1rem 0; + font-family: monospace; + color: #333; +} + +.arrive-btn { + background-color: #4CAF50; + font-size: 1.2rem; + padding: 0.8rem 2rem; + transition: background-color 0.3s; +} + +.arrive-btn:hover { + background-color: #45a049; +} + +.leave-btn { + background-color: #f44336; + font-size: 1.2rem; + padding: 0.8rem 2rem; + transition: background-color 0.3s; +} + +.leave-btn:hover { + background-color: #d32f2f; +} + +.time-history { + width: 100%; + border-collapse: collapse; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); +} + +.time-history th, .time-history td { + padding: 0.8rem; + text-align: left; + border-bottom: 1px solid #ddd; +} + +.time-history th { + background-color: #f2f2f2; + font-weight: bold; +} + +.time-history tr:hover { + background-color: #f5f5f5; +} + +.button-group { + display: flex; + justify-content: center; + gap: 1rem; + margin-top: 1rem; +} + +.pause-btn { + background-color: #ff9800; + font-size: 1.2rem; + padding: 0.8rem 2rem; + transition: background-color 0.3s; +} + +.pause-btn:hover { + background-color: #f57c00; +} + +.resume-btn { + background-color: #2196F3; + font-size: 1.2rem; + padding: 0.8rem 2rem; + transition: background-color 0.3s; +} + +.resume-btn:hover { + background-color: #1976D2; +} + +.break-info { + color: #ff9800; + font-weight: bold; + margin: 0.5rem 0; +} + +.break-total { + color: #666; + font-size: 0.9rem; + margin: 0.5rem 0; +} + +.notification { + position: fixed; + top: 20px; + right: 20px; + padding: 15px 25px; + background-color: #4CAF50; + color: white; + border-radius: 4px; + box-shadow: 0 2px 10px rgba(0,0,0,0.2); + z-index: 1000; + animation: fadeIn 0.3s, fadeOut 0.3s 2.7s; +} + +@keyframes fadeIn { + from { opacity: 0; transform: translateY(-20px); } + to { opacity: 1; transform: translateY(0); } +} + +@keyframes fadeOut { + from { opacity: 1; transform: translateY(0); } + to { opacity: 0; transform: translateY(-20px); } +} + +.config-container { + max-width: 800px; + margin: 0 auto 3rem auto; + padding: 1rem; + background-color: #f9f9f9; + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.form-section { + border: 1px solid #e0e0e0; + border-radius: 5px; + padding: 1rem; + margin-bottom: 1.5rem; + background-color: #f9f9f9; +} + +.form-section h3 { + margin-top: 0; + margin-bottom: 1rem; + color: #333; + font-size: 1.2rem; +} + +.config-form .form-group { + margin-bottom: 1.5rem; +} + +.config-form label { + display: block; + margin-bottom: 0.5rem; + font-weight: bold; +} + +.config-form small { + display: block; + color: #666; + margin-top: 0.25rem; +} + +.btn-primary { + background-color: #007bff; + color: white; + border: none; + padding: 0.5rem 1rem; + border-radius: 4px; + cursor: pointer; + font-size: 1rem; +} + +.btn-primary:hover { + background-color: #0069d9; +} + +.alert { + padding: 0.75rem 1rem; + margin-bottom: 1rem; + border-radius: 4px; +} + +.alert-success { + background-color: #d4edda; + color: #155724; + border: 1px solid #c3e6cb; +} + +.alert-error { + background-color: #f8d7da; + color: #721c24; + border: 1px solid #f5c6cb; +} \ No newline at end of file diff --git a/static/js/script.js b/static/js/script.js new file mode 100644 index 0000000..218e2c4 --- /dev/null +++ b/static/js/script.js @@ -0,0 +1,135 @@ +document.addEventListener('DOMContentLoaded', function() { + console.log('Flask app loaded successfully!'); + // Timer functionality + const timer = document.getElementById('timer'); + const arriveBtn = document.getElementById('arrive-btn'); + const leaveBtn = document.getElementById('leave-btn'); + const pauseBtn = document.getElementById('pause-btn'); + + let isPaused = false; + let timerInterval; + + // Start timer if we're on a page with an active timer + if (timer) { + const startTime = parseInt(timer.dataset.start); + const totalBreakDuration = parseInt(timer.dataset.breaks || 0); + isPaused = timer.dataset.paused === 'true'; + + // Update the pause button text based on current state + if (pauseBtn) { + updatePauseButtonText(); + } + + // Update timer every second + function updateTimer() { + if (isPaused) return; + + const now = Math.floor(Date.now() / 1000); + const diff = now - startTime - totalBreakDuration; + + const hours = Math.floor(diff / 3600); + const minutes = Math.floor((diff % 3600) / 60); + const seconds = diff % 60; + + timer.textContent = `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`; + } + + // Initial update + updateTimer(); + + // Set interval for updates + timerInterval = setInterval(updateTimer, 1000); + } + + function updatePauseButtonText() { + if (pauseBtn) { + if (isPaused) { + pauseBtn.textContent = 'Resume Work'; + pauseBtn.classList.add('resume-btn'); + pauseBtn.classList.remove('pause-btn'); + } else { + pauseBtn.textContent = 'Pause'; + pauseBtn.classList.add('pause-btn'); + pauseBtn.classList.remove('resume-btn'); + } + } + } + + // Handle arrive button click + if (arriveBtn) { + arriveBtn.addEventListener('click', function() { + fetch('/api/arrive', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + } + }) + .then(response => response.json()) + .then(data => { + // Reload the page to show the active timer + window.location.reload(); + }) + .catch(error => { + console.error('Error:', error); + alert('Failed to record arrival time. Please try again.'); + }); + }); + } + + // Handle pause/resume button click + if (pauseBtn) { + pauseBtn.addEventListener('click', function() { + const entryId = pauseBtn.dataset.id; + + fetch(`/api/toggle-pause/${entryId}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + } + }) + .then(response => response.json()) + .then(data => { + isPaused = data.is_paused; + updatePauseButtonText(); + + // Show a notification + const notification = document.createElement('div'); + notification.className = 'notification'; + notification.textContent = data.message; + document.body.appendChild(notification); + + // Remove notification after 3 seconds + setTimeout(() => { + notification.remove(); + }, 3000); + }) + .catch(error => { + console.error('Error:', error); + alert('Failed to pause/resume. Please try again.'); + }); + }); + } + + // Handle leave button click + if (leaveBtn) { + leaveBtn.addEventListener('click', function() { + const entryId = leaveBtn.dataset.id; + + fetch(`/api/leave/${entryId}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + } + }) + .then(response => response.json()) + .then(data => { + // Reload the page to update the UI + window.location.reload(); + }) + .catch(error => { + console.error('Error:', error); + alert('Failed to record departure time. Please try again.'); + }); + }); + } +}); diff --git a/templates/about.html b/templates/about.html new file mode 100644 index 0000000..5edbb20 --- /dev/null +++ b/templates/about.html @@ -0,0 +1,9 @@ +{% extends "layout.html" %} + +{% block content %} +
+

About Us

+

This is a simple Flask web application created as a demonstration.

+

Learn more about our team and mission here.

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

Work Configuration

+ + {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} +
{{ message }}
+ {% endfor %} + {% endif %} + {% endwith %} + +
+
+ + + Standard number of work hours in a day +
+ +
+

Primary Break

+
+ + + Required break time in minutes +
+ +
+ + + Work hours after which a break becomes mandatory +
+
+ +
+

Additional Break

+
+ + + Duration of additional break in minutes +
+ +
+ + + Work hours after which an additional break becomes necessary +
+
+ + +
+
+{% endblock %} \ No newline at end of file diff --git a/templates/contact.html b/templates/contact.html new file mode 100644 index 0000000..6df6dc5 --- /dev/null +++ b/templates/contact.html @@ -0,0 +1,22 @@ +{% extends "layout.html" %} + +{% block content %} +
+

Contact Us

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

Welcome to TimeTrack

+

Track your work hours easily and efficiently

+
+ +
+

Time Tracking

+ +
+ {% if active_entry %} +
+

Currently Working

+

Started at: {{ active_entry.arrival_time.strftime('%Y-%m-%d %H:%M:%S') }}

+
00:00:00
+ + {% if active_entry.is_paused %} +

On break since {{ active_entry.pause_start_time.strftime('%H:%M:%S') }}

+ {% endif %} + + {% if active_entry.total_break_duration > 0 %} +

Total break time: {{ '%d:%02d:%02d'|format(active_entry.total_break_duration//3600, (active_entry.total_break_duration%3600)//60, active_entry.total_break_duration%60) }}

+ {% endif %} + +
+ + +
+
+ {% else %} +
+

Not Currently Working

+ +
+ {% endif %} +
+ +
+

Time Entry History

+ {% if history %} + + + + + + + + + + + + {% for entry in history %} + + + + + + + + {% endfor %} + +
DateArrivalDepartureWork DurationBreak Duration
{{ 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) }}
+ {% else %} +

No time entries recorded yet.

+ {% endif %} +
+
+ +
+
+

Easy Time Tracking

+

Simply click "Arrive" when you start working and "Leave" when you're done.

+
+
+

Break Management

+

Use the Pause button when taking breaks. Your break time is tracked separately.

+
+
+

Detailed History

+

View your complete work history with precise timestamps and durations.

+
+
+

Simple Interface

+

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 new file mode 100644 index 0000000..e9bbc38 --- /dev/null +++ b/templates/layout.html @@ -0,0 +1,32 @@ + + + + + + TimeTrack - {{ title }} + + + +
+ +
+ +
+ {% block content %}{% endblock %} +
+ +
+

© 2023 TimeTrack

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

Time Tracking

+ +
+ {% if active_entry %} +
+

Currently Working

+

Started at: {{ active_entry.arrival_time.strftime('%Y-%m-%d %H:%M:%S') }}

+
00:00:00
+ +
+ {% else %} +
+

Not Currently Working

+ +
+ {% endif %} +
+ +
+

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 %} +
+
+{% endblock %} \ No newline at end of file