Files
tasq/lib/models/programmer_day_sheet.model.dart
T
redz1029 cdfeb954fd 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>
2026-09-27 21:27:03 +08:00

114 lines
3.4 KiB
Dart

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),
);
}
}