import 'dart:typed_data'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; import '../models/attendance_log.dart'; import '../utils/app_time.dart'; import 'profile_provider.dart'; import 'reports_provider.dart'; import 'supabase_provider.dart'; import 'stream_recovery.dart'; import 'realtime_controller.dart'; /// Date range for attendance logbook, defaults to "Last 7 Days". final attendanceDateRangeProvider = StateProvider((ref) { final now = AppTime.now(); final today = DateTime(now.year, now.month, now.day); return ReportDateRange( start: today, end: today.add(const Duration(days: 1)), label: 'Today', ); }); /// All visible attendance logs (own for standard, all for admin/dispatcher/it_staff). final attendanceLogsProvider = StreamProvider>((ref) { final client = ref.watch(supabaseClientProvider); final profileAsync = ref.watch(currentProfileProvider); final profile = profileAsync.valueOrNull; if (profile == null) return Stream.value(const []); final hasFullAccess = profile.role == 'admin' || profile.role == 'dispatcher' || profile.role == 'it_staff'; final wrapper = StreamRecoveryWrapper( stream: hasFullAccess ? client .from('attendance_logs') .stream(primaryKey: ['id']) .order('check_in_at', ascending: false) : client .from('attendance_logs') .stream(primaryKey: ['id']) .eq('user_id', profile.id) .order('check_in_at', ascending: false), onPollData: () async { final query = client.from('attendance_logs').select(); final data = hasFullAccess ? await query.order('check_in_at', ascending: false) : await query .eq('user_id', profile.id) .order('check_in_at', ascending: false); return data.map(AttendanceLog.fromMap).toList(); }, fromMap: AttendanceLog.fromMap, channelName: 'attendance_logs', onStatusChanged: ref.read(realtimeControllerProvider).handleChannelStatus, ); ref.onDispose(wrapper.dispose); return wrapper.stream.map((result) => result.data); }); final attendanceControllerProvider = Provider((ref) { final client = ref.watch(supabaseClientProvider); return AttendanceController(client); }); class AttendanceController { AttendanceController(this._client); final SupabaseClient _client; /// Check in to a duty schedule. Returns the attendance log ID. Future checkIn({ required String dutyScheduleId, required double lat, required double lng, }) async { final data = await _client.rpc( 'attendance_check_in', params: {'p_duty_id': dutyScheduleId, 'p_lat': lat, 'p_lng': lng}, ); return data as String?; } /// Check out from an attendance log. Future checkOut({ required String attendanceId, required double lat, required double lng, String? justification, }) async { await _client.rpc( 'attendance_check_out', params: { 'p_attendance_id': attendanceId, 'p_lat': lat, 'p_lng': lng, // ignore: use_null_aware_elements if (justification != null) 'p_justification': justification, }, ); } /// Overtime check-in (no pre-existing schedule required). /// Creates an overtime duty schedule + attendance log in one RPC call. Future overtimeCheckIn({ required double lat, required double lng, String? justification, }) async { final data = await _client.rpc( 'overtime_check_in', params: {'p_lat': lat, 'p_lng': lng, 'p_justification': justification}, ); return data as String?; } /// Upload a verification selfie and update the attendance log. Future uploadVerification({ required String attendanceId, required Uint8List bytes, required String fileName, required String status, // 'verified', 'unverified' bool isCheckOut = false, }) async { final userId = _client.auth.currentUser!.id; final ext = fileName.split('.').last.toLowerCase(); final prefix = isCheckOut ? 'checkout' : 'checkin'; final path = '$userId/${prefix}_$attendanceId.$ext'; await _client.storage .from('attendance-verification') .uploadBinary( path, bytes, fileOptions: const FileOptions(upsert: true), ); final url = _client.storage .from('attendance-verification') .getPublicUrl(path); final column = isCheckOut ? 'check_out_verification_photo_url' : 'check_in_verification_photo_url'; await _client .from('attendance_logs') .update({'verification_status': status, column: url}) .eq('id', attendanceId); } /// Mark an attendance log as skipped verification. Future skipVerification(String attendanceId) async { await _client .from('attendance_logs') .update({'verification_status': 'skipped'}) .eq('id', attendanceId); } }