cdfeb954fd
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>
114 lines
3.4 KiB
Dart
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),
|
|
);
|
|
}
|
|
}
|