Allow manual time entry.
This commit is contained in:
92
app.py
92
app.py
@@ -1580,6 +1580,98 @@ def resume_entry(entry_id):
|
||||
'total_break_duration': entry_to_resume.total_break_duration
|
||||
})
|
||||
|
||||
@app.route('/api/manual-entry', methods=['POST'])
|
||||
@login_required
|
||||
def manual_entry():
|
||||
try:
|
||||
data = request.get_json()
|
||||
|
||||
# Extract data from request
|
||||
project_id = data.get('project_id')
|
||||
start_date = data.get('start_date')
|
||||
start_time = data.get('start_time')
|
||||
end_date = data.get('end_date')
|
||||
end_time = data.get('end_time')
|
||||
break_minutes = int(data.get('break_minutes', 0))
|
||||
notes = data.get('notes', '')
|
||||
|
||||
# Validate required fields
|
||||
if not all([start_date, start_time, end_date, end_time]):
|
||||
return jsonify({'error': 'Start and end date/time are required'}), 400
|
||||
|
||||
# Parse datetime strings
|
||||
try:
|
||||
arrival_datetime = datetime.strptime(f"{start_date} {start_time}", '%Y-%m-%d %H:%M:%S')
|
||||
departure_datetime = datetime.strptime(f"{end_date} {end_time}", '%Y-%m-%d %H:%M:%S')
|
||||
except ValueError:
|
||||
try:
|
||||
# Try without seconds if parsing fails
|
||||
arrival_datetime = datetime.strptime(f"{start_date} {start_time}:00", '%Y-%m-%d %H:%M:%S')
|
||||
departure_datetime = datetime.strptime(f"{end_date} {end_time}:00", '%Y-%m-%d %H:%M:%S')
|
||||
except ValueError:
|
||||
return jsonify({'error': 'Invalid date/time format'}), 400
|
||||
|
||||
# Validate that end time is after start time
|
||||
if departure_datetime <= arrival_datetime:
|
||||
return jsonify({'error': 'End time must be after start time'}), 400
|
||||
|
||||
# Validate project access if project is specified
|
||||
if project_id:
|
||||
project = Project.query.get(project_id)
|
||||
if not project or not project.is_user_allowed(g.user):
|
||||
return jsonify({'error': 'Invalid or unauthorized project'}), 403
|
||||
|
||||
# Check for overlapping entries for this user
|
||||
overlapping_entry = TimeEntry.query.filter(
|
||||
TimeEntry.user_id == g.user.id,
|
||||
TimeEntry.departure_time.isnot(None),
|
||||
TimeEntry.arrival_time < departure_datetime,
|
||||
TimeEntry.departure_time > arrival_datetime
|
||||
).first()
|
||||
|
||||
if overlapping_entry:
|
||||
return jsonify({
|
||||
'error': 'This time entry overlaps with an existing entry'
|
||||
}), 400
|
||||
|
||||
# Calculate total duration in seconds
|
||||
total_duration = int((departure_datetime - arrival_datetime).total_seconds())
|
||||
break_duration_seconds = break_minutes * 60
|
||||
|
||||
# Validate break duration doesn't exceed total duration
|
||||
if break_duration_seconds >= total_duration:
|
||||
return jsonify({'error': 'Break duration cannot exceed total work duration'}), 400
|
||||
|
||||
# Calculate work duration (total duration minus breaks)
|
||||
work_duration = total_duration - break_duration_seconds
|
||||
|
||||
# Create the manual time entry
|
||||
new_entry = TimeEntry(
|
||||
user_id=g.user.id,
|
||||
arrival_time=arrival_datetime,
|
||||
departure_time=departure_datetime,
|
||||
duration=work_duration,
|
||||
total_break_duration=break_duration_seconds,
|
||||
project_id=int(project_id) if project_id else None,
|
||||
notes=notes,
|
||||
is_paused=False,
|
||||
pause_start_time=None
|
||||
)
|
||||
|
||||
db.session.add(new_entry)
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': 'Manual time entry added successfully',
|
||||
'entry_id': new_entry.id
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating manual time entry: {str(e)}")
|
||||
db.session.rollback()
|
||||
return jsonify({'error': 'An error occurred while creating the time entry'}), 500
|
||||
|
||||
@app.errorhandler(404)
|
||||
def page_not_found(e):
|
||||
return render_template('404.html'), 404
|
||||
|
||||
@@ -61,6 +61,7 @@ Please <a href="{{ url_for('login') }}">login</a> or <a href="{{ url_for('regist
|
||||
<textarea id="work-notes" name="notes" rows="2" placeholder="What are you working on?"></textarea>
|
||||
</div>
|
||||
<button id="arrive-btn" class="arrive-btn">Arrive</button>
|
||||
<button id="manual-entry-btn" class="manual-entry-btn">Add Manual Entry</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -181,8 +182,113 @@ Please <a href="{{ url_for('login') }}">login</a> or <a href="{{ url_for('regist
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Manual Time Entry Modal -->
|
||||
<div id="manual-entry-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<span class="close">×</span>
|
||||
<h3>Add Manual Time Entry</h3>
|
||||
<form id="manual-entry-form">
|
||||
<div class="form-group">
|
||||
<label for="manual-project-select">Project (Optional):</label>
|
||||
<select id="manual-project-select" name="project_id">
|
||||
<option value="">No specific project</option>
|
||||
{% for project in available_projects %}
|
||||
<option value="{{ project.id }}">{{ project.code }} - {{ project.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="manual-start-date">Start Date:</label>
|
||||
<input type="date" id="manual-start-date" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="manual-start-time">Start Time:</label>
|
||||
<input type="time" id="manual-start-time" required step="1">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="manual-end-date">End Date:</label>
|
||||
<input type="date" id="manual-end-date" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="manual-end-time">End Time:</label>
|
||||
<input type="time" id="manual-end-time" required step="1">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="manual-break-minutes">Break Duration (minutes):</label>
|
||||
<input type="number" id="manual-break-minutes" min="0" value="0" placeholder="Break time in minutes">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="manual-notes">Notes (Optional):</label>
|
||||
<textarea id="manual-notes" name="notes" rows="3" placeholder="Description of work performed"></textarea>
|
||||
</div>
|
||||
<button type="submit" class="btn">Add Entry</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Manual entry functionality
|
||||
document.getElementById('manual-entry-btn').addEventListener('click', function() {
|
||||
// Set default dates to today
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
document.getElementById('manual-start-date').value = today;
|
||||
document.getElementById('manual-end-date').value = today;
|
||||
document.getElementById('manual-entry-modal').style.display = 'block';
|
||||
});
|
||||
|
||||
// Manual entry form submission
|
||||
document.getElementById('manual-entry-form').addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const projectId = document.getElementById('manual-project-select').value || null;
|
||||
const startDate = document.getElementById('manual-start-date').value;
|
||||
const startTime = document.getElementById('manual-start-time').value;
|
||||
const endDate = document.getElementById('manual-end-date').value;
|
||||
const endTime = document.getElementById('manual-end-time').value;
|
||||
const breakMinutes = parseInt(document.getElementById('manual-break-minutes').value) || 0;
|
||||
const notes = document.getElementById('manual-notes').value;
|
||||
|
||||
// Validate end time is after start time
|
||||
const startDateTime = new Date(`${startDate}T${startTime}`);
|
||||
const endDateTime = new Date(`${endDate}T${endTime}`);
|
||||
|
||||
if (endDateTime <= startDateTime) {
|
||||
alert('End time must be after start time');
|
||||
return;
|
||||
}
|
||||
|
||||
// Send request to create manual entry
|
||||
fetch('/api/manual-entry', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
project_id: projectId,
|
||||
start_date: startDate,
|
||||
start_time: startTime,
|
||||
end_date: endDate,
|
||||
end_time: endTime,
|
||||
break_minutes: breakMinutes,
|
||||
notes: notes
|
||||
}),
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
document.getElementById('manual-entry-modal').style.display = 'none';
|
||||
location.reload(); // Refresh to show new entry
|
||||
} else {
|
||||
alert('Error: ' + data.message);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('An error occurred while adding the manual entry');
|
||||
});
|
||||
});
|
||||
|
||||
// Edit entry functionality
|
||||
document.querySelectorAll('.edit-entry-btn').forEach(button => {
|
||||
button.addEventListener('click', function() {
|
||||
@@ -402,6 +508,74 @@ Please <a href="{{ url_for('login') }}">login</a> or <a href="{{ url_for('regist
|
||||
display: block;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.manual-entry-btn {
|
||||
background: #17a2b8;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 6px;
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
margin-left: 1rem;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.manual-entry-btn:hover {
|
||||
background: #138496;
|
||||
}
|
||||
|
||||
.modal {
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0,0,0,0.4);
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background-color: #fefefe;
|
||||
margin: 5% auto;
|
||||
padding: 20px;
|
||||
border: 1px solid #888;
|
||||
border-radius: 8px;
|
||||
width: 500px;
|
||||
max-width: 90%;
|
||||
max-height: 80%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.modal .form-group {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.modal label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.modal input,
|
||||
.modal select,
|
||||
.modal textarea {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
border: 2px solid #e9ecef;
|
||||
border-radius: 6px;
|
||||
font-size: 1rem;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.modal input:focus,
|
||||
.modal select:focus,
|
||||
.modal textarea:focus {
|
||||
outline: none;
|
||||
border-color: #4CAF50;
|
||||
box-shadow: 0 0 0 3px rgba(76, 175, 80, 0.1);
|
||||
}
|
||||
</style>
|
||||
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user