diff --git a/lib/screens/dashboard/dashboard_screen.dart b/lib/screens/dashboard/dashboard_screen.dart index 925a1455..f553ae4a 100644 --- a/lib/screens/dashboard/dashboard_screen.dart +++ b/lib/screens/dashboard/dashboard_screen.dart @@ -44,6 +44,7 @@ import '../../widgets/app_metric_card.dart'; import '../../widgets/app_page_header.dart'; import '../../widgets/app_section_header.dart'; import '../../utils/app_time.dart'; +import '../../utils/staff_presence.dart' as staff_presence; class DashboardMetrics { DashboardMetrics({ @@ -369,22 +370,10 @@ final dashboardMetricsProvider = Provider>((ref) { } // Index today's leaves by user. - final todayLeaveByUser = {}; - for (final l in allLeaves) { - if (l.status == 'approved' && - !l.startTime.isAfter(now) && - l.endTime.isAfter(now)) { - todayLeaveByUser[l.userId] = l; - } - } + final todayLeaveByUser = staff_presence.activeLeaveByUser(allLeaves, now); // Index active pass slips by user. - final activePassSlipByUser = {}; - for (final slip in allPassSlips) { - if (slip.isActive) { - activePassSlipByUser[slip.userId] = slip; - } - } + final activePassSlipByUser = staff_presence.activePassSlipByUser(allPassSlips); final noon = DateTime(now.year, now.month, now.day, 12, 0); final onePM = DateTime(now.year, now.month, now.day, 13, 0); diff --git a/lib/utils/staff_presence.dart b/lib/utils/staff_presence.dart new file mode 100644 index 00000000..3a7ca35b --- /dev/null +++ b/lib/utils/staff_presence.dart @@ -0,0 +1,63 @@ +import 'package:tasq/models/leave_of_absence.model.dart'; +import 'package:tasq/models/pass_slip.model.dart'; +import 'programmer_daily_time.dart'; + +/// Index approved leaves covering the given moment by user ID. +/// +/// Filters to leaves where: +/// - status == 'approved' +/// - startTime <= now < endTime +/// +/// Returns a map from userId to the leave record. +Map activeLeaveByUser( + List leaves, + DateTime now, +) { + final result = {}; + for (final leave in leaves) { + if (leave.status == 'approved' && + !leave.startTime.isAfter(now) && + leave.endTime.isAfter(now)) { + result[leave.userId] = leave; + } + } + return result; +} + +/// Index active pass slips by user ID. +/// +/// Filters to slips where slip.isActive is true. +/// +/// Returns a map from userId to the pass slip record. +Map activePassSlipByUser(List slips) { + final result = {}; + for (final slip in slips) { + if (slip.isActive) { + result[slip.userId] = slip; + } + } + return result; +} + +/// Get the set of Manila calendar days covered by approved leaves for a user. +/// +/// Iterates through all approved leaves for the given user and collects the +/// set of Manila day keys (as DateTime.utc(year, month, day)) from each +/// leave's start date through end date, inclusive. +Set approvedLeaveDays( + List leaves, + String userId, +) { + final result = {}; + for (final leave in leaves) { + if (leave.userId == userId && leave.status == 'approved') { + var current = manilaDay(leave.startTime); + final endDay = manilaDay(leave.endTime); + while (!current.isAfter(endDay)) { + result.add(current); + current = current.add(Duration(days: 1)); + } + } + } + return result; +} diff --git a/test/staff_presence_test.dart b/test/staff_presence_test.dart new file mode 100644 index 00000000..c6fd0abb --- /dev/null +++ b/test/staff_presence_test.dart @@ -0,0 +1,266 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:tasq/models/leave_of_absence.model.dart'; +import 'package:tasq/models/pass_slip.model.dart'; +import 'package:tasq/utils/app_time.dart'; +import 'package:tasq/utils/staff_presence.dart'; + +void main() { + setUpAll(() { + AppTime.initialize(); + }); + + group('activeLeaveByUser', () { + test('includes approved leave covering now', () { + final now = AppTime.now(); + final startTime = now.subtract(Duration(hours: 1)); + final endTime = now.add(Duration(hours: 1)); + + final leave = LeaveOfAbsence( + id: 'leave1', + userId: 'user1', + leaveType: 'sick_leave', + justification: 'Sick', + startTime: startTime, + endTime: endTime, + status: 'approved', + filedBy: 'admin', + createdAt: now, + ); + + final result = activeLeaveByUser([leave], now); + expect(result, containsPair('user1', leave)); + }); + + test('excludes pending leave', () { + final now = AppTime.now(); + final startTime = now.subtract(Duration(hours: 1)); + final endTime = now.add(Duration(hours: 1)); + + final leave = LeaveOfAbsence( + id: 'leave1', + userId: 'user1', + leaveType: 'sick_leave', + justification: 'Sick', + startTime: startTime, + endTime: endTime, + status: 'pending', + filedBy: 'admin', + createdAt: now, + ); + + final result = activeLeaveByUser([leave], now); + expect(result, isEmpty); + }); + + test('excludes leave that ended before now', () { + final now = AppTime.now(); + final startTime = now.subtract(Duration(hours: 3)); + final endTime = now.subtract(Duration(hours: 1)); + + final leave = LeaveOfAbsence( + id: 'leave1', + userId: 'user1', + leaveType: 'sick_leave', + justification: 'Sick', + startTime: startTime, + endTime: endTime, + status: 'approved', + filedBy: 'admin', + createdAt: now, + ); + + final result = activeLeaveByUser([leave], now); + expect(result, isEmpty); + }); + + test('excludes leave that starts after now', () { + final now = AppTime.now(); + final startTime = now.add(Duration(hours: 1)); + final endTime = now.add(Duration(hours: 3)); + + final leave = LeaveOfAbsence( + id: 'leave1', + userId: 'user1', + leaveType: 'sick_leave', + justification: 'Sick', + startTime: startTime, + endTime: endTime, + status: 'approved', + filedBy: 'admin', + createdAt: now, + ); + + final result = activeLeaveByUser([leave], now); + expect(result, isEmpty); + }); + }); + + group('activePassSlipByUser', () { + test('includes active pass slip', () { + final slip = PassSlip( + id: 'slip1', + userId: 'user1', + dutyScheduleId: 'sched1', + reason: 'Test', + status: 'approved', + requestedAt: AppTime.now(), + slipStart: AppTime.now(), + slipEnd: null, + ); + + final result = activePassSlipByUser([slip]); + expect(result, containsPair('user1', slip)); + }); + + test('excludes pending pass slip', () { + final slip = PassSlip( + id: 'slip1', + userId: 'user1', + dutyScheduleId: 'sched1', + reason: 'Test', + status: 'pending', + requestedAt: AppTime.now(), + slipStart: null, + slipEnd: null, + ); + + final result = activePassSlipByUser([slip]); + expect(result, isEmpty); + }); + + test('excludes ended pass slip', () { + final slip = PassSlip( + id: 'slip1', + userId: 'user1', + dutyScheduleId: 'sched1', + reason: 'Test', + status: 'approved', + requestedAt: AppTime.now(), + slipStart: AppTime.now(), + slipEnd: AppTime.now().add(Duration(hours: 1)), + ); + + final result = activePassSlipByUser([slip]); + expect(result, isEmpty); + }); + }); + + group('approvedLeaveDays', () { + test('single day leave returns one day key', () { + // Sep 24, 08:00 to Sep 24, 17:00 Manila time → just Sep 24 + final start = AppTime.fromComponents(year: 2026, month: 9, day: 24, hour: 8, minute: 0); + final end = AppTime.fromComponents(year: 2026, month: 9, day: 24, hour: 17, minute: 0); + + final leave = LeaveOfAbsence( + id: 'leave1', + userId: 'user1', + leaveType: 'sick_leave', + justification: 'Sick', + startTime: start, + endTime: end, + status: 'approved', + filedBy: 'admin', + createdAt: DateTime.utc(2026, 9, 23), + ); + + final result = approvedLeaveDays([leave], 'user1'); + expect(result, contains(DateTime.utc(2026, 9, 24))); + expect(result.length, 1); + }); + + test('multi-day leave returns all day keys', () { + // Sep 24, 08:00 to Sep 25, 17:00 Manila time → Sep 24 and Sep 25 + final start = AppTime.fromComponents(year: 2026, month: 9, day: 24, hour: 8, minute: 0); + final end = AppTime.fromComponents(year: 2026, month: 9, day: 25, hour: 17, minute: 0); + + final leave = LeaveOfAbsence( + id: 'leave1', + userId: 'user1', + leaveType: 'vacation_leave', + justification: 'Vacation', + startTime: start, + endTime: end, + status: 'approved', + filedBy: 'admin', + createdAt: DateTime.utc(2026, 9, 23), + ); + + final result = approvedLeaveDays([leave], 'user1'); + expect(result, contains(DateTime.utc(2026, 9, 24))); + expect(result, contains(DateTime.utc(2026, 9, 25))); + expect(result.length, 2); + }); + + test('excludes pending leaves', () { + final start = AppTime.fromComponents(year: 2026, month: 9, day: 24, hour: 8, minute: 0); + final end = AppTime.fromComponents(year: 2026, month: 9, day: 25, hour: 17, minute: 0); + + final leave = LeaveOfAbsence( + id: 'leave1', + userId: 'user1', + leaveType: 'vacation_leave', + justification: 'Vacation', + startTime: start, + endTime: end, + status: 'pending', + filedBy: 'admin', + createdAt: DateTime.utc(2026, 9, 23), + ); + + final result = approvedLeaveDays([leave], 'user1'); + expect(result, isEmpty); + }); + + test('filters by userId', () { + final start = AppTime.fromComponents(year: 2026, month: 9, day: 24, hour: 8, minute: 0); + final end = AppTime.fromComponents(year: 2026, month: 9, day: 25, hour: 17, minute: 0); + + final leave = LeaveOfAbsence( + id: 'leave1', + userId: 'user1', + leaveType: 'vacation_leave', + justification: 'Vacation', + startTime: start, + endTime: end, + status: 'approved', + filedBy: 'admin', + createdAt: DateTime.utc(2026, 9, 23), + ); + + final result = approvedLeaveDays([leave], 'user2'); + expect(result, isEmpty); + }); + + test('handles multiple approved leaves for same user', () { + final leave1 = LeaveOfAbsence( + id: 'leave1', + userId: 'user1', + leaveType: 'sick_leave', + justification: 'Sick', + startTime: AppTime.fromComponents(year: 2026, month: 9, day: 24, hour: 8, minute: 0), + endTime: AppTime.fromComponents(year: 2026, month: 9, day: 24, hour: 17, minute: 0), + status: 'approved', + filedBy: 'admin', + createdAt: AppTime.fromComponents(year: 2026, month: 9, day: 23), + ); + + final leave2 = LeaveOfAbsence( + id: 'leave2', + userId: 'user1', + leaveType: 'vacation_leave', + justification: 'Vacation', + startTime: AppTime.fromComponents(year: 2026, month: 9, day: 26, hour: 8, minute: 0), + endTime: AppTime.fromComponents(year: 2026, month: 9, day: 27, hour: 17, minute: 0), + status: 'approved', + filedBy: 'admin', + createdAt: AppTime.fromComponents(year: 2026, month: 9, day: 23), + ); + + final result = approvedLeaveDays([leave1, leave2], 'user1'); + expect(result, contains(DateTime.utc(2026, 9, 24))); + expect(result, contains(DateTime.utc(2026, 9, 26))); + expect(result, contains(DateTime.utc(2026, 9, 27))); + expect(result.length, 3); + }); + }); +}