From be16a3cd0d56bdf144fcb0677a4df23343eb650c Mon Sep 17 00:00:00 2001 From: Marc Rejohn Castillano Date: Mon, 28 Sep 2026 14:08:28 +0800 Subject: [PATCH] feat(programmer): dated work-log corrections Co-Authored-By: claude-flow --- .../programmer_task_work_logs_provider.dart | 24 ++++++++++----- .../widgets/work_log_section.dart | 23 +++++++++++---- test/programmer_day_data_test.dart | 29 +++++++++++++++++++ test/programmer_tasks_controller_test.dart | 22 ++++++++++++++ 4 files changed, 85 insertions(+), 13 deletions(-) diff --git a/lib/providers/programmer_task_work_logs_provider.dart b/lib/providers/programmer_task_work_logs_provider.dart index 09f5f9e6..74c65967 100644 --- a/lib/providers/programmer_task_work_logs_provider.dart +++ b/lib/providers/programmer_task_work_logs_provider.dart @@ -1,6 +1,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../models/programmer_task_work_log.model.dart'; +import '../utils/programmer_daily_time.dart'; +import 'programmer_day_sheets_provider.dart'; import 'supabase_provider.dart'; /// Work-log entries for a task (newest first), streamed live. @@ -26,19 +28,27 @@ class ProgrammerTaskWorkLogsController { /// Adds a work-log entry. [minutes] is null for the assignee's progress notes /// (their time comes from the running clock) and set for a helper's - /// Time Consumed. + /// Time Consumed. [workDate] overrides the date the entry is attributed to; + /// when null the DB default applies (today's Manila date). Future addWorkLog({ required String taskId, required String description, int? minutes, + DateTime? workDate, }) async { final userId = _client.auth.currentUser?.id; if (userId == null) throw Exception('Not authenticated'); - await _client.from('programmer_task_work_logs').insert({ - 'task_id': taskId, - 'author_id': userId, - 'description': description, - 'minutes': ?minutes, - }); + try { + await _client.from('programmer_task_work_logs').insert({ + 'task_id': taskId, + 'author_id': userId, + 'description': description, + 'minutes': ?minutes, + if (workDate != null) 'work_date': formatDayKey(workDate), + }); + } catch (e) { + if (e is DaySheetActionException) rethrow; + throw DaySheetActionException(mapDaySheetError(e)); + } } } diff --git a/lib/screens/programmer_tasks/widgets/work_log_section.dart b/lib/screens/programmer_tasks/widgets/work_log_section.dart index 03095048..f3a48095 100644 --- a/lib/screens/programmer_tasks/widgets/work_log_section.dart +++ b/lib/screens/programmer_tasks/widgets/work_log_section.dart @@ -16,11 +16,15 @@ import '../programmer_task_prompts.dart'; /// a non-assignee helper gets Hours + Minutes fields and is prompted to deduct /// that time from their own running task. The assignee is prompted to pause /// after logging. +/// +/// When [workDate] is supplied the entry is a correction for a past day: +/// the header names that date and the pause/deduct prompts are skipped. class WorkLogSection extends ConsumerStatefulWidget { - const WorkLogSection({super.key, required this.task, required this.isAssignee}); + const WorkLogSection({super.key, required this.task, required this.isAssignee, this.workDate}); final ProgrammerTask task; final bool isAssignee; + final DateTime? workDate; @override ConsumerState createState() => _WorkLogSectionState(); @@ -68,15 +72,18 @@ class _WorkLogSectionState extends ConsumerState { taskId: widget.task.id, description: quillToDeltaJson(_descQuill), minutes: minutes, + workDate: widget.workDate, ); _descQuill.clear(); _hoursCtrl.clear(); _minutesCtrl.clear(); if (!mounted) return; - if (widget.isAssignee) { - await maybePromptPauseTimer(context, ref, widget.task); - } else if (minutes != null) { - await maybePromptDeductHelperTime(context, ref, minutes, widget.task.id); + if (widget.workDate == null) { + if (widget.isAssignee) { + await maybePromptPauseTimer(context, ref, widget.task); + } else if (minutes != null) { + await maybePromptDeductHelperTime(context, ref, minutes, widget.task.id); + } } } catch (e) { messenger.showSnackBar(SnackBar(content: Text('Could not save: $e'))); @@ -97,7 +104,11 @@ class _WorkLogSectionState extends ConsumerState { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const AppSectionHeader('Work Log'), + AppSectionHeader( + widget.workDate != null + ? 'Correction for ${AppTime.formatDate(widget.workDate!)}' + : 'Work Log', + ), QuillFieldEditor( controller: _descQuill, placeholder: widget.isAssignee diff --git a/test/programmer_day_data_test.dart b/test/programmer_day_data_test.dart index 8c18be12..4014494c 100644 --- a/test/programmer_day_data_test.dart +++ b/test/programmer_day_data_test.dart @@ -326,5 +326,34 @@ void main() { expect(result.workLogs, hasLength(1)); expect(result.workLogs.first.taskId, 'helper-task'); }); + + test('excludes work logs with a different work_date', () async { + final tTask = _task( + id: 'some-task', + status: ProgrammerTaskStatus.inProgress, + startedAt: DateTime.utc(2026, 9, 27, 0, 0), + ); + final client = _FakeClient(tables: { + 'programmer_task_work_logs': [ + { + 'id': 'wl-old', + 'task_id': 'some-task', + 'author_id': 'u1', + 'description': 'yesterday', + 'minutes': 60, + 'created_at': '2026-09-26T10:00:00Z', + 'work_date': '2026-09-26', + }, + ], + 'programmer_task_activity_logs': [], + }); + final result = await fetchDayActivityData( + client, + day: day, + tasks: [tTask], + now: now, + ); + expect(result.workLogs, isEmpty); + }); }); } diff --git a/test/programmer_tasks_controller_test.dart b/test/programmer_tasks_controller_test.dart index 05a84c2a..00197655 100644 --- a/test/programmer_tasks_controller_test.dart +++ b/test/programmer_tasks_controller_test.dart @@ -526,6 +526,28 @@ void main() { expect(rows[1].containsKey('minutes'), isFalse); // null minutes omitted }); + test('addWorkLog with workDate inserts work_date key', () async { + final fake = _FakeClient('u1'); + final wl = ProgrammerTaskWorkLogsController(fake as dynamic); + await wl.addWorkLog( + taskId: 't1', + description: 'correction', + workDate: DateTime.utc(2026, 9, 25), + ); + final rows = fake.tables['programmer_task_work_logs']!; + expect(rows.length, 1); + expect(rows.first['work_date'], '2026-09-25'); + }); + + test('addWorkLog without workDate omits work_date key', () async { + final fake = _FakeClient('u1'); + final wl = ProgrammerTaskWorkLogsController(fake as dynamic); + await wl.addWorkLog(taskId: 't1', description: 'progress'); + final rows = fake.tables['programmer_task_work_logs']!; + expect(rows.length, 1); + expect(rows.first.containsKey('work_date'), isFalse); + }); + test('addComment stores body + author with empty attachments', () async { final fake = _FakeClient('u1'); final cc = ProgrammerTaskCommentsController(fake as dynamic);