From f8c79acbbc8c2b17d7095348147677bbe289a4e6 Mon Sep 17 00:00:00 2001 From: Marc Rejohn Castillano Date: Wed, 11 Mar 2026 18:59:28 +0800 Subject: [PATCH] Fixed Leave rejection and approvals --- lib/providers/leave_provider.dart | 6 ++++++ lib/providers/workforce_provider.dart | 17 +++++++++++------ lib/screens/attendance/attendance_screen.dart | 19 +++++++++++++++++-- lib/screens/workforce/workforce_screen.dart | 8 +++++++- 4 files changed, 41 insertions(+), 9 deletions(-) diff --git a/lib/providers/leave_provider.dart b/lib/providers/leave_provider.dart index b1c7c1e1..71332854 100644 --- a/lib/providers/leave_provider.dart +++ b/lib/providers/leave_provider.dart @@ -8,6 +8,12 @@ import 'stream_recovery.dart'; import 'realtime_controller.dart'; /// All visible leaves (own for standard, all for admin/dispatcher/it_staff). +/// +/// Consumers should **not** treat every record as an active absence; the UI +/// layers (dashboard, logbook) explicitly filter to `status == 'approved'` and +/// verify the leave overlaps the current time. This prevents rejected or +/// pending applications from inadvertently influencing schedules or status +/// computations. final leavesProvider = StreamProvider>((ref) { final client = ref.watch(supabaseClientProvider); final profileAsync = ref.watch(currentProfileProvider); diff --git a/lib/providers/workforce_provider.dart b/lib/providers/workforce_provider.dart index 9239fa9b..5ff8d6e9 100644 --- a/lib/providers/workforce_provider.dart +++ b/lib/providers/workforce_provider.dart @@ -127,12 +127,17 @@ final swapRequestsProvider = StreamProvider>((ref) { ref.onDispose(wrapper.dispose); return wrapper.stream.map((result) { - return result.data - .where( - (row) => - row.requesterId == profile.id || row.recipientId == profile.id, - ) - .toList(); + // only return requests that are still actionable; once a swap has been + // accepted or rejected we no longer need to bubble it up to the UI for + // either party. admins still see "admin_review" rows so they can act on + // escalated cases. + return result.data.where((row) { + if (!(row.requesterId == profile.id || row.recipientId == profile.id)) { + return false; + } + // only keep pending and admin_review statuses + return row.status == 'pending' || row.status == 'admin_review'; + }).toList(); }); }); diff --git a/lib/screens/attendance/attendance_screen.dart b/lib/screens/attendance/attendance_screen.dart index 85246fc1..8f116971 100644 --- a/lib/screens/attendance/attendance_screen.dart +++ b/lib/screens/attendance/attendance_screen.dart @@ -16,6 +16,7 @@ import '../../models/profile.dart'; import '../../providers/attendance_provider.dart'; import '../../providers/debug_settings_provider.dart'; import '../../providers/leave_provider.dart'; +import '../../screens/dashboard/dashboard_screen.dart'; import '../../providers/pass_slip_provider.dart'; import '../../providers/profile_provider.dart'; import '../../providers/reports_provider.dart'; @@ -3796,6 +3797,8 @@ class _LeaveTabState extends ConsumerState<_LeaveTab> { setState(() => _submitting = true); try { await ref.read(leaveControllerProvider).approveLeave(leaveId); + ref.invalidate(leavesProvider); + ref.invalidate(dashboardMetricsProvider); if (mounted) { showSuccessSnackBar(context, 'Leave approved.'); } @@ -3812,6 +3815,8 @@ class _LeaveTabState extends ConsumerState<_LeaveTab> { setState(() => _submitting = true); try { await ref.read(leaveControllerProvider).rejectLeave(leaveId); + ref.invalidate(leavesProvider); + ref.invalidate(dashboardMetricsProvider); if (mounted) { showSuccessSnackBar(context, 'Leave rejected.'); } @@ -3828,6 +3833,8 @@ class _LeaveTabState extends ConsumerState<_LeaveTab> { setState(() => _submitting = true); try { await ref.read(leaveControllerProvider).cancelLeave(leaveId); + ref.invalidate(leavesProvider); + ref.invalidate(dashboardMetricsProvider); if (mounted) { showSuccessSnackBar(context, 'Leave cancelled.'); } @@ -4112,14 +4119,14 @@ class _FileLeaveDialogState extends ConsumerState<_FileLeaveDialog> { return; } - final startDt = DateTime( + var startDt = DateTime( _startDate!.year, _startDate!.month, _startDate!.day, _startTime!.hour, _startTime!.minute, ); - final endDt = DateTime( + var endDt = DateTime( _startDate!.year, _startDate!.month, _startDate!.day, @@ -4132,6 +4139,10 @@ class _FileLeaveDialogState extends ConsumerState<_FileLeaveDialog> { return; } + // convert to app timezone to avoid device-local mismatches + startDt = AppTime.toAppTime(startDt); + endDt = AppTime.toAppTime(endDt); + setState(() => _submitting = true); try { await ref @@ -4143,6 +4154,10 @@ class _FileLeaveDialogState extends ConsumerState<_FileLeaveDialog> { endTime: endDt, autoApprove: widget.isAdmin, ); + // ensure UI and dashboard will refresh promptly even if realtime is + // delayed or temporarily disconnected + ref.invalidate(leavesProvider); + ref.invalidate(dashboardMetricsProvider); if (mounted) { Navigator.of(context).pop(); widget.onSubmitted(); diff --git a/lib/screens/workforce/workforce_screen.dart b/lib/screens/workforce/workforce_screen.dart index 9453efcf..df485c9d 100644 --- a/lib/screens/workforce/workforce_screen.dart +++ b/lib/screens/workforce/workforce_screen.dart @@ -513,7 +513,7 @@ class _ScheduleTile extends ConsumerWidget { if (confirmed != true || !context.mounted) return; - final startDateTime = DateTime( + var startDateTime = DateTime( selectedDate.year, selectedDate.month, selectedDate.day, @@ -531,6 +531,12 @@ class _ScheduleTile extends ConsumerWidget { endDateTime = endDateTime.add(const Duration(days: 1)); } + // ensure times are expressed in the app timezone (Asia/Manila) before + // sending to the backend. previously these were raw local DateTimes which + // caused off-by-offset errors when the device timezone differed. + startDateTime = AppTime.toAppTime(startDateTime); + endDateTime = AppTime.toAppTime(endDateTime); + try { await ref .read(workforceControllerProvider)