Add time rounding option.
This commit is contained in:
61
app.py
61
app.py
@@ -8,6 +8,7 @@ from data_export import (
|
|||||||
export_to_csv, export_to_excel, export_team_hours_to_csv, export_team_hours_to_excel,
|
export_to_csv, export_to_excel, export_team_hours_to_csv, export_team_hours_to_excel,
|
||||||
export_analytics_csv, export_analytics_excel
|
export_analytics_csv, export_analytics_excel
|
||||||
)
|
)
|
||||||
|
from time_utils import apply_time_rounding, round_duration_to_interval, get_user_rounding_settings
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime, time, timedelta
|
from datetime import datetime, time, timedelta
|
||||||
import os
|
import os
|
||||||
@@ -134,7 +135,9 @@ def run_migrations():
|
|||||||
work_config_migrations = [
|
work_config_migrations = [
|
||||||
('additional_break_minutes', "ALTER TABLE work_config ADD COLUMN additional_break_minutes INTEGER DEFAULT 15"),
|
('additional_break_minutes', "ALTER TABLE work_config ADD COLUMN additional_break_minutes INTEGER DEFAULT 15"),
|
||||||
('additional_break_threshold_hours', "ALTER TABLE work_config ADD COLUMN additional_break_threshold_hours FLOAT DEFAULT 9.0"),
|
('additional_break_threshold_hours', "ALTER TABLE work_config ADD COLUMN additional_break_threshold_hours FLOAT DEFAULT 9.0"),
|
||||||
('user_id', "ALTER TABLE work_config ADD COLUMN user_id INTEGER")
|
('user_id', "ALTER TABLE work_config ADD COLUMN user_id INTEGER"),
|
||||||
|
('time_rounding_minutes', "ALTER TABLE work_config ADD COLUMN time_rounding_minutes INTEGER DEFAULT 0"),
|
||||||
|
('round_to_nearest', "ALTER TABLE work_config ADD COLUMN round_to_nearest BOOLEAN DEFAULT 1")
|
||||||
]
|
]
|
||||||
|
|
||||||
for column_name, sql_command in work_config_migrations:
|
for column_name, sql_command in work_config_migrations:
|
||||||
@@ -1368,19 +1371,30 @@ def leave(entry_id):
|
|||||||
|
|
||||||
# Set the departure time
|
# Set the departure time
|
||||||
departure_time = datetime.now()
|
departure_time = datetime.now()
|
||||||
entry.departure_time = departure_time
|
|
||||||
|
# Apply time rounding if enabled
|
||||||
|
rounded_arrival, rounded_departure = apply_time_rounding(entry.arrival_time, departure_time, g.user)
|
||||||
|
entry.arrival_time = rounded_arrival
|
||||||
|
entry.departure_time = rounded_departure
|
||||||
|
|
||||||
# If currently paused, add the final break duration
|
# If currently paused, add the final break duration
|
||||||
if entry.is_paused and entry.pause_start_time:
|
if entry.is_paused and entry.pause_start_time:
|
||||||
final_break_duration = int((departure_time - entry.pause_start_time).total_seconds())
|
final_break_duration = int((rounded_departure - 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
|
entry.pause_start_time = None
|
||||||
|
|
||||||
|
# Apply rounding to break duration if enabled
|
||||||
|
interval_minutes, round_to_nearest = get_user_rounding_settings(g.user)
|
||||||
|
if interval_minutes > 0:
|
||||||
|
entry.total_break_duration = round_duration_to_interval(
|
||||||
|
entry.total_break_duration, interval_minutes, round_to_nearest
|
||||||
|
)
|
||||||
|
|
||||||
# Calculate work duration considering breaks
|
# Calculate work duration considering breaks
|
||||||
entry.duration, effective_break = calculate_work_duration(
|
entry.duration, effective_break = calculate_work_duration(
|
||||||
entry.arrival_time,
|
rounded_arrival,
|
||||||
departure_time,
|
rounded_departure,
|
||||||
entry.total_break_duration
|
entry.total_break_duration
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1447,13 +1461,21 @@ def config():
|
|||||||
config.additional_break_minutes = int(request.form.get('additional_break_minutes', 15))
|
config.additional_break_minutes = int(request.form.get('additional_break_minutes', 15))
|
||||||
config.additional_break_threshold_hours = float(request.form.get('additional_break_threshold_hours', 9.0))
|
config.additional_break_threshold_hours = float(request.form.get('additional_break_threshold_hours', 9.0))
|
||||||
|
|
||||||
|
# Update time rounding settings
|
||||||
|
config.time_rounding_minutes = int(request.form.get('time_rounding_minutes', 0))
|
||||||
|
config.round_to_nearest = 'round_to_nearest' in request.form
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash('Configuration updated successfully!', 'success')
|
flash('Configuration updated successfully!', 'success')
|
||||||
return redirect(url_for('config'))
|
return redirect(url_for('config'))
|
||||||
except ValueError:
|
except ValueError:
|
||||||
flash('Please enter valid numbers for all fields', 'error')
|
flash('Please enter valid numbers for all fields', 'error')
|
||||||
|
|
||||||
return render_template('config.html', title='Configuration', config=config)
|
# Import time utils for display options
|
||||||
|
from time_utils import get_available_rounding_options
|
||||||
|
rounding_options = get_available_rounding_options()
|
||||||
|
|
||||||
|
return render_template('config.html', title='Configuration', config=config, rounding_options=rounding_options)
|
||||||
|
|
||||||
@app.route('/api/delete/<int:entry_id>', methods=['DELETE'])
|
@app.route('/api/delete/<int:entry_id>', methods=['DELETE'])
|
||||||
@login_required
|
@login_required
|
||||||
@@ -1615,18 +1637,21 @@ def manual_entry():
|
|||||||
if departure_datetime <= arrival_datetime:
|
if departure_datetime <= arrival_datetime:
|
||||||
return jsonify({'error': 'End time must be after start time'}), 400
|
return jsonify({'error': 'End time must be after start time'}), 400
|
||||||
|
|
||||||
|
# Apply time rounding if enabled
|
||||||
|
rounded_arrival, rounded_departure = apply_time_rounding(arrival_datetime, departure_datetime, g.user)
|
||||||
|
|
||||||
# Validate project access if project is specified
|
# Validate project access if project is specified
|
||||||
if project_id:
|
if project_id:
|
||||||
project = Project.query.get(project_id)
|
project = Project.query.get(project_id)
|
||||||
if not project or not project.is_user_allowed(g.user):
|
if not project or not project.is_user_allowed(g.user):
|
||||||
return jsonify({'error': 'Invalid or unauthorized project'}), 403
|
return jsonify({'error': 'Invalid or unauthorized project'}), 403
|
||||||
|
|
||||||
# Check for overlapping entries for this user
|
# Check for overlapping entries for this user (using rounded times)
|
||||||
overlapping_entry = TimeEntry.query.filter(
|
overlapping_entry = TimeEntry.query.filter(
|
||||||
TimeEntry.user_id == g.user.id,
|
TimeEntry.user_id == g.user.id,
|
||||||
TimeEntry.departure_time.isnot(None),
|
TimeEntry.departure_time.isnot(None),
|
||||||
TimeEntry.arrival_time < departure_datetime,
|
TimeEntry.arrival_time < rounded_departure,
|
||||||
TimeEntry.departure_time > arrival_datetime
|
TimeEntry.departure_time > rounded_arrival
|
||||||
).first()
|
).first()
|
||||||
|
|
||||||
if overlapping_entry:
|
if overlapping_entry:
|
||||||
@@ -1634,10 +1659,17 @@ def manual_entry():
|
|||||||
'error': 'This time entry overlaps with an existing entry'
|
'error': 'This time entry overlaps with an existing entry'
|
||||||
}), 400
|
}), 400
|
||||||
|
|
||||||
# Calculate total duration in seconds
|
# Calculate total duration in seconds (using rounded times)
|
||||||
total_duration = int((departure_datetime - arrival_datetime).total_seconds())
|
total_duration = int((rounded_departure - rounded_arrival).total_seconds())
|
||||||
break_duration_seconds = break_minutes * 60
|
break_duration_seconds = break_minutes * 60
|
||||||
|
|
||||||
|
# Apply rounding to break duration if enabled
|
||||||
|
interval_minutes, round_to_nearest = get_user_rounding_settings(g.user)
|
||||||
|
if interval_minutes > 0:
|
||||||
|
break_duration_seconds = round_duration_to_interval(
|
||||||
|
break_duration_seconds, interval_minutes, round_to_nearest
|
||||||
|
)
|
||||||
|
|
||||||
# Validate break duration doesn't exceed total duration
|
# Validate break duration doesn't exceed total duration
|
||||||
if break_duration_seconds >= total_duration:
|
if break_duration_seconds >= total_duration:
|
||||||
return jsonify({'error': 'Break duration cannot exceed total work duration'}), 400
|
return jsonify({'error': 'Break duration cannot exceed total work duration'}), 400
|
||||||
@@ -1645,11 +1677,11 @@ def manual_entry():
|
|||||||
# Calculate work duration (total duration minus breaks)
|
# Calculate work duration (total duration minus breaks)
|
||||||
work_duration = total_duration - break_duration_seconds
|
work_duration = total_duration - break_duration_seconds
|
||||||
|
|
||||||
# Create the manual time entry
|
# Create the manual time entry (using rounded times)
|
||||||
new_entry = TimeEntry(
|
new_entry = TimeEntry(
|
||||||
user_id=g.user.id,
|
user_id=g.user.id,
|
||||||
arrival_time=arrival_datetime,
|
arrival_time=rounded_arrival,
|
||||||
departure_time=departure_datetime,
|
departure_time=rounded_departure,
|
||||||
duration=work_duration,
|
duration=work_duration,
|
||||||
total_break_duration=break_duration_seconds,
|
total_break_duration=break_duration_seconds,
|
||||||
project_id=int(project_id) if project_id else None,
|
project_id=int(project_id) if project_id else None,
|
||||||
@@ -1736,6 +1768,7 @@ def admin_settings():
|
|||||||
|
|
||||||
return render_template('admin_settings.html', title='System Settings', settings=settings)
|
return render_template('admin_settings.html', title='System Settings', settings=settings)
|
||||||
|
|
||||||
|
|
||||||
# Company Management Routes
|
# Company Management Routes
|
||||||
@app.route('/admin/company')
|
@app.route('/admin/company')
|
||||||
@admin_required
|
@admin_required
|
||||||
|
|||||||
@@ -238,6 +238,11 @@ class WorkConfig(db.Model):
|
|||||||
break_threshold_hours = db.Column(db.Float, default=6.0) # Work hours that trigger mandatory break
|
break_threshold_hours = db.Column(db.Float, default=6.0) # Work hours that trigger mandatory break
|
||||||
additional_break_minutes = db.Column(db.Integer, default=15) # Default 15 minutes for additional break
|
additional_break_minutes = db.Column(db.Integer, default=15) # Default 15 minutes for additional break
|
||||||
additional_break_threshold_hours = db.Column(db.Float, default=9.0) # Work hours that trigger additional break
|
additional_break_threshold_hours = db.Column(db.Float, default=9.0) # Work hours that trigger additional break
|
||||||
|
|
||||||
|
# Time rounding settings
|
||||||
|
time_rounding_minutes = db.Column(db.Integer, default=0) # 0 = no rounding, 15 = 15 min, 30 = 30 min
|
||||||
|
round_to_nearest = db.Column(db.Boolean, default=True) # True = round to nearest, False = round up
|
||||||
|
|
||||||
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)
|
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=True)
|
||||||
|
|||||||
@@ -46,7 +46,145 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-section">
|
||||||
|
<h3>Time Rounding Settings</h3>
|
||||||
|
<p class="section-description">
|
||||||
|
Time rounding helps standardize billing and reporting by rounding time entries to specified intervals.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="time_rounding_minutes">Time Rounding Interval:</label>
|
||||||
|
<select id="time_rounding_minutes" name="time_rounding_minutes">
|
||||||
|
{% for value, label in rounding_options %}
|
||||||
|
<option value="{{ value }}" {% if config.time_rounding_minutes == value %}selected{% endif %}>
|
||||||
|
{{ label }}
|
||||||
|
</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
<small>Round time entries to the nearest interval</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="checkbox-group">
|
||||||
|
<input type="checkbox"
|
||||||
|
id="round_to_nearest"
|
||||||
|
name="round_to_nearest"
|
||||||
|
{% if config.round_to_nearest %}checked{% endif %}>
|
||||||
|
<label for="round_to_nearest">Round to nearest interval</label>
|
||||||
|
<small>If unchecked, will always round up</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rounding-example" id="rounding-example">
|
||||||
|
<h4>Example:</h4>
|
||||||
|
<div id="example-text">
|
||||||
|
<!-- JavaScript will populate this -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button type="submit" class="btn btn-primary">Save Configuration</button>
|
<button type="submit" class="btn btn-primary">Save Configuration</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const roundingSelect = document.getElementById('time_rounding_minutes');
|
||||||
|
const roundToNearestCheckbox = document.getElementById('round_to_nearest');
|
||||||
|
const exampleDiv = document.getElementById('example-text');
|
||||||
|
|
||||||
|
function updateExample() {
|
||||||
|
const interval = parseInt(roundingSelect.value);
|
||||||
|
const roundToNearest = roundToNearestCheckbox.checked;
|
||||||
|
|
||||||
|
if (interval === 0) {
|
||||||
|
exampleDiv.innerHTML = '<em>No rounding applied. Times are recorded exactly as entered.</em>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const roundingType = roundToNearest ? 'nearest' : 'up';
|
||||||
|
const examples = [];
|
||||||
|
|
||||||
|
if (interval === 15) {
|
||||||
|
if (roundToNearest) {
|
||||||
|
examples.push('9:07 AM → 9:00 AM');
|
||||||
|
examples.push('9:08 AM → 9:15 AM');
|
||||||
|
examples.push('9:23 AM → 9:30 AM');
|
||||||
|
} else {
|
||||||
|
examples.push('9:01 AM → 9:15 AM');
|
||||||
|
examples.push('9:16 AM → 9:30 AM');
|
||||||
|
examples.push('9:31 AM → 9:45 AM');
|
||||||
|
}
|
||||||
|
} else if (interval === 30) {
|
||||||
|
if (roundToNearest) {
|
||||||
|
examples.push('9:14 AM → 9:00 AM');
|
||||||
|
examples.push('9:16 AM → 9:30 AM');
|
||||||
|
examples.push('9:45 AM → 10:00 AM');
|
||||||
|
} else {
|
||||||
|
examples.push('9:01 AM → 9:30 AM');
|
||||||
|
examples.push('9:31 AM → 10:00 AM');
|
||||||
|
examples.push('10:01 AM → 10:30 AM');
|
||||||
|
}
|
||||||
|
} else if (interval === 60) {
|
||||||
|
if (roundToNearest) {
|
||||||
|
examples.push('9:29 AM → 9:00 AM');
|
||||||
|
examples.push('9:31 AM → 10:00 AM');
|
||||||
|
examples.push('10:30 AM → 11:00 AM');
|
||||||
|
} else {
|
||||||
|
examples.push('9:01 AM → 10:00 AM');
|
||||||
|
examples.push('10:01 AM → 11:00 AM');
|
||||||
|
examples.push('11:01 AM → 12:00 PM');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const exampleText = `Rounding ${roundingType} to ${interval} minute intervals:<br>` +
|
||||||
|
examples.map(ex => `• ${ex}`).join('<br>');
|
||||||
|
exampleDiv.innerHTML = exampleText;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update example when settings change
|
||||||
|
roundingSelect.addEventListener('change', updateExample);
|
||||||
|
roundToNearestCheckbox.addEventListener('change', updateExample);
|
||||||
|
|
||||||
|
// Initial example
|
||||||
|
updateExample();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.section-description {
|
||||||
|
color: #666;
|
||||||
|
font-style: italic;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-group input[type="checkbox"] {
|
||||||
|
width: auto;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-group label {
|
||||||
|
margin: 0;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rounding-example {
|
||||||
|
background: #e8f5e9;
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-top: 1rem;
|
||||||
|
border-left: 4px solid #4CAF50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rounding-example h4 {
|
||||||
|
margin-top: 0;
|
||||||
|
color: #2e7d32;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
159
time_utils.py
Normal file
159
time_utils.py
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
"""
|
||||||
|
Time utility functions for TimeTrack application.
|
||||||
|
Includes time rounding functionality.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
import math
|
||||||
|
|
||||||
|
|
||||||
|
def round_time_to_interval(dt, interval_minutes, round_to_nearest=True):
|
||||||
|
"""
|
||||||
|
Round a datetime to the specified interval.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
dt (datetime): The datetime to round
|
||||||
|
interval_minutes (int): The interval in minutes (15, 30, etc.)
|
||||||
|
round_to_nearest (bool): If True, round to nearest interval; if False, round up
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
datetime: The rounded datetime
|
||||||
|
"""
|
||||||
|
if interval_minutes == 0:
|
||||||
|
return dt # No rounding
|
||||||
|
|
||||||
|
# Calculate the number of minutes from midnight
|
||||||
|
minutes_from_midnight = dt.hour * 60 + dt.minute
|
||||||
|
|
||||||
|
if round_to_nearest:
|
||||||
|
# Round to nearest interval
|
||||||
|
rounded_minutes = round(minutes_from_midnight / interval_minutes) * interval_minutes
|
||||||
|
else:
|
||||||
|
# Round up to next interval
|
||||||
|
rounded_minutes = math.ceil(minutes_from_midnight / interval_minutes) * interval_minutes
|
||||||
|
|
||||||
|
# Convert back to hours and minutes
|
||||||
|
rounded_hour = int(rounded_minutes // 60)
|
||||||
|
rounded_minute = int(rounded_minutes % 60)
|
||||||
|
|
||||||
|
# Handle case where rounding goes to next day
|
||||||
|
if rounded_hour >= 24:
|
||||||
|
rounded_hour = 0
|
||||||
|
dt = dt + timedelta(days=1)
|
||||||
|
|
||||||
|
# Create new datetime with rounded time (keep seconds as 0)
|
||||||
|
return dt.replace(hour=rounded_hour, minute=rounded_minute, second=0, microsecond=0)
|
||||||
|
|
||||||
|
|
||||||
|
def round_duration_to_interval(duration_seconds, interval_minutes, round_to_nearest=True):
|
||||||
|
"""
|
||||||
|
Round a duration to the specified interval.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
duration_seconds (int): The duration in seconds
|
||||||
|
interval_minutes (int): The interval in minutes (15, 30, etc.)
|
||||||
|
round_to_nearest (bool): If True, round to nearest interval; if False, round up
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
int: The rounded duration in seconds
|
||||||
|
"""
|
||||||
|
if interval_minutes == 0:
|
||||||
|
return duration_seconds # No rounding
|
||||||
|
|
||||||
|
# Convert to minutes
|
||||||
|
duration_minutes = duration_seconds / 60
|
||||||
|
interval_seconds = interval_minutes * 60
|
||||||
|
|
||||||
|
if round_to_nearest:
|
||||||
|
# Round to nearest interval
|
||||||
|
rounded_intervals = round(duration_minutes / interval_minutes)
|
||||||
|
else:
|
||||||
|
# Round up to next interval
|
||||||
|
rounded_intervals = math.ceil(duration_minutes / interval_minutes)
|
||||||
|
|
||||||
|
return int(rounded_intervals * interval_seconds)
|
||||||
|
|
||||||
|
|
||||||
|
def get_user_rounding_settings(user):
|
||||||
|
"""
|
||||||
|
Get the time rounding settings for a user.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user: The user object
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple: (interval_minutes, round_to_nearest)
|
||||||
|
"""
|
||||||
|
work_config = user.work_config
|
||||||
|
if work_config:
|
||||||
|
return work_config.time_rounding_minutes, work_config.round_to_nearest
|
||||||
|
else:
|
||||||
|
return 0, True # Default: no rounding, round to nearest
|
||||||
|
|
||||||
|
|
||||||
|
def apply_time_rounding(arrival_time, departure_time, user):
|
||||||
|
"""
|
||||||
|
Apply time rounding to arrival and departure times based on user settings.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
arrival_time (datetime): The original arrival time
|
||||||
|
departure_time (datetime): The original departure time
|
||||||
|
user: The user object
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple: (rounded_arrival_time, rounded_departure_time)
|
||||||
|
"""
|
||||||
|
interval_minutes, round_to_nearest = get_user_rounding_settings(user)
|
||||||
|
|
||||||
|
if interval_minutes == 0:
|
||||||
|
return arrival_time, departure_time # No rounding
|
||||||
|
|
||||||
|
# Round arrival time (typically round up to start billing later)
|
||||||
|
rounded_arrival = round_time_to_interval(arrival_time, interval_minutes, round_to_nearest)
|
||||||
|
|
||||||
|
# Round departure time (typically round up to bill more time)
|
||||||
|
rounded_departure = round_time_to_interval(departure_time, interval_minutes, not round_to_nearest)
|
||||||
|
|
||||||
|
# Ensure departure is still after arrival
|
||||||
|
if rounded_departure <= rounded_arrival:
|
||||||
|
# Add one interval to departure time
|
||||||
|
rounded_departure = rounded_departure + timedelta(minutes=interval_minutes)
|
||||||
|
|
||||||
|
return rounded_arrival, rounded_departure
|
||||||
|
|
||||||
|
|
||||||
|
def format_rounding_interval(interval_minutes):
|
||||||
|
"""
|
||||||
|
Format the rounding interval for display.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
interval_minutes (int): The interval in minutes
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Formatted interval description
|
||||||
|
"""
|
||||||
|
if interval_minutes == 0:
|
||||||
|
return "No rounding"
|
||||||
|
elif interval_minutes == 15:
|
||||||
|
return "15 minutes"
|
||||||
|
elif interval_minutes == 30:
|
||||||
|
return "30 minutes"
|
||||||
|
elif interval_minutes == 60:
|
||||||
|
return "1 hour"
|
||||||
|
else:
|
||||||
|
return f"{interval_minutes} minutes"
|
||||||
|
|
||||||
|
|
||||||
|
def get_available_rounding_options():
|
||||||
|
"""
|
||||||
|
Get the available time rounding options.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list: List of tuples (value, label)
|
||||||
|
"""
|
||||||
|
return [
|
||||||
|
(0, "No rounding"),
|
||||||
|
(15, "15 minutes"),
|
||||||
|
(30, "30 minutes"),
|
||||||
|
(60, "1 hour")
|
||||||
|
]
|
||||||
Reference in New Issue
Block a user