Added My Schedule tab in attendance screen

Allow 1 Day, Whole Week and Date Range swapping
This commit is contained in:
2026-03-22 11:52:25 +08:00
parent ba155885c0
commit 049ab2c794
17 changed files with 2515 additions and 305 deletions
+47 -1
View File
@@ -1,3 +1,5 @@
import 'dart:async';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
@@ -53,6 +55,20 @@ final dutySchedulesProvider = StreamProvider<List<DutySchedule>>((ref) {
);
ref.onDispose(wrapper.dispose);
// Immediate poll so any changes that happened while this provider was
// not alive (e.g. a swap was accepted on another device) are reflected
// right away — before the 3-second periodic timer fires.
wrapper.pollNow();
// Periodic safety-net: keep polling every 3 s so that ownership changes
// (swap accepted → user_id updated on duty_schedules) are always picked
// up even if Supabase Realtime misses the event.
final dutyRefreshTimer = Timer.periodic(const Duration(seconds: 3), (_) {
wrapper.pollNow();
});
ref.onDispose(dutyRefreshTimer.cancel);
return wrapper.stream.map((result) => result.data);
});
@@ -127,6 +143,7 @@ final swapRequestsProvider = StreamProvider<List<SwapRequest>>((ref) {
final data = await client
.from('swap_requests')
.select()
.inFilter('status', ['pending', 'admin_review'])
.order('created_at', ascending: false);
return data.map(SwapRequest.fromMap).toList();
},
@@ -136,13 +153,28 @@ final swapRequestsProvider = StreamProvider<List<SwapRequest>>((ref) {
);
ref.onDispose(wrapper.dispose);
// Immediate poll: fetch fresh data right away so any status changes that
// happened while this provider was not alive are reflected instantly,
// before the 3-second periodic timer fires for the first time.
wrapper.pollNow();
// Periodic safety-net: keep polling every 3 s to catch any status changes
// that Supabase Realtime may have missed (e.g. when the swap_requests table
// is not yet in the supabase_realtime publication).
final refreshTimer = Timer.periodic(const Duration(seconds: 3), (_) {
wrapper.pollNow();
});
ref.onDispose(refreshTimer.cancel);
return wrapper.stream.map((result) {
// 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 == profileId || row.recipientId == profileId)) {
// admins see all swaps; standard users only see swaps they're in
if (!isAdmin && !(row.requesterId == profileId || row.recipientId == profileId)) {
return false;
}
// only keep pending and admin_review statuses
@@ -151,6 +183,20 @@ final swapRequestsProvider = StreamProvider<List<SwapRequest>>((ref) {
});
});
/// IDs of swap requests that were acted on locally (accepted, rejected, etc.).
/// Kept as a global provider so the set survives tab switches — widget state
/// is disposed when the user navigates away from My Schedule.
final locallyRemovedSwapIdsProvider = StateProvider<Set<String>>((ref) => {});
/// IDs of duty_schedules owned by the current user that were created by an accepted swap.
final swappedScheduleIdsProvider = Provider<Set<String>>((ref) {
final schedules = ref.watch(dutySchedulesProvider).valueOrNull ?? [];
return {
for (final s in schedules)
if (s.swapRequestId != null) s.id,
};
});
final workforceControllerProvider = Provider<WorkforceController>((ref) {
final client = ref.watch(supabaseClientProvider);
return WorkforceController(client);