feat(programmer): dated work-log corrections

Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
2026-09-28 14:08:28 +08:00
parent cc8d8ab8bf
commit be16a3cd0d
4 changed files with 85 additions and 13 deletions
@@ -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<void> 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));
}
}
}
@@ -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<WorkLogSection> createState() => _WorkLogSectionState();
@@ -68,15 +72,18 @@ class _WorkLogSectionState extends ConsumerState<WorkLogSection> {
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<WorkLogSection> {
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
+29
View File
@@ -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);
});
});
}
@@ -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);