feat(programmer): day rows builder + work-log workDate
- Add workDate field to ProgrammerTaskWorkLog (defaults to Manila day of createdAt) - Add deltaJsonToPlainText function to quill_field.dart for Delta/plain text conversion - Implement buildDayRows function to generate day sheet rows with proper sorting - Implement DaySheetRow serialization and deserialization - Add comprehensive tests for day rows builder and workDate field - Verify backward compatibility via existing programmer_task_ledger_test Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
import 'package:tasq/models/programmer_task.model.dart';
|
||||
import 'package:tasq/models/programmer_task_activity_log.model.dart';
|
||||
import 'package:tasq/models/programmer_task_work_log.model.dart';
|
||||
import 'package:tasq/widgets/quill_field.dart';
|
||||
import 'programmer_daily_time.dart';
|
||||
|
||||
/// A single row in a programmer's day sheet (assignee or helper work).
|
||||
class DaySheetRow {
|
||||
final String taskId;
|
||||
final String title;
|
||||
final String category;
|
||||
final String kind; // 'assignee' or 'helper'
|
||||
final String? projectName;
|
||||
final String? helpedName;
|
||||
final int seconds;
|
||||
final List<String> notes;
|
||||
|
||||
DaySheetRow({
|
||||
required this.taskId,
|
||||
required this.title,
|
||||
required this.category,
|
||||
required this.kind,
|
||||
this.projectName,
|
||||
this.helpedName,
|
||||
required this.seconds,
|
||||
required this.notes,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'task_id': taskId,
|
||||
'title': title,
|
||||
'category': category,
|
||||
'project_name': projectName,
|
||||
'kind': kind,
|
||||
'helped_name': helpedName,
|
||||
'seconds': seconds,
|
||||
'notes': notes,
|
||||
};
|
||||
|
||||
factory DaySheetRow.fromJson(Map<String, dynamic> json) => DaySheetRow(
|
||||
taskId: json['task_id'] as String,
|
||||
title: json['title'] as String,
|
||||
category: json['category'] as String,
|
||||
kind: json['kind'] as String,
|
||||
projectName: json['project_name'] as String?,
|
||||
helpedName: json['helped_name'] as String?,
|
||||
seconds: json['seconds'] as int,
|
||||
notes: List<String>.from(json['notes'] as List? ?? []),
|
||||
);
|
||||
}
|
||||
|
||||
/// Build day rows for a programmer on a specific day.
|
||||
/// Returns rows for assignee tasks (by seconds desc) then helper tasks (by seconds desc).
|
||||
List<DaySheetRow> buildDayRows({
|
||||
required String userId,
|
||||
required DateTime day,
|
||||
required List<ProgrammerTask> tasks,
|
||||
required Map<String, List<ProgrammerTaskActivityLog>> logsByTask,
|
||||
required List<ProgrammerTaskWorkLog> workLogs,
|
||||
required Map<String, String> projectNames,
|
||||
required Map<String, String> profileNames,
|
||||
required DateTime now,
|
||||
}) {
|
||||
final taskMap = {for (final t in tasks) t.id: t};
|
||||
final workLogsByTaskAndAuthor = <String, List<ProgrammerTaskWorkLog>>{};
|
||||
|
||||
// Group work logs by task and author for quick lookup
|
||||
for (final log in workLogs) {
|
||||
final key = '${log.taskId}:${log.authorId}';
|
||||
workLogsByTaskAndAuthor.putIfAbsent(key, () => []).add(log);
|
||||
}
|
||||
|
||||
final rows = <DaySheetRow>[];
|
||||
|
||||
// Process each task
|
||||
for (final task in tasks) {
|
||||
if (!taskMap.containsKey(task.id)) continue;
|
||||
|
||||
final assigneeId = task.assigneeId;
|
||||
|
||||
// Compute daily seconds for this task
|
||||
final logs = logsByTask[task.id] ?? [];
|
||||
final dailyByAssignee = dailySecondsByAssignee(
|
||||
task: task,
|
||||
logs: logs,
|
||||
now: now,
|
||||
);
|
||||
final userDailySeconds = dailyByAssignee[userId] ?? {};
|
||||
final assigneeSeconds = userDailySeconds[day] ?? 0;
|
||||
|
||||
// Check for assignee row: has seconds that day OR authored notes-only log that day
|
||||
final assigneeLogsKey = '${task.id}:$userId';
|
||||
final assigneeLogsThisDay = workLogsByTaskAndAuthor[assigneeLogsKey]
|
||||
?.where((l) => l.workDate == day && l.minutes == null)
|
||||
.toList() ??
|
||||
[];
|
||||
|
||||
if (assigneeSeconds > 0 || assigneeLogsThisDay.isNotEmpty) {
|
||||
final notes = assigneeLogsThisDay
|
||||
.map((l) => deltaJsonToPlainText(l.description))
|
||||
.where((n) => n.isNotEmpty)
|
||||
.toList();
|
||||
|
||||
rows.add(DaySheetRow(
|
||||
taskId: task.id,
|
||||
title: task.title,
|
||||
category: task.category,
|
||||
kind: 'assignee',
|
||||
projectName: task.projectId != null ? projectNames[task.projectId] : null,
|
||||
helpedName: null,
|
||||
seconds: assigneeSeconds,
|
||||
notes: notes,
|
||||
));
|
||||
}
|
||||
|
||||
// Check for helper row: userId != assigneeId AND has minutes logs that day
|
||||
if (userId != assigneeId) {
|
||||
final helperLogsKey = '${task.id}:$userId';
|
||||
final helperLogs = workLogsByTaskAndAuthor[helperLogsKey]
|
||||
?.where((l) => l.workDate == day && l.minutes != null && l.minutes! > 0)
|
||||
.toList() ??
|
||||
[];
|
||||
|
||||
if (helperLogs.isNotEmpty) {
|
||||
final totalMinutes = helperLogs.fold<int>(0, (sum, l) => sum + (l.minutes ?? 0));
|
||||
final seconds = totalMinutes * 60;
|
||||
final notes = helperLogs
|
||||
.map((l) => deltaJsonToPlainText(l.description))
|
||||
.where((n) => n.isNotEmpty)
|
||||
.toList();
|
||||
|
||||
rows.add(DaySheetRow(
|
||||
taskId: task.id,
|
||||
title: task.title,
|
||||
category: task.category,
|
||||
kind: 'helper',
|
||||
projectName: task.projectId != null ? projectNames[task.projectId] : null,
|
||||
helpedName: assigneeId != null ? profileNames[assigneeId] : null,
|
||||
seconds: seconds,
|
||||
notes: notes,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort: assignee rows by seconds desc, then helper rows by seconds desc
|
||||
rows.sort((a, b) {
|
||||
if (a.kind != b.kind) {
|
||||
return a.kind == 'assignee' ? -1 : 1;
|
||||
}
|
||||
return b.seconds.compareTo(a.seconds);
|
||||
});
|
||||
|
||||
return rows;
|
||||
}
|
||||
|
||||
/// Calculate total seconds from day sheet rows.
|
||||
int totalSeconds(List<DaySheetRow> rows) {
|
||||
return rows.fold(0, (sum, row) => sum + row.seconds);
|
||||
}
|
||||
Reference in New Issue
Block a user