112 lines
3.6 KiB
Dart
112 lines
3.6 KiB
Dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
|
|
|
import '../models/leave_of_absence.dart';
|
|
import 'profile_provider.dart';
|
|
import 'supabase_provider.dart';
|
|
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<List<LeaveOfAbsence>>((ref) {
|
|
final client = ref.watch(supabaseClientProvider);
|
|
final profileAsync = ref.watch(currentProfileProvider);
|
|
final profile = profileAsync.valueOrNull;
|
|
if (profile == null) return Stream.value(const <LeaveOfAbsence>[]);
|
|
|
|
final hasFullAccess =
|
|
profile.role == 'admin' ||
|
|
profile.role == 'dispatcher' ||
|
|
profile.role == 'it_staff';
|
|
|
|
final wrapper = StreamRecoveryWrapper<LeaveOfAbsence>(
|
|
stream: hasFullAccess
|
|
? client
|
|
.from('leave_of_absence')
|
|
.stream(primaryKey: ['id'])
|
|
.order('start_time', ascending: false)
|
|
: client
|
|
.from('leave_of_absence')
|
|
.stream(primaryKey: ['id'])
|
|
.eq('user_id', profile.id)
|
|
.order('start_time', ascending: false),
|
|
onPollData: () async {
|
|
final query = client.from('leave_of_absence').select();
|
|
final data = hasFullAccess
|
|
? await query.order('start_time', ascending: false)
|
|
: await query
|
|
.eq('user_id', profile.id)
|
|
.order('start_time', ascending: false);
|
|
return data.map(LeaveOfAbsence.fromMap).toList();
|
|
},
|
|
fromMap: LeaveOfAbsence.fromMap,
|
|
channelName: 'leave_of_absence',
|
|
onStatusChanged: ref.read(realtimeControllerProvider).handleChannelStatus,
|
|
);
|
|
|
|
ref.onDispose(wrapper.dispose);
|
|
return wrapper.stream.map((result) => result.data);
|
|
});
|
|
|
|
final leaveControllerProvider = Provider<LeaveController>((ref) {
|
|
final client = ref.watch(supabaseClientProvider);
|
|
return LeaveController(client);
|
|
});
|
|
|
|
class LeaveController {
|
|
LeaveController(this._client);
|
|
|
|
final SupabaseClient _client;
|
|
|
|
/// File a leave of absence for the current user.
|
|
/// Caller controls auto-approval based on role policy.
|
|
Future<void> fileLeave({
|
|
required String leaveType,
|
|
required String justification,
|
|
required DateTime startTime,
|
|
required DateTime endTime,
|
|
required bool autoApprove,
|
|
}) async {
|
|
final uid = _client.auth.currentUser!.id;
|
|
await _client.from('leave_of_absence').insert({
|
|
'user_id': uid,
|
|
'leave_type': leaveType,
|
|
'justification': justification,
|
|
'start_time': startTime.toIso8601String(),
|
|
'end_time': endTime.toIso8601String(),
|
|
'status': autoApprove ? 'approved' : 'pending',
|
|
'filed_by': uid,
|
|
});
|
|
}
|
|
|
|
/// Approve a leave request.
|
|
Future<void> approveLeave(String leaveId) async {
|
|
await _client
|
|
.from('leave_of_absence')
|
|
.update({'status': 'approved'})
|
|
.eq('id', leaveId);
|
|
}
|
|
|
|
/// Reject a leave request.
|
|
Future<void> rejectLeave(String leaveId) async {
|
|
await _client
|
|
.from('leave_of_absence')
|
|
.update({'status': 'rejected'})
|
|
.eq('id', leaveId);
|
|
}
|
|
|
|
/// Cancel an approved leave.
|
|
Future<void> cancelLeave(String leaveId) async {
|
|
await _client
|
|
.from('leave_of_absence')
|
|
.update({'status': 'cancelled'})
|
|
.eq('id', leaveId);
|
|
}
|
|
}
|