Initial commit.
This commit is contained in:
19
Pipfile
Normal file
19
Pipfile
Normal file
@@ -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"
|
||||
156
app.py
Normal file
156
app.py
Normal file
@@ -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/<int:entry_id>', 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/<int:entry_id>', 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)
|
||||
78
migrate_db.py
Normal file
78
migrate_db.py
Normal file
@@ -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()
|
||||
29
models.py
Normal file
29
models.py
Normal file
@@ -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'<TimeEntry {self.id}: {self.arrival_time} - {self.departure_time}>'
|
||||
|
||||
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'<WorkConfig {self.id}: {self.work_hours_per_day}h/day, {self.mandatory_break_minutes}min break>'
|
||||
8
requirements.txt
Normal file
8
requirements.txt
Normal file
@@ -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
|
||||
314
static/css/style.css
Normal file
314
static/css/style.css
Normal file
@@ -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;
|
||||
}
|
||||
135
static/js/script.js
Normal file
135
static/js/script.js
Normal file
@@ -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.');
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
9
templates/about.html
Normal file
9
templates/about.html
Normal file
@@ -0,0 +1,9 @@
|
||||
{% extends "layout.html" %}
|
||||
|
||||
{% block content %}
|
||||
<section class="about">
|
||||
<h1>About Us</h1>
|
||||
<p>This is a simple Flask web application created as a demonstration.</p>
|
||||
<p>Learn more about our team and mission here.</p>
|
||||
</section>
|
||||
{% endblock %}
|
||||
60
templates/config.html
Normal file
60
templates/config.html
Normal file
@@ -0,0 +1,60 @@
|
||||
{% extends "layout.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="config-container">
|
||||
<h1>Work Configuration</h1>
|
||||
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
<div class="alert alert-{{ category }}">{{ message }}</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<form method="POST" action="{{ url_for('config') }}" class="config-form">
|
||||
<div class="form-group">
|
||||
<label for="work_hours_per_day">Work Hours Per Day:</label>
|
||||
<input type="number" id="work_hours_per_day" name="work_hours_per_day"
|
||||
value="{{ config.work_hours_per_day }}" step="0.5" min="0.5" max="24" required>
|
||||
<small>Standard number of work hours in a day</small>
|
||||
</div>
|
||||
|
||||
<div class="form-section">
|
||||
<h3>Primary Break</h3>
|
||||
<div class="form-group">
|
||||
<label for="mandatory_break_minutes">Mandatory Break Duration (minutes):</label>
|
||||
<input type="number" id="mandatory_break_minutes" name="mandatory_break_minutes"
|
||||
value="{{ config.mandatory_break_minutes }}" step="1" min="0" max="240" required>
|
||||
<small>Required break time in minutes</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="break_threshold_hours">Break Threshold (hours):</label>
|
||||
<input type="number" id="break_threshold_hours" name="break_threshold_hours"
|
||||
value="{{ config.break_threshold_hours }}" step="0.5" min="0" max="24" required>
|
||||
<small>Work hours after which a break becomes mandatory</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-section">
|
||||
<h3>Additional Break</h3>
|
||||
<div class="form-group">
|
||||
<label for="additional_break_minutes">Additional Break Duration (minutes):</label>
|
||||
<input type="number" id="additional_break_minutes" name="additional_break_minutes"
|
||||
value="{{ config.additional_break_minutes }}" step="1" min="0" max="240" required>
|
||||
<small>Duration of additional break in minutes</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="additional_break_threshold_hours">Additional Break Threshold (hours):</label>
|
||||
<input type="number" id="additional_break_threshold_hours" name="additional_break_threshold_hours"
|
||||
value="{{ config.additional_break_threshold_hours }}" step="0.5" min="0" max="24" required>
|
||||
<small>Work hours after which an additional break becomes necessary</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary">Save Configuration</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
22
templates/contact.html
Normal file
22
templates/contact.html
Normal file
@@ -0,0 +1,22 @@
|
||||
{% extends "layout.html" %}
|
||||
|
||||
{% block content %}
|
||||
<section class="contact">
|
||||
<h1>Contact Us</h1>
|
||||
<form method="POST" action="{{ url_for('contact') }}">
|
||||
<div class="form-group">
|
||||
<label for="name">Name:</label>
|
||||
<input type="text" id="name" name="name" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="email">Email:</label>
|
||||
<input type="email" id="email" name="email" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="message">Message:</label>
|
||||
<textarea id="message" name="message" rows="5" required></textarea>
|
||||
</div>
|
||||
<button type="submit">Send Message</button>
|
||||
</form>
|
||||
</section>
|
||||
{% endblock %}
|
||||
94
templates/index.html
Normal file
94
templates/index.html
Normal file
@@ -0,0 +1,94 @@
|
||||
{% extends "layout.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="hero">
|
||||
<h1>Welcome to TimeTrack</h1>
|
||||
<p>Track your work hours easily and efficiently</p>
|
||||
</div>
|
||||
|
||||
<div class="timetrack-container">
|
||||
<h2>Time Tracking</h2>
|
||||
|
||||
<div class="timer-section">
|
||||
{% if active_entry %}
|
||||
<div class="active-timer">
|
||||
<h3>Currently Working</h3>
|
||||
<p>Started at: {{ active_entry.arrival_time.strftime('%Y-%m-%d %H:%M:%S') }}</p>
|
||||
<div id="timer"
|
||||
data-start="{{ active_entry.arrival_time.timestamp() }}"
|
||||
data-breaks="{{ active_entry.total_break_duration }}"
|
||||
data-paused="{{ 'true' if active_entry.is_paused else 'false' }}">00:00:00</div>
|
||||
|
||||
{% if active_entry.is_paused %}
|
||||
<p class="break-info">On break since {{ active_entry.pause_start_time.strftime('%H:%M:%S') }}</p>
|
||||
{% endif %}
|
||||
|
||||
{% if active_entry.total_break_duration > 0 %}
|
||||
<p class="break-total">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) }}</p>
|
||||
{% endif %}
|
||||
|
||||
<div class="button-group">
|
||||
<button id="pause-btn" class="{% if active_entry.is_paused %}resume-btn{% else %}pause-btn{% endif %}" data-id="{{ active_entry.id }}">
|
||||
{% if active_entry.is_paused %}Resume work{% else %}Pause{% endif %}
|
||||
</button>
|
||||
<button id="leave-btn" class="leave-btn" data-id="{{ active_entry.id }}">Leave</button>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="inactive-timer">
|
||||
<h3>Not Currently Working</h3>
|
||||
<button id="arrive-btn" class="arrive-btn">Arrive</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="history-section">
|
||||
<h3>Time Entry History</h3>
|
||||
{% if history %}
|
||||
<table class="time-history">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Arrival</th>
|
||||
<th>Departure</th>
|
||||
<th>Work Duration</th>
|
||||
<th>Break Duration</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for entry in history %}
|
||||
<tr>
|
||||
<td>{{ entry.arrival_time.strftime('%Y-%m-%d') }}</td>
|
||||
<td>{{ entry.arrival_time.strftime('%H:%M:%S') }}</td>
|
||||
<td>{{ entry.departure_time.strftime('%H:%M:%S') }}</td>
|
||||
<td>{{ '%d:%02d:%02d'|format(entry.duration//3600, (entry.duration%3600)//60, entry.duration%60) }}</td>
|
||||
<td>{{ '%d:%02d:%02d'|format(entry.total_break_duration//3600, (entry.total_break_duration%3600)//60, entry.total_break_duration%60) }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p>No time entries recorded yet.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="features">
|
||||
<div class="feature">
|
||||
<h3>Easy Time Tracking</h3>
|
||||
<p>Simply click "Arrive" when you start working and "Leave" when you're done.</p>
|
||||
</div>
|
||||
<div class="feature">
|
||||
<h3>Break Management</h3>
|
||||
<p>Use the Pause button when taking breaks. Your break time is tracked separately.</p>
|
||||
</div>
|
||||
<div class="feature">
|
||||
<h3>Detailed History</h3>
|
||||
<p>View your complete work history with precise timestamps and durations.</p>
|
||||
</div>
|
||||
<div class="feature">
|
||||
<h3>Simple Interface</h3>
|
||||
<p>No complicated setup or configuration needed. Start tracking right away!</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
32
templates/layout.html
Normal file
32
templates/layout.html
Normal file
@@ -0,0 +1,32 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>TimeTrack - {{ title }}</title>
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<nav>
|
||||
<ul>
|
||||
<li><a href="{{ url_for('home') }}">Home</a></li>
|
||||
<li><a href="{{ url_for('about') }}">About</a></li>
|
||||
<li><a href="{{ url_for('contact') }}">Contact</a></li>
|
||||
<!-- Add this to your navigation menu -->
|
||||
<li><a href="{{ url_for('config') }}" {% if title == 'Configuration' %}class="active"{% endif %}>Configuration</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<p>© 2023 TimeTrack</p>
|
||||
</footer>
|
||||
|
||||
<script src="{{ url_for('static', filename='js/script.js') }}"></script>
|
||||
</body>
|
||||
</html>
|
||||
51
templates/timetrack.html
Normal file
51
templates/timetrack.html
Normal file
@@ -0,0 +1,51 @@
|
||||
{% extends "layout.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="timetrack-container">
|
||||
<h1>Time Tracking</h1>
|
||||
|
||||
<div class="timer-section">
|
||||
{% if active_entry %}
|
||||
<div class="active-timer">
|
||||
<h2>Currently Working</h2>
|
||||
<p>Started at: {{ active_entry.arrival_time.strftime('%Y-%m-%d %H:%M:%S') }}</p>
|
||||
<div id="timer" data-start="{{ active_entry.arrival_time.timestamp() }}">00:00:00</div>
|
||||
<button id="leave-btn" class="leave-btn" data-id="{{ active_entry.id }}">Leave</button>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="inactive-timer">
|
||||
<h2>Not Currently Working</h2>
|
||||
<button id="arrive-btn" class="arrive-btn">Arrive</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="history-section">
|
||||
<h2>Time Entry History</h2>
|
||||
{% if history %}
|
||||
<table class="time-history">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Arrival</th>
|
||||
<th>Departure</th>
|
||||
<th>Duration</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for entry in history %}
|
||||
<tr>
|
||||
<td>{{ entry.arrival_time.strftime('%Y-%m-%d') }}</td>
|
||||
<td>{{ entry.arrival_time.strftime('%H:%M:%S') }}</td>
|
||||
<td>{{ entry.departure_time.strftime('%H:%M:%S') }}</td>
|
||||
<td>{{ '%d:%02d:%02d'|format(entry.duration//3600, (entry.duration%3600)//60, entry.duration%60) }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p>No time entries recorded yet.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user