feat(report): accomplishment report data builder
Implement AccomplishmentReportData and buildAccomplishmentReportData() for generating programmer accomplishment reports. Supports both approved-only and include-unapproved modes, with proper row filtering, day aggregation, metrics calculation (totalSeconds, tasksCompleted, helpSeconds, secondsByCategory), and approver tracking. All 8 tests passing. Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
@@ -0,0 +1,610 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:tasq/models/programmer_day_sheet.model.dart';
|
||||
import 'package:tasq/models/programmer_task.model.dart';
|
||||
import 'package:tasq/models/profile.model.dart';
|
||||
import 'package:tasq/providers/reports_provider.dart';
|
||||
import 'package:tasq/screens/programmer_tasks/report/accomplishment_report_data.dart';
|
||||
import 'package:tasq/utils/programmer_day_rows.dart';
|
||||
|
||||
void main() {
|
||||
group('buildAccomplishmentReportData', () {
|
||||
final programmer = Profile(
|
||||
id: 'prog1',
|
||||
role: 'programmer',
|
||||
fullName: 'John Doe',
|
||||
);
|
||||
|
||||
final admin = Profile(
|
||||
id: 'admin1',
|
||||
role: 'administrator',
|
||||
fullName: 'Admin User',
|
||||
);
|
||||
|
||||
final reviewer = Profile(
|
||||
id: 'reviewer1',
|
||||
role: 'reviewer',
|
||||
fullName: 'Jane Reviewer',
|
||||
);
|
||||
|
||||
test('approved-only uses snapshots and counts excluded days', () {
|
||||
final day1 = DateTime.utc(2026, 9, 1);
|
||||
final day2 = DateTime.utc(2026, 9, 2);
|
||||
final day3 = DateTime.utc(2026, 9, 3);
|
||||
final day4 = DateTime.utc(2026, 9, 4);
|
||||
|
||||
final row1 = DaySheetRow(
|
||||
taskId: 't1',
|
||||
title: 'Task 1',
|
||||
category: 'Software Development',
|
||||
kind: 'assignee',
|
||||
seconds: 3600,
|
||||
notes: [],
|
||||
);
|
||||
|
||||
final row2 = DaySheetRow(
|
||||
taskId: 't2',
|
||||
title: 'Task 2',
|
||||
category: 'Bug Fix',
|
||||
kind: 'assignee',
|
||||
seconds: 1800,
|
||||
notes: [],
|
||||
);
|
||||
|
||||
// 2 approved sheets
|
||||
final sheets = [
|
||||
ProgrammerDaySheet(
|
||||
id: 'sheet1',
|
||||
programmerId: 'prog1',
|
||||
status: 'approved',
|
||||
workDate: day1,
|
||||
autoSubmitted: false,
|
||||
resubmissions: 0,
|
||||
reviewedBy: 'reviewer1',
|
||||
approvedSnapshot: DaySheetSnapshot(totalSeconds: 3600, rows: [row1]),
|
||||
createdAt: DateTime.utc(2026, 9, 1),
|
||||
updatedAt: DateTime.utc(2026, 9, 1),
|
||||
),
|
||||
ProgrammerDaySheet(
|
||||
id: 'sheet2',
|
||||
programmerId: 'prog1',
|
||||
status: 'approved',
|
||||
workDate: day2,
|
||||
autoSubmitted: false,
|
||||
resubmissions: 0,
|
||||
reviewedBy: 'reviewer1',
|
||||
approvedSnapshot: DaySheetSnapshot(totalSeconds: 1800, rows: [row2]),
|
||||
createdAt: DateTime.utc(2026, 9, 2),
|
||||
updatedAt: DateTime.utc(2026, 9, 2),
|
||||
),
|
||||
// 1 pending
|
||||
ProgrammerDaySheet(
|
||||
id: 'sheet3',
|
||||
programmerId: 'prog1',
|
||||
status: 'pending',
|
||||
workDate: day3,
|
||||
autoSubmitted: false,
|
||||
resubmissions: 0,
|
||||
createdAt: DateTime.utc(2026, 9, 3),
|
||||
updatedAt: DateTime.utc(2026, 9, 3),
|
||||
),
|
||||
// 1 disapproved
|
||||
ProgrammerDaySheet(
|
||||
id: 'sheet4',
|
||||
programmerId: 'prog1',
|
||||
status: 'disapproved',
|
||||
workDate: day4,
|
||||
autoSubmitted: false,
|
||||
resubmissions: 1,
|
||||
reviewedBy: 'reviewer1',
|
||||
createdAt: DateTime.utc(2026, 9, 4),
|
||||
updatedAt: DateTime.utc(2026, 9, 4),
|
||||
),
|
||||
];
|
||||
|
||||
final range = ReportDateRange(
|
||||
start: DateTime.utc(2026, 9, 1),
|
||||
end: DateTime.utc(2026, 9, 5),
|
||||
label: 'Test Range',
|
||||
);
|
||||
|
||||
final data = buildAccomplishmentReportData(
|
||||
programmer: programmer,
|
||||
range: range,
|
||||
includeUnapproved: false,
|
||||
sheets: sheets,
|
||||
liveRowsByDay: {},
|
||||
tasks: [],
|
||||
profileNames: {'reviewer1': 'Jane Reviewer'},
|
||||
);
|
||||
|
||||
expect(data.days.length, 2);
|
||||
expect(data.excludedPending, 1);
|
||||
expect(data.excludedDisapproved, 1);
|
||||
expect(data.days[0].statusTag, null);
|
||||
expect(data.days[1].statusTag, null);
|
||||
expect(data.totalSeconds, 5400);
|
||||
expect(data.daysReported, 2);
|
||||
});
|
||||
|
||||
test('include unapproved tags days and uses live rows', () {
|
||||
final day1 = DateTime.utc(2026, 9, 1);
|
||||
final day2 = DateTime.utc(2026, 9, 2);
|
||||
final day3 = DateTime.utc(2026, 9, 3);
|
||||
final day4 = DateTime.utc(2026, 9, 4);
|
||||
|
||||
final approvedRow = DaySheetRow(
|
||||
taskId: 't1',
|
||||
title: 'Task 1',
|
||||
category: 'Software Development',
|
||||
kind: 'assignee',
|
||||
seconds: 3600,
|
||||
notes: [],
|
||||
);
|
||||
|
||||
final pendingRow = DaySheetRow(
|
||||
taskId: 't2',
|
||||
title: 'Task 2',
|
||||
category: 'Bug Fix',
|
||||
kind: 'assignee',
|
||||
seconds: 1800,
|
||||
notes: [],
|
||||
);
|
||||
|
||||
final disapprovedRow = DaySheetRow(
|
||||
taskId: 't3',
|
||||
title: 'Task 3',
|
||||
category: 'Meeting',
|
||||
kind: 'assignee',
|
||||
seconds: 900,
|
||||
notes: [],
|
||||
);
|
||||
|
||||
final draftRow = DaySheetRow(
|
||||
taskId: 't4',
|
||||
title: 'Task 4',
|
||||
category: 'Software Support',
|
||||
kind: 'assignee',
|
||||
seconds: 600,
|
||||
notes: [],
|
||||
);
|
||||
|
||||
final sheets = [
|
||||
// approved
|
||||
ProgrammerDaySheet(
|
||||
id: 'sheet1',
|
||||
programmerId: 'prog1',
|
||||
status: 'approved',
|
||||
workDate: day1,
|
||||
autoSubmitted: false,
|
||||
resubmissions: 0,
|
||||
reviewedBy: 'reviewer1',
|
||||
approvedSnapshot:
|
||||
DaySheetSnapshot(totalSeconds: 3600, rows: [approvedRow]),
|
||||
createdAt: DateTime.utc(2026, 9, 1),
|
||||
updatedAt: DateTime.utc(2026, 9, 1),
|
||||
),
|
||||
// pending
|
||||
ProgrammerDaySheet(
|
||||
id: 'sheet2',
|
||||
programmerId: 'prog1',
|
||||
status: 'pending',
|
||||
workDate: day2,
|
||||
autoSubmitted: false,
|
||||
resubmissions: 0,
|
||||
createdAt: DateTime.utc(2026, 9, 2),
|
||||
updatedAt: DateTime.utc(2026, 9, 2),
|
||||
),
|
||||
// disapproved
|
||||
ProgrammerDaySheet(
|
||||
id: 'sheet3',
|
||||
programmerId: 'prog1',
|
||||
status: 'disapproved',
|
||||
workDate: day3,
|
||||
autoSubmitted: false,
|
||||
resubmissions: 1,
|
||||
reviewedBy: 'reviewer1',
|
||||
createdAt: DateTime.utc(2026, 9, 3),
|
||||
updatedAt: DateTime.utc(2026, 9, 3),
|
||||
),
|
||||
// draft
|
||||
ProgrammerDaySheet(
|
||||
id: 'sheet4',
|
||||
programmerId: 'prog1',
|
||||
status: 'draft',
|
||||
workDate: day4,
|
||||
autoSubmitted: false,
|
||||
resubmissions: 0,
|
||||
createdAt: DateTime.utc(2026, 9, 4),
|
||||
updatedAt: DateTime.utc(2026, 9, 4),
|
||||
),
|
||||
];
|
||||
|
||||
final range = ReportDateRange(
|
||||
start: DateTime.utc(2026, 9, 1),
|
||||
end: DateTime.utc(2026, 9, 5),
|
||||
label: 'Test Range',
|
||||
);
|
||||
|
||||
final liveRowsByDay = {
|
||||
day2: [pendingRow],
|
||||
day3: [disapprovedRow],
|
||||
day4: [draftRow],
|
||||
};
|
||||
|
||||
final data = buildAccomplishmentReportData(
|
||||
programmer: programmer,
|
||||
range: range,
|
||||
includeUnapproved: true,
|
||||
sheets: sheets,
|
||||
liveRowsByDay: liveRowsByDay,
|
||||
tasks: [],
|
||||
profileNames: {'reviewer1': 'Jane Reviewer'},
|
||||
);
|
||||
|
||||
expect(data.days.length, 4);
|
||||
expect(data.excludedPending, 0);
|
||||
expect(data.excludedDisapproved, 0);
|
||||
expect(data.days[0].statusTag, null); // approved
|
||||
expect(data.days[1].statusTag, 'Pending'); // pending
|
||||
expect(data.days[2].statusTag, 'Disapproved'); // disapproved
|
||||
expect(data.days[3].statusTag, 'Pending'); // draft
|
||||
});
|
||||
|
||||
test('period end is inclusive', () {
|
||||
final day1 = DateTime.utc(2026, 9, 1);
|
||||
|
||||
final row = DaySheetRow(
|
||||
taskId: 't1',
|
||||
title: 'Task 1',
|
||||
category: 'Software Development',
|
||||
kind: 'assignee',
|
||||
seconds: 3600,
|
||||
notes: [],
|
||||
);
|
||||
|
||||
final sheets = [
|
||||
ProgrammerDaySheet(
|
||||
id: 'sheet1',
|
||||
programmerId: 'prog1',
|
||||
status: 'approved',
|
||||
workDate: day1,
|
||||
autoSubmitted: false,
|
||||
resubmissions: 0,
|
||||
reviewedBy: 'reviewer1',
|
||||
approvedSnapshot: DaySheetSnapshot(totalSeconds: 3600, rows: [row]),
|
||||
createdAt: DateTime.utc(2026, 9, 1),
|
||||
updatedAt: DateTime.utc(2026, 9, 1),
|
||||
),
|
||||
];
|
||||
|
||||
final range = ReportDateRange(
|
||||
start: DateTime.utc(2026, 9, 1),
|
||||
end: DateTime.utc(2026, 10, 1), // exclusive
|
||||
label: 'Test Range',
|
||||
);
|
||||
|
||||
final data = buildAccomplishmentReportData(
|
||||
programmer: programmer,
|
||||
range: range,
|
||||
includeUnapproved: false,
|
||||
sheets: sheets,
|
||||
liveRowsByDay: {},
|
||||
tasks: [],
|
||||
profileNames: {'reviewer1': 'Jane Reviewer'},
|
||||
);
|
||||
|
||||
expect(data.periodStart, DateTime.utc(2026, 9, 1));
|
||||
expect(data.periodEndInclusive, DateTime.utc(2026, 9, 30));
|
||||
});
|
||||
|
||||
test('approver name only when one reviewer approved every included day', () {
|
||||
final day1 = DateTime.utc(2026, 9, 1);
|
||||
final day2 = DateTime.utc(2026, 9, 2);
|
||||
final day3 = DateTime.utc(2026, 9, 3);
|
||||
|
||||
final row = DaySheetRow(
|
||||
taskId: 't1',
|
||||
title: 'Task 1',
|
||||
category: 'Software Development',
|
||||
kind: 'assignee',
|
||||
seconds: 3600,
|
||||
notes: [],
|
||||
);
|
||||
|
||||
// Test: same reviewer for all approved days
|
||||
final sheets1 = [
|
||||
ProgrammerDaySheet(
|
||||
id: 'sheet1',
|
||||
programmerId: 'prog1',
|
||||
status: 'approved',
|
||||
workDate: day1,
|
||||
autoSubmitted: false,
|
||||
resubmissions: 0,
|
||||
reviewedBy: 'reviewer1',
|
||||
approvedSnapshot: DaySheetSnapshot(totalSeconds: 3600, rows: [row]),
|
||||
createdAt: DateTime.utc(2026, 9, 1),
|
||||
updatedAt: DateTime.utc(2026, 9, 1),
|
||||
),
|
||||
ProgrammerDaySheet(
|
||||
id: 'sheet2',
|
||||
programmerId: 'prog1',
|
||||
status: 'approved',
|
||||
workDate: day2,
|
||||
autoSubmitted: false,
|
||||
resubmissions: 0,
|
||||
reviewedBy: 'reviewer1',
|
||||
approvedSnapshot: DaySheetSnapshot(totalSeconds: 3600, rows: [row]),
|
||||
createdAt: DateTime.utc(2026, 9, 2),
|
||||
updatedAt: DateTime.utc(2026, 9, 2),
|
||||
),
|
||||
];
|
||||
|
||||
final range = ReportDateRange(
|
||||
start: DateTime.utc(2026, 9, 1),
|
||||
end: DateTime.utc(2026, 9, 4),
|
||||
label: 'Test Range',
|
||||
);
|
||||
|
||||
final data1 = buildAccomplishmentReportData(
|
||||
programmer: programmer,
|
||||
range: range,
|
||||
includeUnapproved: false,
|
||||
sheets: sheets1,
|
||||
liveRowsByDay: {},
|
||||
tasks: [],
|
||||
profileNames: {'reviewer1': 'Jane Reviewer'},
|
||||
);
|
||||
|
||||
expect(data1.approverName, 'Jane Reviewer');
|
||||
|
||||
// Test: mixed reviewers
|
||||
final sheets2 = [
|
||||
ProgrammerDaySheet(
|
||||
id: 'sheet1',
|
||||
programmerId: 'prog1',
|
||||
status: 'approved',
|
||||
workDate: day1,
|
||||
autoSubmitted: false,
|
||||
resubmissions: 0,
|
||||
reviewedBy: 'reviewer1',
|
||||
approvedSnapshot: DaySheetSnapshot(totalSeconds: 3600, rows: [row]),
|
||||
createdAt: DateTime.utc(2026, 9, 1),
|
||||
updatedAt: DateTime.utc(2026, 9, 1),
|
||||
),
|
||||
ProgrammerDaySheet(
|
||||
id: 'sheet2',
|
||||
programmerId: 'prog1',
|
||||
status: 'approved',
|
||||
workDate: day2,
|
||||
autoSubmitted: false,
|
||||
resubmissions: 0,
|
||||
reviewedBy: 'reviewer2',
|
||||
approvedSnapshot: DaySheetSnapshot(totalSeconds: 3600, rows: [row]),
|
||||
createdAt: DateTime.utc(2026, 9, 2),
|
||||
updatedAt: DateTime.utc(2026, 9, 2),
|
||||
),
|
||||
];
|
||||
|
||||
final data2 = buildAccomplishmentReportData(
|
||||
programmer: programmer,
|
||||
range: range,
|
||||
includeUnapproved: false,
|
||||
sheets: sheets2,
|
||||
liveRowsByDay: {},
|
||||
tasks: [],
|
||||
profileNames: {'reviewer1': 'Jane Reviewer', 'reviewer2': 'Bob Reviewer'},
|
||||
);
|
||||
|
||||
expect(data2.approverName, null);
|
||||
});
|
||||
|
||||
test('tasksCompleted counts tasks completed in range for the programmer', () {
|
||||
final range = ReportDateRange(
|
||||
start: DateTime.utc(2026, 9, 1),
|
||||
end: DateTime.utc(2026, 9, 5),
|
||||
label: 'Test Range',
|
||||
);
|
||||
|
||||
final task1 = ProgrammerTask(
|
||||
id: 't1',
|
||||
title: 'Task 1',
|
||||
category: 'Software Development',
|
||||
status: 'completed',
|
||||
assigneeId: 'prog1',
|
||||
createdAt: DateTime.utc(2026, 9, 1),
|
||||
completedAt: DateTime.utc(2026, 9, 3), // in range
|
||||
updatedAt: DateTime.utc(2026, 9, 3),
|
||||
);
|
||||
|
||||
final task2 = ProgrammerTask(
|
||||
id: 't2',
|
||||
title: 'Task 2',
|
||||
category: 'Bug Fix',
|
||||
status: 'completed',
|
||||
assigneeId: 'prog1',
|
||||
createdAt: DateTime.utc(2026, 9, 1),
|
||||
completedAt: DateTime.utc(2026, 8, 31), // before range
|
||||
updatedAt: DateTime.utc(2026, 8, 31),
|
||||
);
|
||||
|
||||
final task3 = ProgrammerTask(
|
||||
id: 't3',
|
||||
title: 'Task 3',
|
||||
category: 'Meeting',
|
||||
status: 'completed',
|
||||
assigneeId: 'prog2', // different assignee
|
||||
createdAt: DateTime.utc(2026, 9, 1),
|
||||
completedAt: DateTime.utc(2026, 9, 3),
|
||||
updatedAt: DateTime.utc(2026, 9, 3),
|
||||
);
|
||||
|
||||
final task4 = ProgrammerTask(
|
||||
id: 't4',
|
||||
title: 'Task 4',
|
||||
category: 'Software Support',
|
||||
status: 'in_progress', // not completed
|
||||
assigneeId: 'prog1',
|
||||
createdAt: DateTime.utc(2026, 9, 1),
|
||||
updatedAt: DateTime.utc(2026, 9, 3),
|
||||
);
|
||||
|
||||
final task5 = ProgrammerTask(
|
||||
id: 't5',
|
||||
title: 'Task 5',
|
||||
category: 'Software Development',
|
||||
status: 'completed',
|
||||
assigneeId: 'prog1',
|
||||
createdAt: DateTime.utc(2026, 9, 1),
|
||||
completedAt: DateTime.utc(2026, 9, 5), // at end boundary (exclusive)
|
||||
updatedAt: DateTime.utc(2026, 9, 5),
|
||||
);
|
||||
|
||||
final data = buildAccomplishmentReportData(
|
||||
programmer: programmer,
|
||||
range: range,
|
||||
includeUnapproved: false,
|
||||
sheets: [],
|
||||
liveRowsByDay: {},
|
||||
tasks: [task1, task2, task3, task4, task5],
|
||||
profileNames: {},
|
||||
);
|
||||
|
||||
expect(data.tasksCompleted, 1); // only task1
|
||||
});
|
||||
|
||||
test('empty period', () {
|
||||
final range = ReportDateRange(
|
||||
start: DateTime.utc(2026, 9, 1),
|
||||
end: DateTime.utc(2026, 9, 2),
|
||||
label: 'Test Range',
|
||||
);
|
||||
|
||||
final data = buildAccomplishmentReportData(
|
||||
programmer: programmer,
|
||||
range: range,
|
||||
includeUnapproved: false,
|
||||
sheets: [],
|
||||
liveRowsByDay: {},
|
||||
tasks: [],
|
||||
profileNames: {},
|
||||
);
|
||||
|
||||
expect(data.days, isEmpty);
|
||||
expect(data.daysReported, 0);
|
||||
expect(data.totalSeconds, 0);
|
||||
expect(data.excludedPending, 0);
|
||||
expect(data.excludedDisapproved, 0);
|
||||
});
|
||||
|
||||
test('positionLabel based on programmer role', () {
|
||||
final range = ReportDateRange(
|
||||
start: DateTime.utc(2026, 9, 1),
|
||||
end: DateTime.utc(2026, 9, 2),
|
||||
label: 'Test Range',
|
||||
);
|
||||
|
||||
final dataProgrammer = buildAccomplishmentReportData(
|
||||
programmer: programmer,
|
||||
range: range,
|
||||
includeUnapproved: false,
|
||||
sheets: [],
|
||||
liveRowsByDay: {},
|
||||
tasks: [],
|
||||
profileNames: {},
|
||||
);
|
||||
|
||||
final dataAdmin = buildAccomplishmentReportData(
|
||||
programmer: admin,
|
||||
range: range,
|
||||
includeUnapproved: false,
|
||||
sheets: [],
|
||||
liveRowsByDay: {},
|
||||
tasks: [],
|
||||
profileNames: {},
|
||||
);
|
||||
|
||||
expect(dataProgrammer.positionLabel, 'Programmer');
|
||||
expect(dataAdmin.positionLabel, 'Administrator');
|
||||
});
|
||||
|
||||
test('helpSeconds and secondsByCategory calculations', () {
|
||||
final day1 = DateTime.utc(2026, 9, 1);
|
||||
|
||||
final assigneeRow = DaySheetRow(
|
||||
taskId: 't1',
|
||||
title: 'Task 1',
|
||||
category: 'Software Development',
|
||||
kind: 'assignee',
|
||||
seconds: 3600,
|
||||
notes: [],
|
||||
);
|
||||
|
||||
final helperRow = DaySheetRow(
|
||||
taskId: 't2',
|
||||
title: 'Task 2',
|
||||
category: 'Bug Fix',
|
||||
kind: 'helper',
|
||||
seconds: 1800,
|
||||
notes: [],
|
||||
);
|
||||
|
||||
final anotherHelperRow = DaySheetRow(
|
||||
taskId: 't3',
|
||||
title: 'Task 3',
|
||||
category: 'Software Development',
|
||||
kind: 'helper',
|
||||
seconds: 900,
|
||||
notes: [],
|
||||
);
|
||||
|
||||
final row = DaySheetRow(
|
||||
taskId: 't4',
|
||||
title: 'Task 4',
|
||||
category: 'Meeting',
|
||||
kind: 'assignee',
|
||||
seconds: 600,
|
||||
notes: [],
|
||||
);
|
||||
|
||||
final sheets = [
|
||||
ProgrammerDaySheet(
|
||||
id: 'sheet1',
|
||||
programmerId: 'prog1',
|
||||
status: 'approved',
|
||||
workDate: day1,
|
||||
autoSubmitted: false,
|
||||
resubmissions: 0,
|
||||
reviewedBy: 'reviewer1',
|
||||
approvedSnapshot: DaySheetSnapshot(
|
||||
totalSeconds: 6900,
|
||||
rows: [assigneeRow, helperRow, anotherHelperRow, row],
|
||||
),
|
||||
createdAt: DateTime.utc(2026, 9, 1),
|
||||
updatedAt: DateTime.utc(2026, 9, 1),
|
||||
),
|
||||
];
|
||||
|
||||
final range = ReportDateRange(
|
||||
start: DateTime.utc(2026, 9, 1),
|
||||
end: DateTime.utc(2026, 9, 2),
|
||||
label: 'Test Range',
|
||||
);
|
||||
|
||||
final data = buildAccomplishmentReportData(
|
||||
programmer: programmer,
|
||||
range: range,
|
||||
includeUnapproved: false,
|
||||
sheets: sheets,
|
||||
liveRowsByDay: {},
|
||||
tasks: [],
|
||||
profileNames: {'reviewer1': 'Jane Reviewer'},
|
||||
);
|
||||
|
||||
expect(data.helpSeconds, 2700); // 1800 + 900
|
||||
expect(data.secondsByCategory['Software Development'], 4500); // 3600 + 900
|
||||
expect(data.secondsByCategory['Bug Fix'], 1800);
|
||||
expect(data.secondsByCategory['Meeting'], 600);
|
||||
expect(data.totalSeconds, 6900);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user