Improve logging features (edit and delete).
This commit is contained in:
130
app.py
130
app.py
@@ -1,7 +1,8 @@
|
||||
from flask import Flask, render_template, request, redirect, url_for, jsonify, flash
|
||||
from models import db, TimeEntry, WorkConfig
|
||||
from datetime import datetime
|
||||
from datetime import datetime, time
|
||||
import os
|
||||
from sqlalchemy import func
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///timetrack.db'
|
||||
@@ -13,13 +14,22 @@ db.init_app(app)
|
||||
|
||||
@app.route('/')
|
||||
def home():
|
||||
# Get the latest time entry that doesn't have a departure time
|
||||
# Get active time entry (if any)
|
||||
active_entry = TimeEntry.query.filter_by(departure_time=None).first()
|
||||
|
||||
# Get all completed time entries, ordered by most recent first
|
||||
history = TimeEntry.query.filter(TimeEntry.departure_time.isnot(None)).order_by(TimeEntry.arrival_time.desc()).all()
|
||||
# Get today's date
|
||||
today = datetime.now().date()
|
||||
|
||||
return render_template('index.html', title='Home', active_entry=active_entry, history=history)
|
||||
# Get time entries for today only
|
||||
today_start = datetime.combine(today, time.min)
|
||||
today_end = datetime.combine(today, time.max)
|
||||
|
||||
today_entries = TimeEntry.query.filter(
|
||||
TimeEntry.arrival_time >= today_start,
|
||||
TimeEntry.arrival_time <= today_end
|
||||
).order_by(TimeEntry.arrival_time.desc()).all()
|
||||
|
||||
return render_template('index.html', title='Home', active_entry=active_entry, history=today_entries)
|
||||
|
||||
@app.route('/about')
|
||||
def about():
|
||||
@@ -30,10 +40,6 @@ def contact():
|
||||
# redacted
|
||||
return render_template('contact.html', title='Contact')
|
||||
|
||||
@app.route('/thank-you')
|
||||
def thank_you():
|
||||
return render_template('thank_you.html', title='Thank You')
|
||||
|
||||
# We can keep this route as a redirect to home for backward compatibility
|
||||
@app.route('/timetrack')
|
||||
def timetrack():
|
||||
@@ -65,10 +71,14 @@ def leave(entry_id):
|
||||
final_break_duration = int((departure_time - entry.pause_start_time).total_seconds())
|
||||
entry.total_break_duration += final_break_duration
|
||||
entry.is_paused = False
|
||||
entry.pause_start_time = None
|
||||
|
||||
# Calculate duration in seconds (excluding breaks)
|
||||
raw_duration = (departure_time - entry.arrival_time).total_seconds()
|
||||
entry.duration = int(raw_duration - entry.total_break_duration)
|
||||
# Calculate work duration considering breaks
|
||||
entry.duration, effective_break = calculate_work_duration(
|
||||
entry.arrival_time,
|
||||
departure_time,
|
||||
entry.total_break_duration
|
||||
)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
@@ -77,7 +87,8 @@ def leave(entry_id):
|
||||
'arrival_time': entry.arrival_time.strftime('%Y-%m-%d %H:%M:%S'),
|
||||
'departure_time': entry.departure_time.strftime('%Y-%m-%d %H:%M:%S'),
|
||||
'duration': entry.duration,
|
||||
'total_break_duration': entry.total_break_duration
|
||||
'total_break_duration': entry.total_break_duration,
|
||||
'effective_break_duration': effective_break
|
||||
})
|
||||
|
||||
# Add this new route to handle pausing/resuming
|
||||
@@ -152,5 +163,98 @@ def create_tables():
|
||||
if 'is_paused' not in columns or 'pause_start_time' not in columns or 'total_break_duration' not in columns:
|
||||
print("WARNING: Database schema is outdated. Please run migrate_db.py to update it.")
|
||||
|
||||
@app.route('/api/delete/<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__':
|
||||
app.run(debug=True)
|
||||
@@ -6,3 +6,4 @@ itsdangerous==2.0.1
|
||||
click==8.0.1
|
||||
Flask-SQLAlchemy==2.5.1
|
||||
SQLAlchemy==1.4.23
|
||||
python-dotenv==0.19.0
|
||||
@@ -69,39 +69,51 @@ main {
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 1rem;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
margin-bottom: 5px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.form-group input,
|
||||
.form-group textarea {
|
||||
width: 100%;
|
||||
padding: 0.5rem;
|
||||
padding: 8px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 3px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
button {
|
||||
background-color: #4CAF50;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.5rem 1rem;
|
||||
padding: 8px 16px;
|
||||
cursor: pointer;
|
||||
border-radius: 3px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: inline-block;
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
background-color: #4CAF50;
|
||||
color: white;
|
||||
padding: 0.5rem 1rem;
|
||||
text-decoration: none;
|
||||
border-radius: 3px;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background-color: #45a049;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background-color: #f44336;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background-color: #d32f2f;
|
||||
}
|
||||
|
||||
footer {
|
||||
@@ -278,7 +290,8 @@ footer {
|
||||
.config-form small {
|
||||
display: block;
|
||||
color: #666;
|
||||
margin-top: 0.25rem;
|
||||
margin-top: 4px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
@@ -312,3 +325,92 @@ footer {
|
||||
color: #721c24;
|
||||
border: 1px solid #f5c6cb;
|
||||
}
|
||||
|
||||
/* Modal styles */
|
||||
.modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0,0,0,0.5);
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background-color: #fff;
|
||||
margin: 10% auto;
|
||||
padding: 20px;
|
||||
border-radius: 5px;
|
||||
width: 80%;
|
||||
max-width: 500px;
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.close {
|
||||
color: #aaa;
|
||||
float: right;
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.close:hover,
|
||||
.close:focus {
|
||||
color: #000;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.modal-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.edit-entry-btn, .delete-entry-btn {
|
||||
padding: 5px 10px;
|
||||
margin-right: 5px;
|
||||
border: none;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.edit-entry-btn {
|
||||
background-color: #2196F3;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.edit-entry-btn:hover {
|
||||
background-color: #0b7dda;
|
||||
}
|
||||
|
||||
.delete-entry-btn {
|
||||
background-color: #f44336;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.delete-entry-btn:hover {
|
||||
background-color: #d32f2f;
|
||||
}
|
||||
|
||||
input[type="date"], input[type="time"] {
|
||||
font-family: monospace;
|
||||
padding: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Force consistent date format display */
|
||||
input[type="date"]::-webkit-datetime-edit,
|
||||
input[type="time"]::-webkit-datetime-edit {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
/* Add some styling to the format hints */
|
||||
.form-group small {
|
||||
display: block;
|
||||
color: #666;
|
||||
margin-top: 4px;
|
||||
font-style: italic;
|
||||
}
|
||||
@@ -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
234
templates/history.html
Normal 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">×</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">×</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 %}
|
||||
@@ -53,16 +53,21 @@
|
||||
<th>Departure</th>
|
||||
<th>Work Duration</th>
|
||||
<th>Break Duration</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% 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('%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>
|
||||
<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>
|
||||
@@ -91,4 +96,207 @@
|
||||
<p>No complicated setup or configuration needed. Start tracking right away!</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Edit Entry Modal -->
|
||||
<div id="edit-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<span class="close">×</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">×</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 %}
|
||||
@@ -11,10 +11,9 @@
|
||||
<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('history') }}">Complete History</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>
|
||||
</nav>
|
||||
</header>
|
||||
@@ -24,7 +23,7 @@
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<p>© 2023 TimeTrack</p>
|
||||
<p>© 2025 TimeTrack</p>
|
||||
</footer>
|
||||
|
||||
<script src="{{ url_for('static', filename='js/script.js') }}"></script>
|
||||
|
||||
@@ -22,30 +22,212 @@
|
||||
|
||||
<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 %}
|
||||
<!-- Time history table -->
|
||||
<table class="time-history">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Arrival</th>
|
||||
<th>Departure</th>
|
||||
<th>Duration</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="time-history-body">
|
||||
{% for entry in history %}
|
||||
<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>{{ (entry.duration // 3600)|string + 'h ' + ((entry.duration % 3600) // 60)|string + 'm' if entry.duration else 'N/A' }}</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>
|
||||
|
||||
<!-- Edit Entry Modal -->
|
||||
<div id="edit-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<span class="close">×</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">×</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>
|
||||
|
||||
<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 %}
|
||||
Reference in New Issue
Block a user