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,113 @@
|
||||
import 'package:tasq/utils/programmer_daily_time.dart';
|
||||
import 'package:tasq/utils/programmer_day_rows.dart';
|
||||
|
||||
/// Status constants for a programmer day sheet.
|
||||
class DaySheetStatus {
|
||||
static const String draft = 'draft';
|
||||
static const String pending = 'pending';
|
||||
static const String disapproved = 'disapproved';
|
||||
static const String approved = 'approved';
|
||||
|
||||
static String label(String s) {
|
||||
switch (s) {
|
||||
case draft:
|
||||
return 'Draft';
|
||||
case pending:
|
||||
return 'Pending';
|
||||
case disapproved:
|
||||
return 'Disapproved';
|
||||
case approved:
|
||||
return 'Approved';
|
||||
default:
|
||||
return s;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Immutable snapshot of a day sheet at approval time.
|
||||
class DaySheetSnapshot {
|
||||
final int totalSeconds;
|
||||
final List<DaySheetRow> rows;
|
||||
|
||||
const DaySheetSnapshot({required this.totalSeconds, required this.rows});
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'v': 1,
|
||||
'total_seconds': totalSeconds,
|
||||
'rows': rows.map((r) => r.toJson()).toList(),
|
||||
};
|
||||
|
||||
factory DaySheetSnapshot.fromJson(Map<String, dynamic> j) {
|
||||
final rowsList = (j['rows'] as List? ?? [])
|
||||
.map((r) => DaySheetRow.fromJson(r as Map<String, dynamic>))
|
||||
.toList();
|
||||
return DaySheetSnapshot(
|
||||
totalSeconds: (j['total_seconds'] as num).toInt(),
|
||||
rows: rowsList,
|
||||
);
|
||||
}
|
||||
|
||||
factory DaySheetSnapshot.fromRows(List<DaySheetRow> rows) {
|
||||
final total = rows.fold(0, (sum, r) => sum + r.seconds);
|
||||
return DaySheetSnapshot(totalSeconds: total, rows: rows);
|
||||
}
|
||||
}
|
||||
|
||||
/// A programmer's daily time-sheet submission for one work day.
|
||||
class ProgrammerDaySheet {
|
||||
final String id;
|
||||
final String programmerId;
|
||||
final String status;
|
||||
final DateTime workDate;
|
||||
final DateTime? submittedAt;
|
||||
final DateTime? reviewedAt;
|
||||
final bool autoSubmitted;
|
||||
final String? reviewedBy;
|
||||
final int resubmissions;
|
||||
final DaySheetSnapshot? approvedSnapshot;
|
||||
final DateTime createdAt;
|
||||
final DateTime updatedAt;
|
||||
|
||||
const ProgrammerDaySheet({
|
||||
required this.id,
|
||||
required this.programmerId,
|
||||
required this.status,
|
||||
required this.workDate,
|
||||
this.submittedAt,
|
||||
this.reviewedAt,
|
||||
this.autoSubmitted = false,
|
||||
this.reviewedBy,
|
||||
this.resubmissions = 0,
|
||||
this.approvedSnapshot,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
});
|
||||
|
||||
/// True when approved on the first submission (no resubmissions needed).
|
||||
bool get isFirstPassApproved =>
|
||||
status == DaySheetStatus.approved && resubmissions == 0;
|
||||
|
||||
factory ProgrammerDaySheet.fromMap(Map<String, dynamic> m) {
|
||||
return ProgrammerDaySheet(
|
||||
id: m['id'] as String,
|
||||
programmerId: m['programmer_id'] as String,
|
||||
status: m['status'] as String? ?? DaySheetStatus.draft,
|
||||
workDate: parseDayKey(m['work_date'] as String),
|
||||
submittedAt: m['submitted_at'] != null
|
||||
? DateTime.parse(m['submitted_at'] as String)
|
||||
: null,
|
||||
reviewedAt: m['reviewed_at'] != null
|
||||
? DateTime.parse(m['reviewed_at'] as String)
|
||||
: null,
|
||||
autoSubmitted: m['auto_submitted'] as bool? ?? false,
|
||||
reviewedBy: m['reviewed_by'] as String?,
|
||||
resubmissions: (m['resubmissions'] as num?)?.toInt() ?? 0,
|
||||
approvedSnapshot: m['approved_snapshot'] != null
|
||||
? DaySheetSnapshot.fromJson(
|
||||
m['approved_snapshot'] as Map<String, dynamic>)
|
||||
: null,
|
||||
createdAt: DateTime.parse(m['created_at'] as String),
|
||||
updatedAt: DateTime.parse(m['updated_at'] as String),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user