mirror of
https://github.com/DRYTRIX/TimeTracker.git
synced 2026-05-18 20:29:44 -05:00
feat(dashboard): add pause, resume, and time adjustment to timer widget
- Add Pause and Stop buttons when a timer is running; Pause saves the segment so users can resume later without losing context. - When no timer is active, show prominent 'Resume (project name)' to restart with the same project/task/notes as the last entry. - Add quick time adjustment (-15 / -5 / +5 / +15 min) for the active timer via POST /timer/adjust (delta_minutes); limits ±4 hours. - Update CHANGELOG, in-app Help, GETTING_STARTED, and FEATURES_COMPLETE to document the new dashboard timer behavior.
This commit is contained in:
@@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
- **Dashboard timer widget** — Pause and Stop buttons while a timer is running (Pause saves the segment so you can resume later). When no timer is active, a prominent "Resume (project name)" button restarts tracking with the same project/task/notes as your last entry. Quick time adjustment buttons (−15 / −5 / +5 / +15 minutes) let you correct the current session without leaving the dashboard. New route `POST /timer/adjust` for start-time adjustment.
|
||||
|
||||
### Changed
|
||||
- **Log Time Manually page** — Redesigned for a more professional layout: form grouped into sections (Project & task, Date & time, Details) with clear headings and icons; main card uses rounded-xl and shadow-lg; unified label and helper text styling; primary "Log Time" and secondary "Clear" buttons aligned with dashboard button styles; duplicate-entry banner uses rounded-xl.
|
||||
|
||||
|
||||
@@ -538,6 +538,49 @@ def stop_timer():
|
||||
return redirect(url_for("main.dashboard"))
|
||||
|
||||
|
||||
@timer_bp.route("/timer/adjust", methods=["POST"])
|
||||
@login_required
|
||||
def adjust_timer():
|
||||
"""Adjust the active timer's start time by delta_minutes (positive = add time, negative = subtract)."""
|
||||
active_timer = current_user.active_timer
|
||||
if not active_timer:
|
||||
flash(_("No active timer to adjust"), "error")
|
||||
return redirect(url_for("main.dashboard"))
|
||||
|
||||
try:
|
||||
delta_minutes = int(request.form.get("delta_minutes", 0))
|
||||
except (TypeError, ValueError):
|
||||
flash(_("Invalid adjustment value"), "error")
|
||||
return redirect(url_for("main.dashboard"))
|
||||
|
||||
if delta_minutes == 0:
|
||||
return redirect(url_for("main.dashboard"))
|
||||
|
||||
# Clamp to avoid extreme shifts (e.g. ±4 hours)
|
||||
delta_minutes = max(-240, min(240, delta_minutes))
|
||||
from app.models.time_entry import local_now
|
||||
|
||||
new_start = active_timer.start_time - timedelta(minutes=delta_minutes)
|
||||
# Do not set start_time in the future
|
||||
now_local = local_now()
|
||||
if new_start > now_local:
|
||||
new_start = now_local
|
||||
active_timer.start_time = new_start
|
||||
active_timer.updated_at = now_local
|
||||
db.session.commit()
|
||||
|
||||
try:
|
||||
from app.utils.cache import get_cache
|
||||
cache = get_cache()
|
||||
cache.delete(f"dashboard:{current_user.id}")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if request.headers.get("X-Requested-With") == "XMLHttpRequest":
|
||||
return jsonify({"success": True, "start_time": active_timer.start_time.isoformat()})
|
||||
return redirect(url_for("main.dashboard"))
|
||||
|
||||
|
||||
@timer_bp.route("/timer/status")
|
||||
@login_required
|
||||
def timer_status():
|
||||
|
||||
@@ -128,18 +128,61 @@
|
||||
<p class="text-sm font-semibold text-text-light dark:text-text-dark mt-2">
|
||||
{{ _('Elapsed') }}: <span id="dashboard-timer-elapsed">{{ active_timer.duration_formatted }}</span>
|
||||
</p>
|
||||
<div class="mt-3 flex flex-wrap items-center gap-2">
|
||||
<span class="text-xs text-text-muted-light dark:text-text-muted-dark">{{ _('Adjust time') }}:</span>
|
||||
<form action="{{ url_for('timer.adjust_timer') }}" method="POST" class="inline">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<input type="hidden" name="delta_minutes" value="-15">
|
||||
<button type="submit" class="inline-flex items-center justify-center w-8 h-8 rounded-lg bg-gray-100 hover:bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-600 text-text-light dark:text-text-dark text-sm font-medium transition-colors" title="{{ _('Subtract 15 min') }}">−15</button>
|
||||
</form>
|
||||
<form action="{{ url_for('timer.adjust_timer') }}" method="POST" class="inline">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<input type="hidden" name="delta_minutes" value="-5">
|
||||
<button type="submit" class="inline-flex items-center justify-center w-8 h-8 rounded-lg bg-gray-100 hover:bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-600 text-text-light dark:text-text-dark text-sm font-medium transition-colors" title="{{ _('Subtract 5 min') }}">−5</button>
|
||||
</form>
|
||||
<form action="{{ url_for('timer.adjust_timer') }}" method="POST" class="inline">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<input type="hidden" name="delta_minutes" value="5">
|
||||
<button type="submit" class="inline-flex items-center justify-center w-8 h-8 rounded-lg bg-gray-100 hover:bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-600 text-text-light dark:text-text-dark text-sm font-medium transition-colors" title="{{ _('Add 5 min') }}">+5</button>
|
||||
</form>
|
||||
<form action="{{ url_for('timer.adjust_timer') }}" method="POST" class="inline">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<input type="hidden" name="delta_minutes" value="15">
|
||||
<button type="submit" class="inline-flex items-center justify-center w-8 h-8 rounded-lg bg-gray-100 hover:bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-600 text-text-light dark:text-text-dark text-sm font-medium transition-colors" title="{{ _('Add 15 min') }}">+15</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col sm:flex-row gap-2">
|
||||
<form action="{{ url_for('timer.stop_timer') }}" method="POST" class="inline">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="bg-amber-500 hover:bg-amber-600 text-white px-5 py-2.5 rounded-lg font-semibold shadow-lg hover:shadow-xl transition-all duration-200 transform hover:-translate-y-0.5" title="{{ _('Pause and save current time; you can resume later') }}">
|
||||
<i class="fas fa-pause mr-2"></i>{{ _('Pause') }}
|
||||
</button>
|
||||
</form>
|
||||
<form action="{{ url_for('timer.stop_timer') }}" method="POST" class="inline">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="bg-red-500 hover:bg-red-600 text-white px-5 py-2.5 rounded-lg font-semibold shadow-lg hover:shadow-xl transition-all duration-200 transform hover:-translate-y-0.5">
|
||||
<i class="fas fa-stop mr-2"></i>{{ _('Stop') }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
<form action="{{ url_for('timer.stop_timer') }}" method="POST" class="inline">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="bg-red-500 hover:bg-red-600 text-white px-5 py-2.5 rounded-lg font-semibold shadow-lg hover:shadow-xl transition-all duration-200 transform hover:-translate-y-0.5">
|
||||
<i class="fas fa-stop mr-2"></i>{{ _('Stop Timer') }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-center py-4">
|
||||
<p class="text-text-muted-light dark:text-text-muted-dark mb-3">{{ _('No active timer.') }}</p>
|
||||
<p class="text-sm text-text-muted-light dark:text-text-muted-dark">{{ _('Click "Start Timer" to begin tracking your time.') }}</p>
|
||||
{% if recent_entries %}
|
||||
<p class="text-sm text-text-muted-light dark:text-text-muted-dark mb-3">{{ _('Resume your last session or start a new timer.') }}</p>
|
||||
<div class="flex flex-wrap justify-center gap-3">
|
||||
<a href="{{ url_for('timer.resume_timer', timer_id=recent_entries[0].id) }}" class="inline-flex items-center bg-green-500 hover:bg-green-600 text-white px-5 py-2.5 rounded-lg font-semibold shadow-lg hover:shadow-xl transition-all duration-200 transform hover:-translate-y-0.5">
|
||||
<i class="fas fa-play mr-2"></i>{{ _('Resume') }}{% if recent_entries[0].project %} ({{ recent_entries[0].project.name }}){% elif recent_entries[0].client %} ({{ recent_entries[0].client.name }}){% endif %}
|
||||
</a>
|
||||
<button type="button" id="openStartTimer" class="bg-primary hover:bg-primary-dark text-white px-5 py-2.5 rounded-lg font-semibold shadow-lg hover:shadow-xl transition-all duration-200 transform hover:-translate-y-0.5">
|
||||
<i class="fas fa-plus mr-2"></i>{{ _('Start new') }}
|
||||
</button>
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="text-sm text-text-muted-light dark:text-text-muted-dark">{{ _('Click "Start Timer" above to begin tracking your time.') }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
@@ -133,6 +133,9 @@
|
||||
<h4 class="font-semibold mb-2">{{ _('Timer Features') }}</h4>
|
||||
<ul class="space-y-1 text-text-muted-light dark:text-text-muted-dark">
|
||||
<li><i class="fas fa-check text-green-600 mr-2"></i>{{ _('Real-time duration display') }}</li>
|
||||
<li><i class="fas fa-check text-green-600 mr-2"></i>{{ _('Pause and Stop on the dashboard — Pause saves your time so you can resume later') }}</li>
|
||||
<li><i class="fas fa-check text-green-600 mr-2"></i>{{ _('Resume last session with one click (same project/task/notes)') }}</li>
|
||||
<li><i class="fas fa-check text-green-600 mr-2"></i>{{ _('Quick time adjustment (−15 / −5 / +5 / +15 min) while the timer is running') }}</li>
|
||||
<li><i class="fas fa-check text-green-600 mr-2"></i>{{ _('Continues running if browser closes') }}</li>
|
||||
<li><i class="fas fa-check text-green-600 mr-2"></i>{{ _('Automatic idle detection (configurable)') }}</li>
|
||||
<li><i class="fas fa-check text-green-600 mr-2"></i>{{ _('One active timer per user (configurable)') }}</li>
|
||||
|
||||
@@ -57,6 +57,7 @@ TimeTracker is a comprehensive, self-hosted time tracking and project management
|
||||
|
||||
#### 4. **Timer Management**
|
||||
- Start, stop, pause, and resume timers
|
||||
- **Dashboard timer widget**: Pause (saves segment) and Stop; one-click "Resume (project)" to continue with the same project/task/notes; quick time adjustment (−15 / −5 / +5 / +15 min) while running
|
||||
- Edit active timers
|
||||
- Delete timers
|
||||
- Timer history and audit trail
|
||||
|
||||
@@ -249,7 +249,7 @@ Break your project into manageable tasks:
|
||||
2. **Select a project** (and optionally a task)
|
||||
3. **Click Start** — the timer begins
|
||||
4. **Work on your task** — timer continues even if you close the browser
|
||||
5. **Click Stop** when finished — time entry is saved automatically
|
||||
5. Use **Pause** to save the segment and resume later, or **Stop** when finished. Use the **−15 / −5 / +5 / +15** buttons to adjust the current session time if needed.
|
||||
|
||||
**💡 Tip**: The timer runs on the server, so it keeps going even if you:
|
||||
- Close your browser
|
||||
|
||||
Reference in New Issue
Block a user