diff --git a/AndroidApp/app/src/main/java/calvin/erfmann/opentimetracker/data/Models.kt b/AndroidApp/app/src/main/java/calvin/erfmann/opentimetracker/data/Models.kt index b2da0e7..03cb4ff 100644 --- a/AndroidApp/app/src/main/java/calvin/erfmann/opentimetracker/data/Models.kt +++ b/AndroidApp/app/src/main/java/calvin/erfmann/opentimetracker/data/Models.kt @@ -7,7 +7,6 @@ import kotlinx.serialization.Serializable - @Serializable data class AuthResponse( val token: String diff --git a/AndroidApp/app/src/main/java/calvin/erfmann/opentimetracker/ui/DashboardViewModel.kt b/AndroidApp/app/src/main/java/calvin/erfmann/opentimetracker/ui/DashboardViewModel.kt index d2aaefd..dcfb3d2 100644 --- a/AndroidApp/app/src/main/java/calvin/erfmann/opentimetracker/ui/DashboardViewModel.kt +++ b/AndroidApp/app/src/main/java/calvin/erfmann/opentimetracker/ui/DashboardViewModel.kt @@ -10,6 +10,9 @@ import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import java.time.Duration import java.time.Instant +import java.time.LocalDateTime +import java.time.ZoneOffset +import java.time.format.DateTimeFormatter class DashboardViewModel(private val api: ApiService) : ViewModel() { @@ -39,21 +42,38 @@ class DashboardViewModel(private val api: ApiService) : ViewModel() { private fun startLocalTimer() { viewModelScope.launch { while (isActive) { - state.value?.entry?.startTime?.let { isoTime -> + val currentEntry = state.value?.entry + if (state.value?.isTracking == true && currentEntry != null) { try { - // Assuming ISO format like "2023-10-27T10:00:00Z" - val start = Instant.parse(isoTime) - val now = Instant.now() - val seconds = Duration.between(start, now).seconds - if (seconds >= 0) { - val h = seconds / 3600 - val m = (seconds % 3600) / 60 - val s = seconds % 60 - elapsedTime.value = String.format("%02d:%02d:%02d", h, m, s) + val startTimeStr = currentEntry.startTime + + // Robust Parsing: Try Instant first, fallback to LocalDateTime assuming System Default Zone + val startInstant = try { + Instant.parse(startTimeStr) + } catch (e: Exception) { + // CHANGE: Use systemDefault() instead of UTC. + // If server sends "15:51" and it is 15:51 on your clock, this matches the timeline correctly. + LocalDateTime.parse(startTimeStr) + .atZone(java.time.ZoneId.systemDefault()) + .toInstant() } + + val now = Instant.now() + var diffSeconds = Duration.between(startInstant, now).seconds + + // Prevent negative time display due to small clock skews + if (diffSeconds < 0) diffSeconds = 0 + + val h = diffSeconds / 3600 + val m = (diffSeconds % 3600) / 60 + val s = diffSeconds % 60 + elapsedTime.value = String.format("%02d:%02d:%02d", h, m, s) } catch (e: Exception) { + e.printStackTrace() elapsedTime.value = "--:--" } + } else { + elapsedTime.value = "00:00:00" } delay(1000) } @@ -62,10 +82,10 @@ class DashboardViewModel(private val api: ApiService) : ViewModel() { fun startActivity(activityId: String) { viewModelScope.launch { - // Optimistic UI Update possible here try { api.startActivity(activityId) - // Polling will update the full state shortly + // Refresh immediately instead of waiting for the next poll + state.value = api.getStatus() } catch (e: Exception) { e.printStackTrace() } @@ -76,7 +96,7 @@ class DashboardViewModel(private val api: ApiService) : ViewModel() { viewModelScope.launch { try { api.stopActivity() - // Explicitly refresh state immediately for better UX + // Refresh immediately state.value = api.getStatus() } catch (e: Exception) { e.printStackTrace() @@ -84,4 +104,3 @@ class DashboardViewModel(private val api: ApiService) : ViewModel() { } } } - diff --git a/app.py b/app.py index bb2a5f7..5d11e2c 100644 --- a/app.py +++ b/app.py @@ -257,7 +257,7 @@ def start_timer_bg(activity_id): 'time_entry_id': new_entry_id, 'status': 'open', 'is_template': False, - 'source': 'auto', # Mark as auto-generated + 'source': 'auto', 'created_at': datetime.now(), 'comments': [] }) @@ -886,6 +886,116 @@ def api_status(user_id): } for a in activities] }) +# --- API Timer Controls (Token Based) --- + +@app.route('/api/timer/start/', methods=['POST']) +@token_required +def api_start_timer(user_id, activity_id): + # Stop ANY running timer + db.time_entries.update_many( + {'user_id': user_id, 'end_time': None}, + {'$set': {'end_time': datetime.now()}} + ) + + # Start new + start_time = datetime.now() + new_entry_id = db.time_entries.insert_one({ + 'user_id': user_id, + 'activity_id': ObjectId(activity_id), + 'start_time': start_time, + 'end_time': None, + 'note': '', + 'subcategory': '' + }).inserted_id + + # Auto-add templates + templates = db.tasks.find({ + 'user_id': user_id, + 'activity_id': ObjectId(activity_id), + 'is_template': True + }) + + for t in templates: + db.tasks.insert_one({ + 'user_id': user_id, + 'name': t['name'], + 'activity_id': ObjectId(activity_id), + 'time_entry_id': new_entry_id, + 'status': 'open', + 'is_template': False, + 'source': 'auto', + 'created_at': datetime.now(), + 'comments': [] + }) + + activity = db.activities.find_one({'_id': ObjectId(activity_id)}) + + return jsonify({ + 'status': 'success', + 'entry_id': str(new_entry_id), + 'start_time': start_time.isoformat(), + 'activity_name': activity['name'], + 'activity_color': activity.get('color', '#3498db') + }) + +@app.route('/api/timer/stop', methods=['POST']) +@token_required +def api_stop_timer(user_id): + result = db.time_entries.update_many( + {'user_id': user_id, 'end_time': None}, + {'$set': {'end_time': datetime.now()}} + ) + return jsonify({'status': 'stopped', 'modified': result.modified_count}) + +@app.route('/api/timer/update', methods=['POST']) +@token_required +def api_update_active_entry(user_id): + data = request.get_json() + fields = {} + if 'note' in data: fields['note'] = data['note'] + if 'subcategory' in data: fields['subcategory'] = data['subcategory'] + + if not fields: + return jsonify({'message': 'No fields to update'}), 400 + + db.time_entries.update_one( + {'user_id': user_id, 'end_time': None}, + {'$set': fields} + ) + return jsonify({'status': 'updated'}) + +@app.route('/api/tasks/toggle', methods=['POST']) +@token_required +def api_toggle_task(user_id): + data = request.get_json() + task_id = data.get('task_id') + is_checked = data.get('is_checked') + + if not task_id: return jsonify({'message': 'task_id required'}), 400 + + status = 'completed' if is_checked else 'open' + completed_at = datetime.now() if is_checked else None + + update_doc = { + 'status': status, + 'completed_at': completed_at + } + + # Helper: Link to active timer if checking off + if is_checked: + current_entry = db.time_entries.find_one({ + 'user_id': user_id, + 'end_time': None + }) + if current_entry: + update_doc['time_entry_id'] = current_entry['_id'] + + db.tasks.update_one( + {'_id': ObjectId(task_id), 'user_id': user_id}, + {'$set': update_doc} + ) + return jsonify({'status': 'success'}) + # --- Activity Management API --- @app.route('/api/activities/create', methods=['POST'])