Initial user management.

This commit is contained in:
Jens Luedicke
2025-06-28 09:33:39 +02:00
parent dc4229e468
commit 452f3abd80
13 changed files with 766 additions and 37 deletions

337
app.py
View File

@@ -1,54 +1,310 @@
from flask import Flask, render_template, request, redirect, url_for, jsonify, flash from flask import Flask, render_template, request, redirect, url_for, jsonify, flash, session, g
from models import db, TimeEntry, WorkConfig from models import db, TimeEntry, WorkConfig, User
from datetime import datetime, time import logging
from datetime import datetime, time, timedelta
import os import os
from sqlalchemy import func from sqlalchemy import func
from functools import wraps
# Configure logging
logging.basicConfig(level=logging.DEBUG)
app = Flask(__name__) app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///timetrack.db' app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///timetrack.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False 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 app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'dev_key_for_timetrack')
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=7) # Session lasts for 7 days
# Initialize the database with the app # Initialize the database with the app
db.init_app(app) db.init_app(app)
# Authentication decorator
def login_required(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if 'user_id' not in session:
flash('Please log in to access this page', 'error')
return redirect(url_for('login', next=request.url))
return f(*args, **kwargs)
return decorated_function
# Admin-only decorator
def admin_required(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if 'user_id' not in session:
flash('Please log in to access this page', 'error')
return redirect(url_for('login', next=request.url))
user = User.query.get(session['user_id'])
if not user or not user.is_admin:
flash('You need administrator privileges to access this page', 'error')
return redirect(url_for('home'))
return f(*args, **kwargs)
return decorated_function
@app.before_request
def load_logged_in_user():
user_id = session.get('user_id')
if user_id is None:
g.user = None
else:
g.user = User.query.get(user_id)
@app.route('/') @app.route('/')
def home(): def home():
# Get active time entry (if any) if g.user:
active_entry = TimeEntry.query.filter_by(departure_time=None).first() # Get active time entry (if any) for the current user
active_entry = TimeEntry.query.filter_by(user_id=g.user.id, departure_time=None).first()
# Get today's date # Get today's date
today = datetime.now().date() today = datetime.now().date()
# Get time entries for today only # Get time entries for today only for the current user
today_start = datetime.combine(today, time.min) today_start = datetime.combine(today, time.min)
today_end = datetime.combine(today, time.max) today_end = datetime.combine(today, time.max)
today_entries = TimeEntry.query.filter( today_entries = TimeEntry.query.filter(
TimeEntry.arrival_time >= today_start, TimeEntry.user_id == g.user.id,
TimeEntry.arrival_time <= today_end TimeEntry.arrival_time >= today_start,
).order_by(TimeEntry.arrival_time.desc()).all() 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) return render_template('index.html', title='Home', active_entry=active_entry, history=today_entries)
else:
# Show landing page for non-logged in users
return render_template('index.html', title='Home')
@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
username = request.form.get('username')
password = request.form.get('password')
remember = 'remember' in request.form
user = User.query.filter_by(username=username).first()
if user and user.check_password(password):
session.clear()
session['user_id'] = user.id
if remember:
session.permanent = True
next_page = request.args.get('next')
if not next_page or not next_page.startswith('/'):
next_page = url_for('home')
flash(f'Welcome back, {user.username}!', 'success')
return redirect(next_page)
flash('Invalid username or password', 'error')
return render_template('login.html', title='Login')
@app.route('/logout')
def logout():
session.clear()
flash('You have been logged out', 'info')
return redirect(url_for('login'))
@app.route('/register', methods=['GET', 'POST'])
def register():
if request.method == 'POST':
username = request.form.get('username')
email = request.form.get('email')
password = request.form.get('password')
confirm_password = request.form.get('confirm_password')
# Validate input
error = None
if not username:
error = 'Username is required'
elif not email:
error = 'Email is required'
elif not password:
error = 'Password is required'
elif password != confirm_password:
error = 'Passwords do not match'
elif User.query.filter_by(username=username).first():
error = 'Username already exists'
elif User.query.filter_by(email=email).first():
error = 'Email already registered'
if error is None:
new_user = User(username=username, email=email)
new_user.set_password(password)
db.session.add(new_user)
db.session.commit()
flash('Registration successful! You can now log in.', 'success')
return redirect(url_for('login'))
flash(error, 'error')
return render_template('register.html', title='Register')
@app.route('/admin/users')
@admin_required
def admin_users():
users = User.query.all()
return render_template('admin_users.html', title='User Management', users=users)
@app.route('/admin/users/create', methods=['GET', 'POST'])
@admin_required
def create_user():
if request.method == 'POST':
username = request.form.get('username')
email = request.form.get('email')
password = request.form.get('password')
is_admin = 'is_admin' in request.form
# Validate input
error = None
if not username:
error = 'Username is required'
elif not email:
error = 'Email is required'
elif not password:
error = 'Password is required'
elif User.query.filter_by(username=username).first():
error = 'Username already exists'
elif User.query.filter_by(email=email).first():
error = 'Email already registered'
if error is None:
new_user = User(username=username, email=email, is_admin=is_admin)
new_user.set_password(password)
db.session.add(new_user)
db.session.commit()
flash(f'User {username} created successfully!', 'success')
return redirect(url_for('admin_users'))
flash(error, 'error')
return render_template('create_user.html', title='Create User')
@app.route('/admin/users/edit/<int:user_id>', methods=['GET', 'POST'])
@admin_required
def edit_user(user_id):
user = User.query.get_or_404(user_id)
if request.method == 'POST':
username = request.form.get('username')
email = request.form.get('email')
password = request.form.get('password')
is_admin = 'is_admin' in request.form
# Validate input
error = None
if not username:
error = 'Username is required'
elif not email:
error = 'Email is required'
elif username != user.username and User.query.filter_by(username=username).first():
error = 'Username already exists'
elif email != user.email and User.query.filter_by(email=email).first():
error = 'Email already registered'
if error is None:
user.username = username
user.email = email
user.is_admin = is_admin
if password:
user.set_password(password)
db.session.commit()
flash(f'User {username} updated successfully!', 'success')
return redirect(url_for('admin_users'))
flash(error, 'error')
return render_template('edit_user.html', title='Edit User', user=user)
@app.route('/admin/users/delete/<int:user_id>', methods=['POST'])
@admin_required
def delete_user(user_id):
user = User.query.get_or_404(user_id)
# Prevent deleting yourself
if user.id == session.get('user_id'):
flash('You cannot delete your own account', 'error')
return redirect(url_for('admin_users'))
username = user.username
db.session.delete(user)
db.session.commit()
flash(f'User {username} deleted successfully', 'success')
return redirect(url_for('admin_users'))
@app.route('/profile', methods=['GET', 'POST'])
@login_required
def profile():
user = User.query.get(session['user_id'])
if request.method == 'POST':
email = request.form.get('email')
current_password = request.form.get('current_password')
new_password = request.form.get('new_password')
confirm_password = request.form.get('confirm_password')
# Validate input
error = None
if not email:
error = 'Email is required'
elif email != user.email and User.query.filter_by(email=email).first():
error = 'Email already registered'
# Password change validation
if new_password:
if not current_password:
error = 'Current password is required to set a new password'
elif not user.check_password(current_password):
error = 'Current password is incorrect'
elif new_password != confirm_password:
error = 'New passwords do not match'
if error is None:
user.email = email
if new_password:
user.set_password(new_password)
db.session.commit()
flash('Profile updated successfully!', 'success')
return redirect(url_for('profile'))
flash(error, 'error')
return render_template('profile.html', title='My Profile', user=user)
@app.route('/about') @app.route('/about')
@login_required
def about(): def about():
return render_template('about.html', title='About') return render_template('about.html', title='About')
@app.route('/contact', methods=['GET', 'POST']) @app.route('/contact', methods=['GET', 'POST'])
@login_required
def contact(): def contact():
# redacted # redacted
return render_template('contact.html', title='Contact') return render_template('contact.html', title='Contact')
# We can keep this route as a redirect to home for backward compatibility # We can keep this route as a redirect to home for backward compatibility
@app.route('/timetrack') @app.route('/timetrack')
@login_required
def timetrack(): def timetrack():
return redirect(url_for('home')) return redirect(url_for('home'))
@app.route('/api/arrive', methods=['POST']) @app.route('/api/arrive', methods=['POST'])
@login_required
def arrive(): def arrive():
# Create a new time entry with arrival time # Create a new time entry with arrival time for the current user
new_entry = TimeEntry(arrival_time=datetime.now()) new_entry = TimeEntry(user_id=session['user_id'], arrival_time=datetime.now())
db.session.add(new_entry) db.session.add(new_entry)
db.session.commit() db.session.commit()
@@ -58,9 +314,10 @@ def arrive():
}) })
@app.route('/api/leave/<int:entry_id>', methods=['POST']) @app.route('/api/leave/<int:entry_id>', methods=['POST'])
@login_required
def leave(entry_id): def leave(entry_id):
# Find the time entry # Find the time entry for the current user
entry = TimeEntry.query.get_or_404(entry_id) entry = TimeEntry.query.filter_by(id=entry_id, user_id=session['user_id']).first_or_404()
# Set the departure time # Set the departure time
departure_time = datetime.now() departure_time = datetime.now()
@@ -93,9 +350,10 @@ def leave(entry_id):
# Add this new route to handle pausing/resuming # Add this new route to handle pausing/resuming
@app.route('/api/toggle-pause/<int:entry_id>', methods=['POST']) @app.route('/api/toggle-pause/<int:entry_id>', methods=['POST'])
@login_required
def toggle_pause(entry_id): def toggle_pause(entry_id):
# Find the time entry # Find the time entry for the current user
entry = TimeEntry.query.get_or_404(entry_id) entry = TimeEntry.query.filter_by(id=entry_id, user_id=session['user_id']).first_or_404()
now = datetime.now() now = datetime.now()
@@ -124,6 +382,7 @@ def toggle_pause(entry_id):
}) })
@app.route('/config', methods=['GET', 'POST']) @app.route('/config', methods=['GET', 'POST'])
@login_required
def config(): def config():
# Get current configuration or create default if none exists # Get current configuration or create default if none exists
config = WorkConfig.query.order_by(WorkConfig.id.desc()).first() config = WorkConfig.query.order_by(WorkConfig.id.desc()).first()
@@ -164,15 +423,17 @@ def create_tables():
print("WARNING: Database schema is outdated. Please run migrate_db.py to update it.") print("WARNING: Database schema is outdated. Please run migrate_db.py to update it.")
@app.route('/api/delete/<int:entry_id>', methods=['DELETE']) @app.route('/api/delete/<int:entry_id>', methods=['DELETE'])
@login_required
def delete_entry(entry_id): def delete_entry(entry_id):
entry = TimeEntry.query.get_or_404(entry_id) entry = TimeEntry.query.filter_by(id=entry_id, user_id=session['user_id']).first_or_404()
db.session.delete(entry) db.session.delete(entry)
db.session.commit() db.session.commit()
return jsonify({'success': True, 'message': 'Entry deleted successfully'}) return jsonify({'success': True, 'message': 'Entry deleted successfully'})
@app.route('/api/update/<int:entry_id>', methods=['PUT']) @app.route('/api/update/<int:entry_id>', methods=['PUT'])
@login_required
def update_entry(entry_id): def update_entry(entry_id):
entry = TimeEntry.query.get_or_404(entry_id) entry = TimeEntry.query.filter_by(id=entry_id, user_id=session['user_id']).first_or_404()
data = request.json data = request.json
if 'arrival_time' in data: if 'arrival_time' in data:
@@ -210,9 +471,10 @@ def update_entry(entry_id):
}) })
@app.route('/history') @app.route('/history')
@login_required
def history(): def history():
# Get all time entries, ordered by most recent first # Get all time entries for the current user, ordered by most recent first
all_entries = TimeEntry.query.order_by(TimeEntry.arrival_time.desc()).all() all_entries = TimeEntry.query.filter_by(user_id=session['user_id']).order_by(TimeEntry.arrival_time.desc()).all()
return render_template('history.html', title='Time Entry History', entries=all_entries) return render_template('history.html', title='Time Entry History', entries=all_entries)
@@ -263,12 +525,13 @@ def calculate_work_duration(arrival_time, departure_time, total_break_duration):
return work_duration, effective_break_duration return work_duration, effective_break_duration
@app.route('/api/resume/<int:entry_id>', methods=['POST']) @app.route('/api/resume/<int:entry_id>', methods=['POST'])
@login_required
def resume_entry(entry_id): def resume_entry(entry_id):
# Find the entry to resume # Find the entry to resume for the current user
entry_to_resume = TimeEntry.query.get_or_404(entry_id) entry_to_resume = TimeEntry.query.filter_by(id=entry_id, user_id=session['user_id']).first_or_404()
# Check if there's already an active entry # Check if there's already an active entry
active_entry = TimeEntry.query.filter_by(departure_time=None).first() active_entry = TimeEntry.query.filter_by(user_id=session['user_id'], departure_time=None).first()
if active_entry: if active_entry:
return jsonify({ return jsonify({
'success': False, 'success': False,
@@ -292,5 +555,21 @@ def resume_entry(entry_id):
'total_break_duration': entry_to_resume.total_break_duration 'total_break_duration': entry_to_resume.total_break_duration
}) })
@app.errorhandler(404)
def page_not_found(e):
return render_template('404.html'), 404
@app.errorhandler(500)
def internal_server_error(e):
return render_template('500.html'), 500
@app.route('/test')
def test():
return "App is working!"
@app.context_processor
def inject_current_year():
return {'current_year': datetime.now().year}
if __name__ == '__main__': if __name__ == '__main__':
app.run(debug=True) app.run(debug=True)

View File

@@ -1,6 +1,9 @@
from app import app, db from app import app, db
import sqlite3 import sqlite3
import os import os
from models import User, TimeEntry, WorkConfig
from werkzeug.security import generate_password_hash
from datetime import datetime
def migrate_database(): def migrate_database():
db_path = 'timetrack.db' db_path = 'timetrack.db'
@@ -35,6 +38,11 @@ def migrate_database():
print("Adding total_break_duration column to time_entry...") print("Adding total_break_duration column to time_entry...")
cursor.execute("ALTER TABLE time_entry ADD COLUMN total_break_duration INTEGER DEFAULT 0") cursor.execute("ALTER TABLE time_entry ADD COLUMN total_break_duration INTEGER DEFAULT 0")
# Add user_id column if it doesn't exist
if 'user_id' not in time_entry_columns:
print("Adding user_id column to time_entry...")
cursor.execute("ALTER TABLE time_entry ADD COLUMN user_id INTEGER")
# Check if the work_config table exists # Check if the work_config table exists
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='work_config'") cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='work_config'")
if not cursor.fetchone(): if not cursor.fetchone():
@@ -46,7 +54,8 @@ def migrate_database():
mandatory_break_minutes INTEGER DEFAULT 30, mandatory_break_minutes INTEGER DEFAULT 30,
break_threshold_hours FLOAT DEFAULT 6.0, break_threshold_hours FLOAT DEFAULT 6.0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
user_id INTEGER
) )
""") """)
# Insert default config # Insert default config
@@ -68,11 +77,48 @@ def migrate_database():
print("Adding additional_break_threshold_hours column to work_config...") 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") cursor.execute("ALTER TABLE work_config ADD COLUMN additional_break_threshold_hours FLOAT DEFAULT 9.0")
# Add user_id column to work_config if it doesn't exist
if 'user_id' not in work_config_columns:
print("Adding user_id column to work_config...")
cursor.execute("ALTER TABLE work_config ADD COLUMN user_id INTEGER")
# Commit changes and close connection # Commit changes and close connection
conn.commit() conn.commit()
conn.close() conn.close()
print("Database migration completed successfully!") with app.app_context():
# Create tables if they don't exist
db.create_all()
# Check if admin user exists
admin = User.query.filter_by(username='admin').first()
if not admin:
# Create admin user
admin = User(
username='admin',
email='admin@timetrack.local',
is_admin=True
)
admin.set_password('admin') # Default password, should be changed
db.session.add(admin)
db.session.commit()
print("Created admin user with username 'admin' and password 'admin'")
print("Please change the admin password after first login!")
# Update existing time entries to associate with admin user
orphan_entries = TimeEntry.query.filter_by(user_id=None).all()
for entry in orphan_entries:
entry.user_id = admin.id
# Update existing work configs to associate with admin user
orphan_configs = WorkConfig.query.filter_by(user_id=None).all()
for config in orphan_configs:
config.user_id = admin.id
db.session.commit()
print(f"Associated {len(orphan_entries)} existing time entries with admin user")
print(f"Associated {len(orphan_configs)} existing work configs with admin user")
if __name__ == "__main__": if __name__ == "__main__":
migrate_database() migrate_database()
print("Database migration completed")

View File

@@ -1,8 +1,29 @@
from flask_sqlalchemy import SQLAlchemy from flask_sqlalchemy import SQLAlchemy
from werkzeug.security import generate_password_hash, check_password_hash
from datetime import datetime from datetime import datetime
db = SQLAlchemy() db = SQLAlchemy()
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(64), unique=True, nullable=False)
email = db.Column(db.String(120), unique=True, nullable=False)
password_hash = db.Column(db.String(128))
is_admin = db.Column(db.Boolean, default=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
# Relationship with TimeEntry
time_entries = db.relationship('TimeEntry', backref='user', lazy='dynamic')
def set_password(self, password):
self.password_hash = generate_password_hash(password)
def check_password(self, password):
return check_password_hash(self.password_hash, password)
def __repr__(self):
return f'<User {self.username}>'
class TimeEntry(db.Model): class TimeEntry(db.Model):
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
arrival_time = db.Column(db.DateTime, nullable=False) arrival_time = db.Column(db.DateTime, nullable=False)
@@ -11,6 +32,7 @@ class TimeEntry(db.Model):
is_paused = db.Column(db.Boolean, default=False) is_paused = db.Column(db.Boolean, default=False)
pause_start_time = db.Column(db.DateTime, nullable=True) pause_start_time = db.Column(db.DateTime, nullable=True)
total_break_duration = db.Column(db.Integer, default=0) # Total break duration in seconds total_break_duration = db.Column(db.Integer, default=0) # Total break duration in seconds
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=True)
def __repr__(self): def __repr__(self):
return f'<TimeEntry {self.id}: {self.arrival_time} - {self.departure_time}>' return f'<TimeEntry {self.id}: {self.arrival_time} - {self.departure_time}>'
@@ -24,6 +46,7 @@ class WorkConfig(db.Model):
additional_break_threshold_hours = db.Column(db.Float, default=9.0) # Work hours that trigger 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) created_at = db.Column(db.DateTime, default=datetime.now)
updated_at = db.Column(db.DateTime, default=datetime.now, onupdate=datetime.now) updated_at = db.Column(db.DateTime, default=datetime.now, onupdate=datetime.now)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=True)
def __repr__(self): def __repr__(self):
return f'<WorkConfig {self.id}: {self.work_hours_per_day}h/day, {self.mandatory_break_minutes}min break>' return f'<WorkConfig {self.id}: {self.work_hours_per_day}h/day, {self.mandatory_break_minutes}min break>'

View File

@@ -132,6 +132,37 @@ document.addEventListener('DOMContentLoaded', function() {
}); });
}); });
} }
// Add dropdown menu functionality
const dropdownToggles = document.querySelectorAll('.dropdown-toggle');
dropdownToggles.forEach(toggle => {
toggle.addEventListener('click', function(e) {
e.preventDefault();
const parent = this.parentElement;
const menu = parent.querySelector('.dropdown-menu');
// Close all other open dropdowns
document.querySelectorAll('.dropdown-menu').forEach(item => {
if (item !== menu && item.classList.contains('show')) {
item.classList.remove('show');
}
});
// Toggle current dropdown
menu.classList.toggle('show');
});
});
// Close dropdown when clicking outside
document.addEventListener('click', function(e) {
if (!e.target.matches('.dropdown-toggle')) {
const dropdowns = document.querySelectorAll('.dropdown-menu.show');
dropdowns.forEach(dropdown => {
dropdown.classList.remove('show');
});
}
});
}); });
// Add event listener for resume work buttons // Add event listener for resume work buttons

9
templates/404.html Normal file
View File

@@ -0,0 +1,9 @@
{% extends "layout.html" %}
{% block content %}
<div class="error-container">
<h1>404 - Page Not Found</h1>
<p>The page you are looking for does not exist.</p>
<a href="{{ url_for('home') }}" class="btn">Return to Home</a>
</div>
{% endblock %}

0
templates/500.html Normal file
View File

View File

@@ -0,0 +1,88 @@
{% extends "layout.html" %}
{% block content %}
<div class="admin-container">
<h1>User Management</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 %}
<div class="admin-actions">
<a href="{{ url_for('create_user') }}" class="btn btn-success">Create New User</a>
</div>
<div class="user-list">
<table class="table">
<thead>
<tr>
<th>Username</th>
<th>Email</th>
<th>Role</th>
<th>Created</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for user in users %}
<tr>
<td>{{ user.username }}</td>
<td>{{ user.email }}</td>
<td>{% if user.is_admin %}Admin{% else %}User{% endif %}</td>
<td>{{ user.created_at.strftime('%Y-%m-%d') }}</td>
<td>
<a href="{{ url_for('edit_user', user_id=user.id) }}" class="btn btn-sm btn-primary">Edit</a>
{% if user.id != g.user.id %}
<button class="btn btn-sm btn-danger" onclick="confirmDelete({{ user.id }}, '{{ user.username }}')">Delete</button>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- Delete Confirmation Modal -->
<div id="delete-modal" class="modal">
<div class="modal-content">
<span class="close">&times;</span>
<h2>Confirm Deletion</h2>
<p>Are you sure you want to delete user <span id="delete-username"></span>?</p>
<p>This action cannot be undone.</p>
<form id="delete-form" method="POST">
<button type="button" id="cancel-delete" class="btn btn-secondary">Cancel</button>
<button type="submit" class="btn btn-danger">Delete</button>
</form>
</div>
</div>
<script>
function confirmDelete(userId, username) {
document.getElementById('delete-username').textContent = username;
document.getElementById('delete-form').action = "{{ url_for('delete_user', user_id=0) }}".replace('0', userId);
document.getElementById('delete-modal').style.display = 'block';
}
// Close modal when clicking the X
document.querySelector('.close').addEventListener('click', function() {
document.getElementById('delete-modal').style.display = 'none';
});
// Close modal when clicking Cancel
document.getElementById('cancel-delete').addEventListener('click', function() {
document.getElementById('delete-modal').style.display = 'none';
});
// Close modal when clicking outside
window.addEventListener('click', function(event) {
if (event.target == document.getElementById('delete-modal')) {
document.getElementById('delete-modal').style.display = 'none';
}
});
</script>
</div>
{% endblock %}

View File

@@ -0,0 +1,44 @@
{% extends "layout.html" %}
{% block content %}
<div class="admin-container">
<h1>Create New User</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('create_user') }}" class="user-form">
<div class="form-group">
<label for="username">Username</label>
<input type="text" id="username" name="username" class="form-control" required autofocus>
</div>
<div class="form-group">
<label for="email">Email</label>
<input type="email" id="email" name="email" class="form-control" required>
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" id="password" name="password" class="form-control" required>
</div>
<div class="form-group">
<label class="checkbox-container">
<input type="checkbox" name="is_admin"> Administrator privileges
<span class="checkmark"></span>
</label>
</div>
<div class="form-group">
<button type="submit" class="btn btn-success">Create User</button>
<a href="{{ url_for('admin_users') }}" class="btn btn-secondary">Cancel</a>
</div>
</form>
</div>
{% endblock %}

44
templates/edit_user.html Normal file
View File

@@ -0,0 +1,44 @@
{% extends "layout.html" %}
{% block content %}
<div class="admin-container">
<h1>Edit User: {{ user.username }}</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('edit_user', user_id=user.id) }}" class="user-form">
<div class="form-group">
<label for="username">Username</label>
<input type="text" id="username" name="username" class="form-control" value="{{ user.username }}" required>
</div>
<div class="form-group">
<label for="email">Email</label>
<input type="email" id="email" name="email" class="form-control" value="{{ user.email }}" required>
</div>
<div class="form-group">
<label for="password">New Password (leave blank to keep current)</label>
<input type="password" id="password" name="password" class="form-control">
</div>
<div class="form-group">
<label class="checkbox-container">
<input type="checkbox" name="is_admin" {% if user.is_admin %}checked{% endif %}> Administrator privileges
<span class="checkmark"></span>
</label>
</div>
<div class="form-group">
<button type="submit" class="btn btn-primary">Update User</button>
<a href="{{ url_for('admin_users') }}" class="btn btn-secondary">Cancel</a>
</div>
</form>
</div>
{% endblock %}

View File

@@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TimeTrack - {{ title }}</title> <title>{{ title }} - TimeTrack</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
</head> </head>
<body> <body>
@@ -11,19 +11,48 @@
<nav> <nav>
<ul> <ul>
<li><a href="{{ url_for('home') }}">Home</a></li> <li><a href="{{ url_for('home') }}">Home</a></li>
<li><a href="{{ url_for('history') }}">Complete History</a></li> {% if g.user %}
<li><a href="{{ url_for('config') }}" {% if title == 'Configuration' %}class="active"{% endif %}>Configuration</a></li> <li><a href="{{ url_for('history') }}">History</a></li>
<li><a href="{{ url_for('about') }}">About</a></li> <li><a href="{{ url_for('config') }}">Config</a></li>
<li><a href="{{ url_for('about') }}">About</a></li>
<li><a href="{{ url_for('contact') }}">Contact</a></li>
<!-- Add Admin dropdown menu here -->
{% if g.user.is_admin %}
<li class="dropdown">
<a href="#" class="dropdown-toggle">Admin</a>
<ul class="dropdown-menu">
<li><a href="{{ url_for('admin_users') }}">User Management</a></li>
</ul>
</li>
{% endif %}
<li><a href="{{ url_for('profile') }}">Profile</a></li>
<li><a href="{{ url_for('logout') }}">Logout</a></li>
{% else %}
<li><a href="{{ url_for('login') }}">Login</a></li>
<li><a href="{{ url_for('register') }}">Register</a></li>
{% endif %}
</ul> </ul>
</nav> </nav>
</header> </header>
<main> <main>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
<div class="flash-messages">
{% for category, message in messages %}
<div class="alert alert-{{ category }}">{{ message }}</div>
{% endfor %}
</div>
{% endif %}
{% endwith %}
{% block content %}{% endblock %} {% block content %}{% endblock %}
</main> </main>
<footer> <footer>
<p>&copy; 2025 TimeTrack</p> <p>&copy; {{ current_year }} TimeTrack. All rights reserved.</p>
</footer> </footer>
<script src="{{ url_for('static', filename='js/script.js') }}"></script> <script src="{{ url_for('static', filename='js/script.js') }}"></script>

42
templates/login.html Normal file
View File

@@ -0,0 +1,42 @@
{% extends "layout.html" %}
{% block content %}
<div class="auth-container">
<h1>Login to TimeTrack</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('login') }}" class="auth-form">
<div class="form-group">
<label for="username">Username</label>
<input type="text" id="username" name="username" class="form-control" required autofocus>
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" id="password" name="password" class="form-control" required>
</div>
<div class="form-group">
<label class="checkbox-container">
<input type="checkbox" name="remember"> Remember me
<span class="checkmark"></span>
</label>
</div>
<div class="form-group">
<button type="submit" class="btn btn-primary">Login</button>
</div>
<div class="auth-links">
<p>Don't have an account? <a href="{{ url_for('register') }}">Register here</a></p>
</div>
</form>
</div>
{% endblock %}

49
templates/profile.html Normal file
View File

@@ -0,0 +1,49 @@
{% extends "layout.html" %}
{% block content %}
<div class="profile-container">
<h1>My Profile</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 %}
<div class="profile-info">
<p><strong>Username:</strong> {{ user.username }}</p>
<p><strong>Account Type:</strong> {% if user.is_admin %}Administrator{% else %}User{% endif %}</p>
<p><strong>Member Since:</strong> {{ user.created_at.strftime('%Y-%m-%d') }}</p>
</div>
<h2>Update Profile</h2>
<form method="POST" action="{{ url_for('profile') }}" class="profile-form">
<div class="form-group">
<label for="email">Email</label>
<input type="email" id="email" name="email" class="form-control" value="{{ user.email }}" required>
</div>
<h3>Change Password</h3>
<div class="form-group">
<label for="current_password">Current Password</label>
<input type="password" id="current_password" name="current_password" class="form-control">
</div>
<div class="form-group">
<label for="new_password">New Password</label>
<input type="password" id="new_password" name="new_password" class="form-control">
</div>
<div class="form-group">
<label for="confirm_password">Confirm New Password</label>
<input type="password" id="confirm_password" name="confirm_password" class="form-control">
</div>
<div class="form-group">
<button type="submit" class="btn btn-primary">Update Profile</button>
</div>
</form>
</div>
{% endblock %}

45
templates/register.html Normal file
View File

@@ -0,0 +1,45 @@
{% extends "layout.html" %}
{% block content %}
<div class="auth-container">
<h1>Register for TimeTrack</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('register') }}" class="auth-form">
<div class="form-group">
<label for="username">Username</label>
<input type="text" id="username" name="username" class="form-control" required autofocus>
</div>
<div class="form-group">
<label for="email">Email</label>
<input type="email" id="email" name="email" class="form-control" required>
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" id="password" name="password" class="form-control" required>
</div>
<div class="form-group">
<label for="confirm_password">Confirm Password</label>
<input type="password" id="confirm_password" name="confirm_password" class="form-control" required>
</div>
<div class="form-group">
<button type="submit" class="btn btn-primary">Register</button>
</div>
<div class="auth-links">
<p>Already have an account? <a href="{{ url_for('login') }}">Login here</a></p>
</div>
</form>
</div>
{% endblock %}