tasq/lib/providers/attendance_provider.dart

161 lines
5.0 KiB
Dart

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<ReportDateRange>((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<List<AttendanceLog>>((ref) {
final client = ref.watch(supabaseClientProvider);
final profileAsync = ref.watch(currentProfileProvider);
final profile = profileAsync.valueOrNull;
if (profile == null) return Stream.value(const <AttendanceLog>[]);
final hasFullAccess =
profile.role == 'admin' ||
profile.role == 'programmer' ||
profile.role == 'dispatcher' ||
profile.role == 'it_staff';
final wrapper = StreamRecoveryWrapper<AttendanceLog>(
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<AttendanceController>((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<String?> 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<void> 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,
'p_justification': justification,
},
);
}
/// Overtime check-in (no pre-existing schedule required).
/// Creates an overtime duty schedule + attendance log in one RPC call.
Future<String?> 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<void> 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<void> skipVerification(String attendanceId) async {
await _client
.from('attendance_logs')
.update({'verification_status': 'skipped'})
.eq('id', attendanceId);
}
}