feat(programmer): day-sheet model + stats, streak and badges
Add ProgrammerDaySheet model with DaySheetStatus/DaySheetSnapshot, computeProgrammerStats with streak, first-pass rate, approved totals, hasCleanWeek, and tiered badge progress via computeBadges. 10 tests pass. Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
@@ -0,0 +1,264 @@
|
||||
import 'package:flutter/material.dart';
|
||||
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/utils/app_time.dart';
|
||||
import 'package:tasq/utils/programmer_stats.dart';
|
||||
|
||||
void main() {
|
||||
setUpAll(AppTime.initialize);
|
||||
|
||||
ProgrammerDaySheet makeSheet({
|
||||
String id = 'id',
|
||||
String programmerId = 'user1',
|
||||
String status = 'pending',
|
||||
required DateTime workDate,
|
||||
int resubmissions = 0,
|
||||
DaySheetSnapshot? approvedSnapshot,
|
||||
}) {
|
||||
return ProgrammerDaySheet(
|
||||
id: id,
|
||||
programmerId: programmerId,
|
||||
status: status,
|
||||
workDate: workDate,
|
||||
resubmissions: resubmissions,
|
||||
approvedSnapshot: approvedSnapshot,
|
||||
createdAt: DateTime.utc(2026, 1, 1),
|
||||
updatedAt: DateTime.utc(2026, 1, 1),
|
||||
);
|
||||
}
|
||||
|
||||
ProgrammerStats statsWith({
|
||||
int completedTotal = 0,
|
||||
int bugFixCompleted = 0,
|
||||
Duration approvedTotal = Duration.zero,
|
||||
Duration helpGivenTotal = Duration.zero,
|
||||
Duration helpGivenThisMonth = Duration.zero,
|
||||
Duration todayFocus = Duration.zero,
|
||||
int completedThisWeek = 0,
|
||||
int completedLastWeek = 0,
|
||||
int currentStreak = 0,
|
||||
int bestStreak = 0,
|
||||
double? firstPassRate,
|
||||
bool hasCleanWeek = false,
|
||||
}) {
|
||||
return ProgrammerStats(
|
||||
todayFocus: todayFocus,
|
||||
helpGivenThisMonth: helpGivenThisMonth,
|
||||
approvedTotal: approvedTotal,
|
||||
helpGivenTotal: helpGivenTotal,
|
||||
completedThisWeek: completedThisWeek,
|
||||
completedLastWeek: completedLastWeek,
|
||||
currentStreak: currentStreak,
|
||||
bestStreak: bestStreak,
|
||||
completedTotal: completedTotal,
|
||||
bugFixCompleted: bugFixCompleted,
|
||||
firstPassRate: firstPassRate,
|
||||
hasCleanWeek: hasCleanWeek,
|
||||
);
|
||||
}
|
||||
|
||||
test('fromMap parses work_date and snapshot', () {
|
||||
final ts = DateTime.utc(2026, 9, 28).toIso8601String();
|
||||
final m = <String, dynamic>{
|
||||
'id': 'ds1',
|
||||
'programmer_id': 'user1',
|
||||
'status': 'approved',
|
||||
'work_date': '2026-09-23',
|
||||
'submitted_at': null,
|
||||
'reviewed_at': null,
|
||||
'auto_submitted': false,
|
||||
'reviewed_by': null,
|
||||
'resubmissions': 0,
|
||||
'approved_snapshot': <String, dynamic>{
|
||||
'v': 1,
|
||||
'total_seconds': 3600,
|
||||
'rows': <dynamic>[],
|
||||
},
|
||||
'created_at': ts,
|
||||
'updated_at': ts,
|
||||
};
|
||||
final sheet = ProgrammerDaySheet.fromMap(m);
|
||||
expect(sheet.workDate, DateTime.utc(2026, 9, 23));
|
||||
expect(sheet.approvedSnapshot?.totalSeconds, 3600);
|
||||
expect(sheet.isFirstPassApproved, isTrue);
|
||||
});
|
||||
|
||||
group('computeStreak', () {
|
||||
test('streak counts weekdays back from today, skipping weekends', () {
|
||||
// today Mon Sep 28 (no sheet);
|
||||
// sheets: Wed Sep 23 pending, Thu Sep 24 pending, Fri Sep 25 approved
|
||||
final sheets = [
|
||||
makeSheet(id: 'a', workDate: DateTime.utc(2026, 9, 23), status: 'pending'),
|
||||
makeSheet(id: 'b', workDate: DateTime.utc(2026, 9, 24), status: 'pending'),
|
||||
makeSheet(id: 'c', workDate: DateTime.utc(2026, 9, 25), status: 'approved'),
|
||||
];
|
||||
final result = computeStreak(
|
||||
sheets: sheets,
|
||||
leaveDays: {},
|
||||
today: DateTime.utc(2026, 9, 28),
|
||||
);
|
||||
expect(result.current, 3);
|
||||
});
|
||||
|
||||
test('a disapproved day breaks the streak', () {
|
||||
// Fri Sep 25 disapproved -> streak breaks immediately after skipping today
|
||||
final sheets = [
|
||||
makeSheet(id: 'a', workDate: DateTime.utc(2026, 9, 25), status: 'disapproved'),
|
||||
];
|
||||
final result = computeStreak(
|
||||
sheets: sheets,
|
||||
leaveDays: {},
|
||||
today: DateTime.utc(2026, 9, 28),
|
||||
);
|
||||
expect(result.current, 0);
|
||||
});
|
||||
|
||||
test('approved leave days are neutral', () {
|
||||
// Thu Sep 24 in leaveDays (no sheet); Wed Sep 23 approved, Fri Sep 25 approved;
|
||||
// today Mon Sep 28 (no sheet) -> current 2
|
||||
final sheets = [
|
||||
makeSheet(id: 'a', workDate: DateTime.utc(2026, 9, 23), status: 'approved'),
|
||||
makeSheet(id: 'b', workDate: DateTime.utc(2026, 9, 25), status: 'approved'),
|
||||
];
|
||||
final result = computeStreak(
|
||||
sheets: sheets,
|
||||
leaveDays: {DateTime.utc(2026, 9, 24)},
|
||||
today: DateTime.utc(2026, 9, 28),
|
||||
);
|
||||
expect(result.current, 2);
|
||||
});
|
||||
|
||||
test('best streak finds the longest earlier run', () {
|
||||
// Earlier run of 4: Mon Sep 7, Tue Sep 8, Wed Sep 9, Thu Sep 10 all approved
|
||||
// Then gap (Fri Sep 11 missing), later Fri Sep 25 approved (run=1)
|
||||
// today Mon Sep 28 (no sheet) -> current=1, best=4
|
||||
final sheets = [
|
||||
makeSheet(id: 'a', workDate: DateTime.utc(2026, 9, 7), status: 'approved'),
|
||||
makeSheet(id: 'b', workDate: DateTime.utc(2026, 9, 8), status: 'approved'),
|
||||
makeSheet(id: 'c', workDate: DateTime.utc(2026, 9, 9), status: 'approved'),
|
||||
makeSheet(id: 'd', workDate: DateTime.utc(2026, 9, 10), status: 'approved'),
|
||||
makeSheet(id: 'e', workDate: DateTime.utc(2026, 9, 25), status: 'approved'),
|
||||
];
|
||||
final result = computeStreak(
|
||||
sheets: sheets,
|
||||
leaveDays: {},
|
||||
today: DateTime.utc(2026, 9, 28),
|
||||
);
|
||||
expect(result.best, 4);
|
||||
});
|
||||
});
|
||||
|
||||
group('firstPassRate', () {
|
||||
test('first-pass rate over 30 days', () {
|
||||
// 2 approved(r=0), 1 approved(r=1), 1 disapproved -> 2 first-pass / 4 reviewed = 0.5
|
||||
final now = DateTime.utc(2026, 9, 28);
|
||||
final base = DateTime.utc(2026, 9, 20);
|
||||
final sheets = [
|
||||
makeSheet(id: 'a', workDate: base, status: 'approved', resubmissions: 0),
|
||||
makeSheet(
|
||||
id: 'b',
|
||||
workDate: base.add(const Duration(days: 1)),
|
||||
status: 'approved',
|
||||
resubmissions: 0),
|
||||
makeSheet(
|
||||
id: 'c',
|
||||
workDate: base.add(const Duration(days: 2)),
|
||||
status: 'approved',
|
||||
resubmissions: 1),
|
||||
makeSheet(
|
||||
id: 'd',
|
||||
workDate: base.add(const Duration(days: 3)),
|
||||
status: 'disapproved',
|
||||
resubmissions: 0),
|
||||
];
|
||||
final stats = computeProgrammerStats(
|
||||
userId: 'user1',
|
||||
now: now,
|
||||
tasks: [],
|
||||
sheets: sheets,
|
||||
leaveDays: {},
|
||||
todayFocus: Duration.zero,
|
||||
);
|
||||
expect(stats.firstPassRate, closeTo(0.5, 0.001));
|
||||
});
|
||||
|
||||
test('no reviewed sheets gives null first-pass rate', () {
|
||||
final stats = computeProgrammerStats(
|
||||
userId: 'user1',
|
||||
now: DateTime.utc(2026, 9, 28),
|
||||
tasks: [],
|
||||
sheets: [
|
||||
makeSheet(
|
||||
id: 'a',
|
||||
workDate: DateTime.utc(2026, 9, 20),
|
||||
status: 'draft',
|
||||
resubmissions: 0),
|
||||
],
|
||||
leaveDays: {},
|
||||
todayFocus: Duration.zero,
|
||||
);
|
||||
expect(stats.firstPassRate, isNull);
|
||||
});
|
||||
});
|
||||
|
||||
test('completedThisWeek uses a Monday-start week', () {
|
||||
// now = Mon Sep 28, 2026
|
||||
// week Mon Sep 28 - Sun Oct 4; last week Mon Sep 21 - Sun Sep 27
|
||||
final now = DateTime.utc(2026, 9, 28);
|
||||
|
||||
ProgrammerTask makeTask(String id, DateTime completedAt) {
|
||||
return ProgrammerTask(
|
||||
id: id,
|
||||
title: 'Task $id',
|
||||
category: ProgrammerTaskCategory.others,
|
||||
status: ProgrammerTaskStatus.completed,
|
||||
assigneeId: 'user1',
|
||||
createdAt: DateTime.utc(2026, 1, 1),
|
||||
updatedAt: DateTime.utc(2026, 1, 1),
|
||||
completedAt: completedAt,
|
||||
);
|
||||
}
|
||||
|
||||
final tasks = [
|
||||
makeTask('t1', DateTime.utc(2026, 9, 28)), // this week (Mon)
|
||||
makeTask('t2', DateTime.utc(2026, 9, 29)), // this week (Tue)
|
||||
makeTask('t3', DateTime.utc(2026, 9, 27)), // last week (Sun Sep 27)
|
||||
makeTask('t4', DateTime.utc(2026, 9, 21)), // last week (Mon Sep 21)
|
||||
];
|
||||
|
||||
final stats = computeProgrammerStats(
|
||||
userId: 'user1',
|
||||
now: now,
|
||||
tasks: tasks,
|
||||
sheets: [],
|
||||
leaveDays: {},
|
||||
todayFocus: Duration.zero,
|
||||
);
|
||||
|
||||
expect(stats.completedThisWeek, 2);
|
||||
expect(stats.completedLastWeek, 2);
|
||||
});
|
||||
|
||||
group('computeBadges', () {
|
||||
test('tiered badge shows highest earned tier and next target', () {
|
||||
final b =
|
||||
computeBadges(statsWith(completedTotal: 12), includeSheetBadges: false)
|
||||
.firstWhere((b) => b.id == 'finisher');
|
||||
expect(b.earned, isTrue);
|
||||
expect(b.title, 'Finisher · 10');
|
||||
expect(b.target, 50);
|
||||
});
|
||||
|
||||
test('includeSheetBadges false hides sheet-based badges', () {
|
||||
final ids =
|
||||
computeBadges(statsWith(), includeSheetBadges: false)
|
||||
.map((b) => b.id)
|
||||
.toSet();
|
||||
expect(
|
||||
ids,
|
||||
isNot(containsAll(
|
||||
['deep_work', 'on_a_roll', 'team_player', 'clean_week'])));
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user