Improve logging features (edit and delete).

This commit is contained in:
2025-06-27 15:14:57 +02:00
committed by Jens Luedicke
parent a8d1f33874
commit 7f9783b2fc
8 changed files with 888 additions and 80 deletions

130
app.py
View File

@@ -1,7 +1,8 @@
from flask import Flask, render_template, request, redirect, url_for, jsonify, flash from flask import Flask, render_template, request, redirect, url_for, jsonify, flash
from models import db, TimeEntry, WorkConfig from models import db, TimeEntry, WorkConfig
from datetime import datetime from datetime import datetime, time
import os import os
from sqlalchemy import func
app = Flask(__name__) app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///timetrack.db' app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///timetrack.db'
@@ -13,13 +14,22 @@ db.init_app(app)
@app.route('/') @app.route('/')
def home(): def home():
# Get the latest time entry that doesn't have a departure time # Get active time entry (if any)
active_entry = TimeEntry.query.filter_by(departure_time=None).first() active_entry = TimeEntry.query.filter_by(departure_time=None).first()
# Get all completed time entries, ordered by most recent first # Get today's date
history = TimeEntry.query.filter(TimeEntry.departure_time.isnot(None)).order_by(TimeEntry.arrival_time.desc()).all() today = datetime.now().date()
return render_template('index.html', title='Home', active_entry=active_entry, history=history) # Get time entries for today only
today_start = datetime.combine(today, time.min)
today_end = datetime.combine(today, time.max)
today_entries = TimeEntry.query.filter(
TimeEntry.arrival_time >= today_start,
TimeEntry.arrival_time <= today_end
).order_by(TimeEntry.arrival_time.desc()).all()
return render_template('index.html', title='Home', active_entry=active_entry, history=today_entries)
@app.route('/about') @app.route('/about')
def about(): def about():
@@ -30,10 +40,6 @@ def contact():
# redacted # redacted
return render_template('contact.html', title='Contact') 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 # We can keep this route as a redirect to home for backward compatibility
@app.route('/timetrack') @app.route('/timetrack')
def timetrack(): def timetrack():
@@ -65,10 +71,14 @@ def leave(entry_id):
final_break_duration = int((departure_time - entry.pause_start_time).total_seconds()) final_break_duration = int((departure_time - entry.pause_start_time).total_seconds())
entry.total_break_duration += final_break_duration entry.total_break_duration += final_break_duration
entry.is_paused = False entry.is_paused = False
entry.pause_start_time = None
# Calculate duration in seconds (excluding breaks) # Calculate work duration considering breaks
raw_duration = (departure_time - entry.arrival_time).total_seconds() entry.duration, effective_break = calculate_work_duration(
entry.duration = int(raw_duration - entry.total_break_duration) entry.arrival_time,
departure_time,
entry.total_break_duration
)
db.session.commit() db.session.commit()
@@ -77,7 +87,8 @@ def leave(entry_id):
'arrival_time': entry.arrival_time.strftime('%Y-%m-%d %H:%M:%S'), 'arrival_time': entry.arrival_time.strftime('%Y-%m-%d %H:%M:%S'),
'departure_time': entry.departure_time.strftime('%Y-%m-%d %H:%M:%S'), 'departure_time': entry.departure_time.strftime('%Y-%m-%d %H:%M:%S'),
'duration': entry.duration, 'duration': entry.duration,
'total_break_duration': entry.total_break_duration 'total_break_duration': entry.total_break_duration,
'effective_break_duration': effective_break
}) })
# Add this new route to handle pausing/resuming # Add this new route to handle pausing/resuming
@@ -152,5 +163,98 @@ def create_tables():
if 'is_paused' not in columns or 'pause_start_time' not in columns or 'total_break_duration' not in columns: 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.") print("WARNING: Database schema is outdated. Please run migrate_db.py to update it.")
@app.route('/api/delete/<int:entry_id>', methods=['DELETE'])
def delete_entry(entry_id):
entry = TimeEntry.query.get_or_404(entry_id)
db.session.delete(entry)
db.session.commit()
return jsonify({'success': True, 'message': 'Entry deleted successfully'})
@app.route('/api/update/<int:entry_id>', methods=['PUT'])
def update_entry(entry_id):
entry = TimeEntry.query.get_or_404(entry_id)
data = request.json
if 'arrival_time' in data:
try:
entry.arrival_time = datetime.strptime(data['arrival_time'], '%Y-%m-%d %H:%M:%S')
except ValueError:
return jsonify({'success': False, 'message': 'Invalid arrival time format'}), 400
if 'departure_time' in data and data['departure_time']:
try:
entry.departure_time = datetime.strptime(data['departure_time'], '%Y-%m-%d %H:%M:%S')
# Recalculate duration if both times are present
if entry.arrival_time and entry.departure_time:
# Calculate work duration considering breaks
entry.duration, _ = calculate_work_duration(
entry.arrival_time,
entry.departure_time,
entry.total_break_duration
)
except ValueError:
return jsonify({'success': False, 'message': 'Invalid departure time format'}), 400
db.session.commit()
return jsonify({
'success': True,
'message': 'Entry updated successfully',
'entry': {
'id': entry.id,
'arrival_time': entry.arrival_time.strftime('%Y-%m-%d %H:%M:%S'),
'departure_time': entry.departure_time.strftime('%Y-%m-%d %H:%M:%S') if entry.departure_time else None,
'duration': entry.duration,
'is_paused': entry.is_paused,
'total_break_duration': entry.total_break_duration
}
})
@app.route('/history')
def history():
# Get all time entries, ordered by most recent first
all_entries = TimeEntry.query.order_by(TimeEntry.arrival_time.desc()).all()
return render_template('history.html', title='Time Entry History', entries=all_entries)
def calculate_work_duration(arrival_time, departure_time, total_break_duration):
"""
Calculate work duration considering both configured and actual break times.
Args:
arrival_time: Datetime of arrival
departure_time: Datetime of departure
total_break_duration: Actual logged break duration in seconds
Returns:
tuple: (work_duration_in_seconds, effective_break_duration_in_seconds)
"""
# Calculate raw duration
raw_duration = (departure_time - arrival_time).total_seconds()
# Get work configuration for break rules
config = WorkConfig.query.order_by(WorkConfig.id.desc()).first()
if not config:
config = WorkConfig() # Use default values if no config exists
# Calculate mandatory breaks based on work duration
work_hours = raw_duration / 3600 # Convert seconds to hours
configured_break_seconds = 0
# Apply primary break if work duration exceeds threshold
if work_hours > config.break_threshold_hours:
configured_break_seconds += config.mandatory_break_minutes * 60
# Apply additional break if work duration exceeds additional threshold
if work_hours > config.additional_break_threshold_hours:
configured_break_seconds += config.additional_break_minutes * 60
# Use the greater of configured breaks or actual logged breaks
effective_break_duration = max(configured_break_seconds, total_break_duration)
# Calculate final work duration
work_duration = int(raw_duration - effective_break_duration)
return work_duration, effective_break_duration
if __name__ == '__main__': if __name__ == '__main__':
app.run(debug=True) app.run(debug=True)

View File

@@ -6,3 +6,4 @@ itsdangerous==2.0.1
click==8.0.1 click==8.0.1
Flask-SQLAlchemy==2.5.1 Flask-SQLAlchemy==2.5.1
SQLAlchemy==1.4.23 SQLAlchemy==1.4.23
python-dotenv==0.19.0

View File

@@ -69,39 +69,51 @@ main {
} }
.form-group { .form-group {
margin-bottom: 1rem; margin-bottom: 15px;
} }
.form-group label { .form-group label {
display: block; display: block;
margin-bottom: 0.5rem; margin-bottom: 5px;
font-weight: bold;
} }
.form-group input, .form-group input,
.form-group textarea { .form-group textarea {
width: 100%; width: 100%;
padding: 0.5rem; padding: 8px;
border: 1px solid #ddd; border: 1px solid #ddd;
border-radius: 3px; border-radius: 4px;
} }
button { button {
background-color: #4CAF50; background-color: #4CAF50;
color: white; color: white;
border: none; border: none;
padding: 0.5rem 1rem; padding: 8px 16px;
cursor: pointer; cursor: pointer;
border-radius: 3px; border-radius: 4px;
} }
.btn { .btn {
display: inline-block; padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
background-color: #4CAF50; background-color: #4CAF50;
color: white; color: white;
padding: 0.5rem 1rem; }
text-decoration: none;
border-radius: 3px; .btn:hover {
margin-top: 1rem; background-color: #45a049;
}
.btn-danger {
background-color: #f44336;
}
.btn-danger:hover {
background-color: #d32f2f;
} }
footer { footer {
@@ -278,7 +290,8 @@ footer {
.config-form small { .config-form small {
display: block; display: block;
color: #666; color: #666;
margin-top: 0.25rem; margin-top: 4px;
font-style: italic;
} }
.btn-primary { .btn-primary {
@@ -312,3 +325,92 @@ footer {
color: #721c24; color: #721c24;
border: 1px solid #f5c6cb; border: 1px solid #f5c6cb;
} }
/* Modal styles */
.modal {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.5);
}
.modal-content {
background-color: #fff;
margin: 10% auto;
padding: 20px;
border-radius: 5px;
width: 80%;
max-width: 500px;
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
}
.close {
color: #aaa;
float: right;
font-size: 28px;
font-weight: bold;
cursor: pointer;
}
.close:hover,
.close:focus {
color: #000;
text-decoration: none;
}
.modal-actions {
display: flex;
justify-content: flex-end;
gap: 10px;
margin-top: 20px;
}
.edit-entry-btn, .delete-entry-btn {
padding: 5px 10px;
margin-right: 5px;
border: none;
border-radius: 3px;
cursor: pointer;
}
.edit-entry-btn {
background-color: #2196F3;
color: white;
}
.edit-entry-btn:hover {
background-color: #0b7dda;
}
.delete-entry-btn {
background-color: #f44336;
color: white;
}
.delete-entry-btn:hover {
background-color: #d32f2f;
}
input[type="date"], input[type="time"] {
font-family: monospace;
padding: 8px;
width: 100%;
}
/* Force consistent date format display */
input[type="date"]::-webkit-datetime-edit,
input[type="time"]::-webkit-datetime-edit {
font-family: monospace;
}
/* Add some styling to the format hints */
.form-group small {
display: block;
color: #666;
margin-top: 4px;
font-style: italic;
}

View File

@@ -1,22 +0,0 @@
{% 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 %}

234
templates/history.html Normal file
View File

@@ -0,0 +1,234 @@
{% extends "layout.html" %}
{% block content %}
<div class="timetrack-container">
<h2>Complete Time Entry History</h2>
<div class="history-section">
{% if entries %}
<table class="time-history">
<thead>
<tr>
<th>Date</th>
<th>Arrival</th>
<th>Departure</th>
<th>Work Duration</th>
<th>Break Duration</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for entry in entries %}
<tr data-entry-id="{{ entry.id }}">
<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') if entry.departure_time else 'Active' }}</td>
<td>{{ '%d:%02d:%02d'|format(entry.duration//3600, (entry.duration%3600)//60, entry.duration%60) if entry.duration is not none else 'In progress' }}</td>
<td>{{ '%d:%02d:%02d'|format(entry.total_break_duration//3600, (entry.total_break_duration%3600)//60, entry.total_break_duration%60) if entry.total_break_duration is not none else '00:00:00' }}</td>
<td>
<button class="edit-entry-btn" data-id="{{ entry.id }}">Edit</button>
<button class="delete-entry-btn" data-id="{{ entry.id }}">Delete</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p>No time entries recorded yet.</p>
{% endif %}
</div>
</div>
<!-- Edit Entry Modal -->
<div id="edit-modal" class="modal">
<div class="modal-content">
<span class="close">&times;</span>
<h3>Edit Time Entry</h3>
<form id="edit-entry-form">
<input type="hidden" id="edit-entry-id">
<div class="form-group">
<label for="edit-arrival-date">Arrival Date:</label>
<input type="date" id="edit-arrival-date" required>
</div>
<div class="form-group">
<label for="edit-arrival-time">Arrival Time:</label>
<input type="time" id="edit-arrival-time" required>
</div>
<div class="form-group">
<label for="edit-departure-date">Departure Date:</label>
<input type="date" id="edit-departure-date">
</div>
<div class="form-group">
<label for="edit-departure-time">Departure Time:</label>
<input type="time" id="edit-departure-time">
</div>
<button type="submit" class="btn">Save Changes</button>
</form>
</div>
</div>
<!-- Delete Confirmation Modal -->
<div id="delete-modal" class="modal">
<div class="modal-content">
<span class="close">&times;</span>
<h3>Confirm Deletion</h3>
<p>Are you sure you want to delete this time entry? This action cannot be undone.</p>
<input type="hidden" id="delete-entry-id">
<div class="modal-actions">
<button id="confirm-delete" class="btn btn-danger">Delete</button>
<button id="cancel-delete" class="btn">Cancel</button>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Edit entry functionality
document.querySelectorAll('.edit-entry-btn').forEach(button => {
button.addEventListener('click', function() {
const entryId = this.getAttribute('data-id');
const row = document.querySelector(`tr[data-entry-id="${entryId}"]`);
const cells = row.querySelectorAll('td');
// Get date and time from the row
const dateStr = cells[0].textContent.trim();
const arrivalTimeStr = cells[1].textContent.trim();
const departureTimeStr = cells[2].textContent.trim();
// Set values in the form
document.getElementById('edit-entry-id').value = entryId;
document.getElementById('edit-arrival-date').value = dateStr;
// Format time for input (HH:MM format)
document.getElementById('edit-arrival-time').value = arrivalTimeStr.substring(0, 5);
if (departureTimeStr && departureTimeStr !== 'Active') {
document.getElementById('edit-departure-date').value = dateStr;
document.getElementById('edit-departure-time').value = departureTimeStr.substring(0, 5);
} else {
document.getElementById('edit-departure-date').value = '';
document.getElementById('edit-departure-time').value = '';
}
// Show the modal
document.getElementById('edit-modal').style.display = 'block';
});
});
// Delete entry functionality
document.querySelectorAll('.delete-entry-btn').forEach(button => {
button.addEventListener('click', function() {
const entryId = this.getAttribute('data-id');
document.getElementById('delete-entry-id').value = entryId;
document.getElementById('delete-modal').style.display = 'block';
});
});
// Close modals when clicking the X
document.querySelectorAll('.close').forEach(closeBtn => {
closeBtn.addEventListener('click', function() {
this.closest('.modal').style.display = 'none';
});
});
// Close modals when clicking outside
window.addEventListener('click', function(event) {
if (event.target.classList.contains('modal')) {
event.target.style.display = 'none';
}
});
// Cancel delete
document.getElementById('cancel-delete').addEventListener('click', function() {
document.getElementById('delete-modal').style.display = 'none';
});
// Confirm delete
document.getElementById('confirm-delete').addEventListener('click', function() {
const entryId = document.getElementById('delete-entry-id').value;
fetch(`/api/delete/${entryId}`, {
method: 'DELETE',
})
.then(response => response.json())
.then(data => {
if (data.success) {
// Remove the row from the table
document.querySelector(`tr[data-entry-id="${entryId}"]`).remove();
// Close the modal
document.getElementById('delete-modal').style.display = 'none';
// Show success message
alert('Entry deleted successfully');
} else {
alert('Error: ' + data.message);
}
})
.catch(error => {
console.error('Error:', error);
alert('An error occurred while deleting the entry');
});
});
// Submit edit form
document.getElementById('edit-entry-form').addEventListener('submit', function(e) {
e.preventDefault();
const entryId = document.getElementById('edit-entry-id').value;
const arrivalDate = document.getElementById('edit-arrival-date').value;
const arrivalTime = document.getElementById('edit-arrival-time').value;
const departureDate = document.getElementById('edit-departure-date').value || '';
const departureTime = document.getElementById('edit-departure-time').value || '';
// Ensure we have seconds in the time strings
const arrivalTimeWithSeconds = arrivalTime.includes(':') ?
(arrivalTime.split(':').length === 2 ? arrivalTime + ':00' : arrivalTime) :
arrivalTime + ':00:00';
// Format datetime strings for the API (YYYY-MM-DD HH:MM:SS)
const arrivalDateTime = `${arrivalDate} ${arrivalTimeWithSeconds}`;
let departureDateTime = null;
if (departureDate && departureTime) {
const departureTimeWithSeconds = departureTime.includes(':') ?
(departureTime.split(':').length === 2 ? departureTime + ':00' : departureTime) :
departureTime + ':00:00';
departureDateTime = `${departureDate} ${departureTimeWithSeconds}`;
}
// Send update request
fetch(`/api/update/${entryId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
arrival_time: arrivalDateTime,
departure_time: departureDateTime
}),
})
.then(response => {
if (!response.ok) {
return response.json().then(data => {
throw new Error(data.message || 'Server error');
});
}
return response.json();
})
.then(data => {
if (data.success) {
// Close the modal
document.getElementById('edit-modal').style.display = 'none';
// Refresh the page to show updated data
location.reload();
} else {
alert('Error: ' + data.message);
}
})
.catch(error => {
console.error('Error:', error);
alert('An error occurred while updating the entry: ' + error.message);
});
});
});
</script>
{% endblock %}

View File

@@ -53,16 +53,21 @@
<th>Departure</th> <th>Departure</th>
<th>Work Duration</th> <th>Work Duration</th>
<th>Break Duration</th> <th>Break Duration</th>
<th>Actions</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for entry in history %} {% for entry in history %}
<tr> <tr data-entry-id="{{ entry.id }}">
<td>{{ entry.arrival_time.strftime('%Y-%m-%d') }}</td> <td>{{ entry.arrival_time.strftime('%Y-%m-%d') }}</td>
<td>{{ entry.arrival_time.strftime('%H:%M:%S') }}</td> <td>{{ entry.arrival_time.strftime('%H:%M:%S') }}</td>
<td>{{ entry.departure_time.strftime('%H:%M:%S') }}</td> <td>{{ entry.departure_time.strftime('%H:%M:%S') if entry.departure_time else 'Active' }}</td>
<td>{{ '%d:%02d:%02d'|format(entry.duration//3600, (entry.duration%3600)//60, entry.duration%60) }}</td> <td>{{ '%d:%02d:%02d'|format(entry.duration//3600, (entry.duration%3600)//60, entry.duration%60) if entry.duration is not none else 'In progress' }}</td>
<td>{{ '%d:%02d:%02d'|format(entry.total_break_duration//3600, (entry.total_break_duration%3600)//60, entry.total_break_duration%60) }}</td> <td>{{ '%d:%02d:%02d'|format(entry.total_break_duration//3600, (entry.total_break_duration%3600)//60, entry.total_break_duration%60) if entry.total_break_duration is not none else '00:00:00' }}</td>
<td>
<button class="edit-entry-btn" data-id="{{ entry.id }}">Edit</button>
<button class="delete-entry-btn" data-id="{{ entry.id }}">Delete</button>
</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
@@ -91,4 +96,207 @@
<p>No complicated setup or configuration needed. Start tracking right away!</p> <p>No complicated setup or configuration needed. Start tracking right away!</p>
</div> </div>
</div> </div>
<!-- Edit Entry Modal -->
<div id="edit-modal" class="modal">
<div class="modal-content">
<span class="close">&times;</span>
<h3>Edit Time Entry</h3>
<form id="edit-entry-form">
<input type="hidden" id="edit-entry-id">
<div class="form-group">
<label for="edit-arrival-date">Arrival Date:</label>
<input type="date" id="edit-arrival-date" required>
<small>Format: YYYY-MM-DD</small>
</div>
<div class="form-group">
<label for="edit-arrival-time">Arrival Time (24h):</label>
<input type="time" id="edit-arrival-time" required step="1">
<small>Format: HH:MM (24-hour)</small>
</div>
<div class="form-group">
<label for="edit-departure-date">Departure Date:</label>
<input type="date" id="edit-departure-date">
<small>Format: YYYY-MM-DD</small>
</div>
<div class="form-group">
<label for="edit-departure-time">Departure Time (24h):</label>
<input type="time" id="edit-departure-time" step="1">
<small>Format: HH:MM (24-hour)</small>
</div>
<button type="submit" class="btn">Save Changes</button>
</form>
</div>
</div>
<!-- Delete Confirmation Modal -->
<div id="delete-modal" class="modal">
<div class="modal-content">
<span class="close">&times;</span>
<h3>Confirm Deletion</h3>
<p>Are you sure you want to delete this time entry? This action cannot be undone.</p>
<input type="hidden" id="delete-entry-id">
<div class="modal-actions">
<button id="confirm-delete" class="btn btn-danger">Delete</button>
<button id="cancel-delete" class="btn">Cancel</button>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Edit entry functionality
document.querySelectorAll('.edit-entry-btn').forEach(button => {
button.addEventListener('click', function() {
const entryId = this.getAttribute('data-id');
const row = document.querySelector(`tr[data-entry-id="${entryId}"]`);
const cells = row.querySelectorAll('td');
// Get date and time from the row
const dateStr = cells[0].textContent.trim();
const arrivalTimeStr = cells[1].textContent.trim();
const departureTimeStr = cells[2].textContent.trim();
// Set values in the form
document.getElementById('edit-entry-id').value = entryId;
document.getElementById('edit-arrival-date').value = dateStr;
// Format time for input (HH:MM format)
document.getElementById('edit-arrival-time').value = arrivalTimeStr.substring(0, 5);
if (departureTimeStr && departureTimeStr !== 'Active') {
document.getElementById('edit-departure-date').value = dateStr;
document.getElementById('edit-departure-time').value = departureTimeStr.substring(0, 5);
} else {
document.getElementById('edit-departure-date').value = '';
document.getElementById('edit-departure-time').value = '';
}
// Show the modal
document.getElementById('edit-modal').style.display = 'block';
});
});
// Delete entry functionality
document.querySelectorAll('.delete-entry-btn').forEach(button => {
button.addEventListener('click', function() {
const entryId = this.getAttribute('data-id');
document.getElementById('delete-entry-id').value = entryId;
document.getElementById('delete-modal').style.display = 'block';
});
});
// Close modals when clicking the X
document.querySelectorAll('.close').forEach(closeBtn => {
closeBtn.addEventListener('click', function() {
this.closest('.modal').style.display = 'none';
});
});
// Close modals when clicking outside
window.addEventListener('click', function(event) {
if (event.target.classList.contains('modal')) {
event.target.style.display = 'none';
}
});
// Cancel delete
document.getElementById('cancel-delete').addEventListener('click', function() {
document.getElementById('delete-modal').style.display = 'none';
});
// Confirm delete
document.getElementById('confirm-delete').addEventListener('click', function() {
const entryId = document.getElementById('delete-entry-id').value;
fetch(`/api/delete/${entryId}`, {
method: 'DELETE',
})
.then(response => response.json())
.then(data => {
if (data.success) {
// Remove the row from the table
document.querySelector(`tr[data-entry-id="${entryId}"]`).remove();
// Close the modal
document.getElementById('delete-modal').style.display = 'none';
// Show success message
alert('Entry deleted successfully');
} else {
alert('Error: ' + data.message);
}
})
.catch(error => {
console.error('Error:', error);
alert('An error occurred while deleting the entry');
});
});
// Submit edit form
document.getElementById('edit-entry-form').addEventListener('submit', function(e) {
e.preventDefault();
const entryId = document.getElementById('edit-entry-id').value;
const arrivalDate = document.getElementById('edit-arrival-date').value;
const arrivalTime = document.getElementById('edit-arrival-time').value;
const departureDate = document.getElementById('edit-departure-date').value || '';
const departureTime = document.getElementById('edit-departure-time').value || '';
// Ensure we have seconds in the time strings
const arrivalTimeWithSeconds = arrivalTime.includes(':') ?
(arrivalTime.split(':').length === 2 ? arrivalTime + ':00' : arrivalTime) :
arrivalTime + ':00:00';
// Format datetime strings for the API (YYYY-MM-DD HH:MM:SS)
const arrivalDateTime = `${arrivalDate} ${arrivalTimeWithSeconds}`;
let departureDateTime = null;
if (departureDate && departureTime) {
const departureTimeWithSeconds = departureTime.includes(':') ?
(departureTime.split(':').length === 2 ? departureTime + ':00' : departureTime) :
departureTime + ':00:00';
departureDateTime = `${departureDate} ${departureTimeWithSeconds}`;
}
console.log('Sending update:', {
arrival_time: arrivalDateTime,
departure_time: departureDateTime
});
// Send update request
fetch(`/api/update/${entryId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
arrival_time: arrivalDateTime,
departure_time: departureDateTime
}),
})
.then(response => {
if (!response.ok) {
return response.json().then(data => {
throw new Error(data.message || 'Server error');
});
}
return response.json();
})
.then(data => {
if (data.success) {
// Close the modal
document.getElementById('edit-modal').style.display = 'none';
// Refresh the page to show updated data
location.reload();
} else {
alert('Error: ' + data.message);
}
})
.catch(error => {
console.error('Error:', error);
alert('An error occurred while updating the entry: ' + error.message);
});
});
});
</script>
{% endblock %} {% endblock %}

View File

@@ -11,10 +11,9 @@
<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('about') }}">About</a></li> <li><a href="{{ url_for('history') }}">Complete History</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> <li><a href="{{ url_for('config') }}" {% if title == 'Configuration' %}class="active"{% endif %}>Configuration</a></li>
<li><a href="{{ url_for('about') }}">About</a></li>
</ul> </ul>
</nav> </nav>
</header> </header>
@@ -24,7 +23,7 @@
</main> </main>
<footer> <footer>
<p>&copy; 2023 TimeTrack</p> <p>&copy; 2025 TimeTrack</p>
</footer> </footer>
<script src="{{ url_for('static', filename='js/script.js') }}"></script> <script src="{{ url_for('static', filename='js/script.js') }}"></script>

View File

@@ -22,30 +22,212 @@
<div class="history-section"> <div class="history-section">
<h2>Time Entry History</h2> <h2>Time Entry History</h2>
{% if history %} <!-- Time history table -->
<table class="time-history"> <table class="time-history">
<thead> <thead>
<tr> <tr>
<th>Date</th> <th>Date</th>
<th>Arrival</th> <th>Arrival</th>
<th>Departure</th> <th>Departure</th>
<th>Duration</th> <th>Duration</th>
</tr> <th>Actions</th>
</thead> </tr>
<tbody> </thead>
{% for entry in history %} <tbody id="time-history-body">
<tr> {% for entry in history %}
<td>{{ entry.arrival_time.strftime('%Y-%m-%d') }}</td> <tr data-entry-id="{{ entry.id }}">
<td>{{ entry.arrival_time.strftime('%H:%M:%S') }}</td> <td>{{ entry.arrival_time.strftime('%Y-%m-%d') }}</td>
<td>{{ entry.departure_time.strftime('%H:%M:%S') }}</td> <td>{{ entry.arrival_time.strftime('%H:%M:%S') }}</td>
<td>{{ '%d:%02d:%02d'|format(entry.duration//3600, (entry.duration%3600)//60, entry.duration%60) }}</td> <td>{{ entry.departure_time.strftime('%H:%M:%S') if entry.departure_time else 'Active' }}</td>
</tr> <td>{{ (entry.duration // 3600)|string + 'h ' + ((entry.duration % 3600) // 60)|string + 'm' if entry.duration else 'N/A' }}</td>
{% endfor %} <td>
</tbody> <button class="edit-entry-btn" data-id="{{ entry.id }}">Edit</button>
</table> <button class="delete-entry-btn" data-id="{{ entry.id }}">Delete</button>
{% else %} </td>
<p>No time entries recorded yet.</p> </tr>
{% endif %} {% endfor %}
</tbody>
</table>
<!-- Edit Entry Modal -->
<div id="edit-modal" class="modal">
<div class="modal-content">
<span class="close">&times;</span>
<h3>Edit Time Entry</h3>
<form id="edit-entry-form">
<input type="hidden" id="edit-entry-id">
<div class="form-group">
<label for="edit-arrival-date">Arrival Date:</label>
<input type="date" id="edit-arrival-date" required>
</div>
<div class="form-group">
<label for="edit-arrival-time">Arrival Time:</label>
<input type="time" id="edit-arrival-time" required>
</div>
<div class="form-group">
<label for="edit-departure-date">Departure Date:</label>
<input type="date" id="edit-departure-date">
</div>
<div class="form-group">
<label for="edit-departure-time">Departure Time:</label>
<input type="time" id="edit-departure-time">
</div>
<button type="submit" class="btn">Save Changes</button>
</form>
</div>
</div>
<!-- Delete Confirmation Modal -->
<div id="delete-modal" class="modal">
<div class="modal-content">
<span class="close">&times;</span>
<h3>Confirm Deletion</h3>
<p>Are you sure you want to delete this time entry? This action cannot be undone.</p>
<input type="hidden" id="delete-entry-id">
<div class="modal-actions">
<button id="confirm-delete" class="btn btn-danger">Delete</button>
<button id="cancel-delete" class="btn">Cancel</button>
</div>
</div>
</div>
</div> </div>
</div> </div>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Edit entry functionality
document.querySelectorAll('.edit-entry-btn').forEach(button => {
button.addEventListener('click', function() {
const entryId = this.getAttribute('data-id');
const row = document.querySelector(`tr[data-entry-id="${entryId}"]`);
const cells = row.querySelectorAll('td');
// Get date and time from the row
const dateStr = cells[0].textContent;
const arrivalTimeStr = cells[1].textContent;
const departureTimeStr = cells[2].textContent !== 'Active' ? cells[2].textContent : '';
// Parse date and time
const arrivalDate = new Date(`${dateStr}T${arrivalTimeStr}`);
// Set values in the form
document.getElementById('edit-entry-id').value = entryId;
document.getElementById('edit-arrival-date').value = dateStr;
document.getElementById('edit-arrival-time').value = arrivalTimeStr;
if (departureTimeStr) {
const departureDate = new Date(`${dateStr}T${departureTimeStr}`);
document.getElementById('edit-departure-date').value = dateStr;
document.getElementById('edit-departure-time').value = departureTimeStr;
} else {
document.getElementById('edit-departure-date').value = '';
document.getElementById('edit-departure-time').value = '';
}
// Show the modal
document.getElementById('edit-modal').style.display = 'block';
});
});
// Delete entry functionality
document.querySelectorAll('.delete-entry-btn').forEach(button => {
button.addEventListener('click', function() {
const entryId = this.getAttribute('data-id');
document.getElementById('delete-entry-id').value = entryId;
document.getElementById('delete-modal').style.display = 'block';
});
});
// Close modals when clicking the X
document.querySelectorAll('.close').forEach(closeBtn => {
closeBtn.addEventListener('click', function() {
this.closest('.modal').style.display = 'none';
});
});
// Close modals when clicking outside
window.addEventListener('click', function(event) {
if (event.target.classList.contains('modal')) {
event.target.style.display = 'none';
}
});
// Cancel delete
document.getElementById('cancel-delete').addEventListener('click', function() {
document.getElementById('delete-modal').style.display = 'none';
});
// Confirm delete
document.getElementById('confirm-delete').addEventListener('click', function() {
const entryId = document.getElementById('delete-entry-id').value;
fetch(`/api/delete/${entryId}`, {
method: 'DELETE',
})
.then(response => response.json())
.then(data => {
if (data.success) {
// Remove the row from the table
document.querySelector(`tr[data-entry-id="${entryId}"]`).remove();
// Close the modal
document.getElementById('delete-modal').style.display = 'none';
// Show success message
alert('Entry deleted successfully');
} else {
alert('Error: ' + data.message);
}
})
.catch(error => {
console.error('Error:', error);
alert('An error occurred while deleting the entry');
});
});
// Submit edit form
document.getElementById('edit-entry-form').addEventListener('submit', function(e) {
e.preventDefault();
const entryId = document.getElementById('edit-entry-id').value;
const arrivalDate = document.getElementById('edit-arrival-date').value;
const arrivalTime = document.getElementById('edit-arrival-time').value;
const departureDate = document.getElementById('edit-departure-date').value;
const departureTime = document.getElementById('edit-departure-time').value;
// Format datetime strings
const arrivalDateTime = `${arrivalDate} ${arrivalTime}:00`;
let departureDateTime = null;
if (departureDate && departureTime) {
departureDateTime = `${departureDate} ${departureTime}:00`;
}
// Send update request
fetch(`/api/update/${entryId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
arrival_time: arrivalDateTime,
departure_time: departureDateTime
}),
})
.then(response => response.json())
.then(data => {
if (data.success) {
// Close the modal
document.getElementById('edit-modal').style.display = 'none';
// Refresh the page to show updated data
location.reload();
} else {
alert('Error: ' + data.message);
}
})
.catch(error => {
console.error('Error:', error);
alert('An error occurred while updating the entry');
});
});
});
</script>
{% endblock %} {% endblock %}