import 'dart:async'; import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; import 'package:flutter/material.dart'; import 'package:uuid/uuid.dart'; import '../brick/cache_helpers.dart'; import '../models/it_service_request.dart'; import '../models/it_service_request_assignment.dart'; import '../models/it_service_request_activity_log.dart'; import '../models/it_service_request_action.dart'; import '../utils/app_time.dart'; import '../utils/snackbar.dart' show isOfflineSaveError; import 'connectivity_provider.dart'; import 'profile_provider.dart'; import 'supabase_provider.dart'; import 'user_offices_provider.dart'; import 'stream_recovery.dart'; import 'realtime_controller.dart'; // --------------------------------------------------------------------------- // Query parameters // --------------------------------------------------------------------------- class ItServiceRequestQuery { const ItServiceRequestQuery({ this.offset = 0, this.limit = 50, this.searchQuery = '', this.officeId, this.status, this.dateRange, }); final int offset; final int limit; final String searchQuery; final String? officeId; final String? status; final DateTimeRange? dateRange; ItServiceRequestQuery copyWith({ int? offset, int? limit, String? searchQuery, String? officeId, String? status, DateTimeRange? dateRange, }) { return ItServiceRequestQuery( offset: offset ?? this.offset, limit: limit ?? this.limit, searchQuery: searchQuery ?? this.searchQuery, officeId: officeId ?? this.officeId, status: status ?? this.status, dateRange: dateRange ?? this.dateRange, ); } } final itServiceRequestQueryProvider = StateProvider( (ref) => const ItServiceRequestQuery(), ); // --------------------------------------------------------------------------- // Offline-pending state // --------------------------------------------------------------------------- /// IT Service Requests created offline (typed model — merged into live stream). final offlinePendingItServiceRequestsProvider = StateProvider>((ref) => const []); /// Raw RPC params for offline-created requests — used for reconnect replay. final offlinePendingItServiceRequestRawProvider = StateProvider>>((ref) => const []); /// Field-level updates to existing requests made offline, keyed by request id. final offlinePendingItServiceRequestUpdatesProvider = StateProvider>>((ref) => const {}); /// Pending action-taken entries submitted offline. /// Each entry: {id, request_id, user_id, action_taken} final offlinePendingIsrActionsProvider = StateProvider>>((ref) => const []); // --------------------------------------------------------------------------- // Stream providers // --------------------------------------------------------------------------- final itServiceRequestsProvider = StreamProvider>((ref) { final userId = ref.watch(currentUserIdProvider); if (userId == null) return const Stream.empty(); final client = ref.watch(supabaseClientProvider); final profile = ref.watch(currentProfileProvider).valueOrNull; final userOfficesAsync = ref.watch(userOfficesProvider); final pendingNew = ref.watch(offlinePendingItServiceRequestsProvider); final pendingUpdates = ref.watch(offlinePendingItServiceRequestUpdatesProvider); List applyPending(List rows) { final byId = {for (final r in rows) r.id: r}; pendingUpdates.forEach((id, fields) { final existing = byId[id]; if (existing == null) return; byId[id] = _applyUpdateFields(existing, fields); }); for (final p in pendingNew) { byId.putIfAbsent(p.id, () => p); } return byId.values.toList() ..sort((a, b) => b.createdAt.compareTo(a.createdAt)); } final wrapper = StreamRecoveryWrapper( stream: client .from('it_service_requests') .stream(primaryKey: ['id']) .order('created_at', ascending: false), onPollData: () async { final data = await client .from('it_service_requests') .select() .order('created_at', ascending: false) .range(0, 199); return data.map(ItServiceRequest.fromMap).toList(); }, fromMap: ItServiceRequest.fromMap, channelName: 'it_service_requests', onStatusChanged: ref.read(realtimeControllerProvider).handleChannelStatus, onOfflineData: () async { final all = await cachedListFromBrick(); all.sort((a, b) => b.createdAt.compareTo(a.createdAt)); return applyPending(all); }, onCacheMirror: (rows) => mirrorBatchToBrick(rows, tag: 'it_service_requests'), ); ref.onDispose(wrapper.dispose); // Reconnect-replay: POST queued items directly, bypassing Brick's queue. ref.listen(isOnlineProvider, (wasOnline, isNowOnline) async { if (wasOnline == true || !isNowOnline) return; // 1. Replay offline-created requests (via RPC) final pendingRaw = List>.from( ref.read(offlinePendingItServiceRequestRawProvider), ); if (pendingRaw.isNotEmpty) { debugPrint( '[itServiceRequestsProvider] reconnected — syncing ${pendingRaw.length} request(s)', ); final syncedIds = []; for (final params in pendingRaw) { final id = params['p_id'] as String; try { await client.rpc( 'insert_it_service_request_with_number', params: params, ); syncedIds.add(id); } catch (e) { final msg = e.toString(); if (msg.contains('23505') || msg.contains('duplicate key')) { syncedIds.add(id); } else { debugPrint( '[itServiceRequestsProvider] failed to sync id=$id: $e', ); } } } if (syncedIds.isNotEmpty) { final remainingRaw = ref .read(offlinePendingItServiceRequestRawProvider) .where((p) => !syncedIds.contains(p['p_id'] as String)) .toList(); ref.read(offlinePendingItServiceRequestRawProvider.notifier).state = List.unmodifiable(remainingRaw); final remainingModels = ref .read(offlinePendingItServiceRequestsProvider) .where((r) => !syncedIds.contains(r.id)) .toList(); ref.read(offlinePendingItServiceRequestsProvider.notifier).state = List.unmodifiable(remainingModels); } } // 2. Replay offline field/status updates final pendingUpdatesMap = Map>.from( ref.read(offlinePendingItServiceRequestUpdatesProvider), ); if (pendingUpdatesMap.isNotEmpty) { debugPrint( '[itServiceRequestsProvider] reconnected — syncing ${pendingUpdatesMap.length} update(s)', ); final syncedIds = []; for (final entry in pendingUpdatesMap.entries) { try { await client .from('it_service_requests') .update(entry.value) .eq('id', entry.key); syncedIds.add(entry.key); } catch (e) { debugPrint( '[itServiceRequestsProvider] failed to sync update id=${entry.key}: $e', ); } } if (syncedIds.isNotEmpty) { final remaining = Map>.from( ref.read(offlinePendingItServiceRequestUpdatesProvider), ); for (final id in syncedIds) { remaining.remove(id); } ref.read(offlinePendingItServiceRequestUpdatesProvider.notifier).state = remaining; } } // 3. Replay pending action-taken entries final pendingActions = List>.from( ref.read(offlinePendingIsrActionsProvider), ); if (pendingActions.isNotEmpty) { debugPrint( '[itServiceRequestsProvider] reconnected — syncing ${pendingActions.length} action(s)', ); final syncedIds = []; for (final action in pendingActions) { final id = action['id'] as String; try { await client .from('it_service_request_actions') .upsert(action, onConflict: 'request_id,user_id'); syncedIds.add(id); } catch (e) { final msg = e.toString(); if (msg.contains('23505') || msg.contains('duplicate key')) { syncedIds.add(id); } else { debugPrint( '[itServiceRequestsProvider] failed to sync action id=$id: $e', ); } } } if (syncedIds.isNotEmpty) { final remaining = ref .read(offlinePendingIsrActionsProvider) .where((a) => !syncedIds.contains(a['id'] as String)) .toList(); ref.read(offlinePendingIsrActionsProvider.notifier).state = List.unmodifiable(remaining); } } }); return wrapper.stream.map((result) { var items = applyPending(result.data); if (profile != null && profile.role == 'standard') { final officeIds = (userOfficesAsync.valueOrNull ?? []) .where((a) => a.userId == userId) .map((a) => a.officeId) .toSet(); items = items .where( (r) => r.creatorId == userId || (r.officeId != null && officeIds.contains(r.officeId)), ) .toList(); } return items; }); }); final itServiceRequestByIdProvider = Provider.family( (ref, id) { final requests = ref.watch(itServiceRequestsProvider).valueOrNull; if (requests == null) return null; try { return requests.firstWhere((r) => r.id == id); } catch (_) { return null; } }, ); final itServiceRequestAssignmentsProvider = StreamProvider>((ref) { final userId = ref.watch(currentUserIdProvider); if (userId == null) return const Stream.empty(); final client = ref.watch(supabaseClientProvider); final wrapper = StreamRecoveryWrapper( stream: client .from('it_service_request_assignments') .stream(primaryKey: ['id']) .order('created_at', ascending: false), onPollData: () async { final data = await client .from('it_service_request_assignments') .select() .order('created_at', ascending: false); return data.map(ItServiceRequestAssignment.fromMap).toList(); }, fromMap: ItServiceRequestAssignment.fromMap, channelName: 'it_service_request_assignments', onStatusChanged: ref .read(realtimeControllerProvider) .handleChannelStatus, onOfflineData: () async { final all = await cachedListFromBrick(); all.sort((a, b) => b.createdAt.compareTo(a.createdAt)); return all; }, onCacheMirror: (rows) => mirrorBatchToBrick( rows, tag: 'isr_assignments', ), ); ref.onDispose(wrapper.dispose); return wrapper.stream.map((result) => result.data); }); final itServiceRequestActivityLogsProvider = StreamProvider.family, String>(( ref, requestId, ) { final client = ref.watch(supabaseClientProvider); final wrapper = StreamRecoveryWrapper( stream: client .from('it_service_request_activity_logs') .stream(primaryKey: ['id']) .eq('request_id', requestId) .order('created_at', ascending: false), onPollData: () async { final data = await client .from('it_service_request_activity_logs') .select() .eq('request_id', requestId) .order('created_at', ascending: false); return data.map(ItServiceRequestActivityLog.fromMap).toList(); }, fromMap: ItServiceRequestActivityLog.fromMap, channelName: 'isr_activity_logs_$requestId', onStatusChanged: ref .read(realtimeControllerProvider) .handleChannelStatus, onOfflineData: () async { final all = await cachedListFromBrick(); final filtered = all .where((l) => l.requestId == requestId) .toList() ..sort((a, b) => b.createdAt.compareTo(a.createdAt)); return filtered; }, onCacheMirror: (rows) => mirrorBatchToBrick( rows, tag: 'isr_activity_logs', ), ); ref.onDispose(wrapper.dispose); return wrapper.stream.map((result) => result.data); }); final itServiceRequestActionsProvider = StreamProvider.family, String>(( ref, requestId, ) { final client = ref.watch(supabaseClientProvider); final wrapper = StreamRecoveryWrapper( stream: client .from('it_service_request_actions') .stream(primaryKey: ['id']) .eq('request_id', requestId) .order('created_at', ascending: false), onPollData: () async { final data = await client .from('it_service_request_actions') .select() .eq('request_id', requestId) .order('created_at', ascending: false); return data.map(ItServiceRequestAction.fromMap).toList(); }, fromMap: ItServiceRequestAction.fromMap, channelName: 'isr_actions_$requestId', onStatusChanged: ref .read(realtimeControllerProvider) .handleChannelStatus, onOfflineData: () async { final all = await cachedListFromBrick(); final filtered = all .where((a) => a.requestId == requestId) .toList() ..sort((a, b) => b.createdAt.compareTo(a.createdAt)); return filtered; }, onCacheMirror: (rows) => mirrorBatchToBrick( rows, tag: 'isr_actions', ), ); ref.onDispose(wrapper.dispose); return wrapper.stream.map((result) => result.data); }); // --------------------------------------------------------------------------- // Controller // --------------------------------------------------------------------------- final itServiceRequestControllerProvider = Provider( (ref) { final client = ref.watch(supabaseClientProvider); return ItServiceRequestController(client, ref); }, ); class ItServiceRequestController { ItServiceRequestController(this._client, [this._ref]); final SupabaseClient _client; final Ref? _ref; /// Creates a new IT Service Request with auto-generated number. /// /// Returns `{'id': ..., 'request_number': ...}` on success. If offline, /// queues locally and returns `{'id': localId, 'request_number': null, 'pending': true}`. Future> createRequest({ required String eventName, required List services, String? servicesOther, String? officeId, String? requestedBy, String? requestedByUserId, String status = 'draft', }) async { final userId = _client.auth.currentUser?.id; if (userId == null) throw Exception('Not authenticated'); final offlineId = const Uuid().v4(); final rpcParams = { 'p_id': offlineId, 'p_event_name': eventName, 'p_services': services, 'p_creator_id': userId, 'p_office_id': officeId, 'p_requested_by': requestedBy, 'p_requested_by_user_id': requestedByUserId, 'p_status': status, }; try { final result = await _client.rpc( 'insert_it_service_request_with_number', params: rpcParams, ); final row = result is List ? (result.first as Map) : result; final requestId = row['id'] as String; if (servicesOther != null && servicesOther.isNotEmpty) { await _client .from('it_service_requests') .update({'services_other': servicesOther}) .eq('id', requestId); } await _client.from('it_service_request_activity_logs').insert({ 'request_id': requestId, 'actor_id': userId, 'action_type': 'created', }); return {'id': requestId, 'request_number': row['request_number']}; } catch (e) { if (!isOfflineSaveError(e)) { debugPrint('createRequest error: $e'); rethrow; } final now = DateTime.now().toUtc(); final local = ItServiceRequest( id: offlineId, services: services, eventName: eventName, status: status, outsidePremiseAllowed: false, createdAt: now, updatedAt: now, creatorId: userId, officeId: officeId, requestedBy: requestedBy, requestedByUserId: requestedByUserId, servicesOther: servicesOther, ); _queuePendingRequest(local, rpcParams); debugPrint( '[ItServiceRequestController] createRequest queued offline: $offlineId', ); return {'id': offlineId, 'request_number': null, 'pending': true}; } } /// Updates IT Service Request fields. Offline: queues the field patch. Future updateRequest({ required String requestId, String? eventName, List? services, String? servicesOther, String? eventDetails, DateTime? eventDate, DateTime? eventEndDate, DateTime? dryRunDate, DateTime? dryRunEndDate, String? contactPerson, String? contactNumber, String? remarks, String? officeId, String? requestedBy, String? requestedByUserId, String? approvedBy, String? approvedByUserId, bool? outsidePremiseAllowed, DateTime? dateTimeReceived, DateTime? dateTimeChecked, }) async { final updates = {}; if (eventName != null) updates['event_name'] = eventName; if (services != null) updates['services'] = services; if (servicesOther != null) updates['services_other'] = servicesOther; if (eventDetails != null) updates['event_details'] = eventDetails; if (eventDate != null) updates['event_date'] = eventDate.toIso8601String(); if (eventEndDate != null) { updates['event_end_date'] = eventEndDate.toIso8601String(); } if (dryRunDate != null) { updates['dry_run_date'] = dryRunDate.toIso8601String(); } if (dryRunEndDate != null) { updates['dry_run_end_date'] = dryRunEndDate.toIso8601String(); } if (contactPerson != null) updates['contact_person'] = contactPerson; if (contactNumber != null) updates['contact_number'] = contactNumber; if (remarks != null) updates['remarks'] = remarks; if (officeId != null) updates['office_id'] = officeId; if (requestedBy != null) updates['requested_by'] = requestedBy; if (requestedByUserId != null) { updates['requested_by_user_id'] = requestedByUserId; } if (approvedBy != null) updates['approved_by'] = approvedBy; if (approvedByUserId != null) { updates['approved_by_user_id'] = approvedByUserId; } if (outsidePremiseAllowed != null) { updates['outside_premise_allowed'] = outsidePremiseAllowed; } if (dateTimeReceived != null) { updates['date_time_received'] = dateTimeReceived.toIso8601String(); } if (dateTimeChecked != null) { updates['date_time_checked'] = dateTimeChecked.toIso8601String(); } if (updates.isEmpty) return; try { await _client .from('it_service_requests') .update(updates) .eq('id', requestId); final userId = _client.auth.currentUser?.id; await _client.from('it_service_request_activity_logs').insert({ 'request_id': requestId, 'actor_id': userId, 'action_type': 'updated', 'meta': {'fields': updates.keys.toList()}, }); } catch (e) { if (!isOfflineSaveError(e)) rethrow; _queueRequestUpdate(requestId, updates); } } /// Update only the status of an IT Service Request. Offline: queues. Future updateStatus({ required String requestId, required String status, String? cancellationReason, }) async { final userId = _client.auth.currentUser?.id; final updates = {'status': status}; if (status == 'scheduled') { updates['approved_at'] = DateTime.now().toUtc().toIso8601String(); updates['approved_by_user_id'] = userId; } if (status == 'completed') { updates['completed_at'] = DateTime.now().toUtc().toIso8601String(); } if (status == 'cancelled') { updates['cancelled_at'] = DateTime.now().toUtc().toIso8601String(); if (cancellationReason != null) { updates['cancellation_reason'] = cancellationReason; } } try { await _client .from('it_service_requests') .update(updates) .eq('id', requestId); await _client.from('it_service_request_activity_logs').insert({ 'request_id': requestId, 'actor_id': userId, 'action_type': 'status_changed', 'meta': {'status': status}, }); } catch (e) { if (!isOfflineSaveError(e)) rethrow; _queueRequestUpdate(requestId, updates); } } /// Approve a request (admin only). Offline: queues the status patch. Future approveRequest({ required String requestId, required String approverName, }) async { final userId = _client.auth.currentUser?.id; final updates = { 'status': 'scheduled', 'approved_by': approverName, 'approved_by_user_id': userId, 'approved_at': DateTime.now().toUtc().toIso8601String(), }; try { await _client .from('it_service_requests') .update(updates) .eq('id', requestId); await _client.from('it_service_request_activity_logs').insert({ 'request_id': requestId, 'actor_id': userId, 'action_type': 'approved', }); } catch (e) { if (!isOfflineSaveError(e)) rethrow; _queueRequestUpdate(requestId, updates); } } // ----------------------------------------------------------------------- // Assignment management (online-only — admin/dispatcher function) // ----------------------------------------------------------------------- Future assignStaff({ required String requestId, required List userIds, }) async { final actorId = _client.auth.currentUser?.id; final rows = userIds .map((uid) => {'request_id': requestId, 'user_id': uid}) .toList(); await _client .from('it_service_request_assignments') .upsert(rows, onConflict: 'request_id,user_id'); await _client.from('it_service_request_activity_logs').insert({ 'request_id': requestId, 'actor_id': actorId, 'action_type': 'assigned', 'meta': {'user_ids': userIds}, }); } Future unassignStaff({ required String requestId, required String userId, }) async { final actorId = _client.auth.currentUser?.id; await _client .from('it_service_request_assignments') .delete() .eq('request_id', requestId) .eq('user_id', userId); await _client.from('it_service_request_activity_logs').insert({ 'request_id': requestId, 'actor_id': actorId, 'action_type': 'unassigned', 'meta': {'user_id': userId}, }); } // ----------------------------------------------------------------------- // Action Taken — offline-safe // ----------------------------------------------------------------------- Future createOrUpdateAction({ required String requestId, required String actionTaken, }) async { final userId = _client.auth.currentUser?.id; if (userId == null) throw Exception('Not authenticated'); final actionId = const Uuid().v4(); try { final existing = await _client .from('it_service_request_actions') .select('id') .eq('request_id', requestId) .eq('user_id', userId) .maybeSingle(); if (existing != null) { await _client .from('it_service_request_actions') .update({'action_taken': actionTaken}) .eq('id', existing['id']); return existing['id'] as String; } else { final result = await _client .from('it_service_request_actions') .insert({ 'id': actionId, 'request_id': requestId, 'user_id': userId, 'action_taken': actionTaken, }) .select('id') .single(); return result['id'] as String; } } catch (e) { if (!isOfflineSaveError(e)) rethrow; _queuePendingAction({ 'id': actionId, 'request_id': requestId, 'user_id': userId, 'action_taken': actionTaken, }); return actionId; } } // ----------------------------------------------------------------------- // Evidence Attachments // ----------------------------------------------------------------------- Future uploadEvidence({ required String requestId, required String actionId, required String fileName, required Uint8List bytes, DateTime? takenAt, }) async { final userId = _client.auth.currentUser?.id; if (userId == null) throw Exception('Not authenticated'); final path = '$requestId/evidence/$fileName'; if (kIsWeb) { await _client.storage .from('it_service_attachments') .uploadBinary( path, bytes, fileOptions: const FileOptions(upsert: true), ); } else { final tmpDir = Directory.systemTemp; final tmpFile = File('${tmpDir.path}/$fileName'); await tmpFile.writeAsBytes(bytes); await _client.storage .from('it_service_attachments') .upload(path, tmpFile, fileOptions: const FileOptions(upsert: true)); try { await tmpFile.delete(); } catch (_) {} } await _client.from('it_service_request_evidence').insert({ 'request_id': requestId, 'action_id': actionId, 'user_id': userId, 'file_path': path, 'file_name': fileName, 'taken_at': takenAt?.toIso8601String(), }); return _client.storage.from('it_service_attachments').getPublicUrl(path); } Future deleteEvidence({required String evidenceId}) async { final row = await _client .from('it_service_request_evidence') .select('file_path') .eq('id', evidenceId) .single(); final path = row['file_path'] as String; await _client.storage.from('it_service_attachments').remove([path]); await _client .from('it_service_request_evidence') .delete() .eq('id', evidenceId); } // ----------------------------------------------------------------------- // File Attachments (event files, max 25MB) // ----------------------------------------------------------------------- Future uploadAttachment({ required String requestId, required String fileName, required Uint8List bytes, }) async { if (bytes.length > 25 * 1024 * 1024) { throw Exception('File size exceeds 25MB limit'); } final path = '$requestId/attachments/$fileName'; if (kIsWeb) { await _client.storage .from('it_service_attachments') .uploadBinary( path, bytes, fileOptions: const FileOptions(upsert: true), ); } else { final tmpDir = Directory.systemTemp; final tmpFile = File('${tmpDir.path}/$fileName'); await tmpFile.writeAsBytes(bytes); await _client.storage .from('it_service_attachments') .upload(path, tmpFile, fileOptions: const FileOptions(upsert: true)); try { await tmpFile.delete(); } catch (_) {} } return _client.storage.from('it_service_attachments').getPublicUrl(path); } Future>> listAttachments(String requestId) async { try { final files = await _client.storage .from('it_service_attachments') .list(path: '$requestId/attachments'); return files .map( (f) => { 'name': f.name, 'url': _client.storage .from('it_service_attachments') .getPublicUrl('$requestId/attachments/${f.name}'), }, ) .toList(); } catch (_) { return []; } } Future deleteAttachment({ required String requestId, required String fileName, }) async { final path = '$requestId/attachments/$fileName'; await _client.storage.from('it_service_attachments').remove([path]); } Future>> listEvidence(String requestId) async { final rows = await _client .from('it_service_request_evidence') .select() .eq('request_id', requestId) .order('created_at', ascending: false); return rows .map( (r) => { 'id': r['id'], 'file_name': r['file_name'], 'file_path': r['file_path'], 'taken_at': r['taken_at'], 'url': _client.storage .from('it_service_attachments') .getPublicUrl(r['file_path'] as String), }, ) .toList(); } // ----------------------------------------------------------------------- // Queue helpers // ----------------------------------------------------------------------- void _queuePendingRequest( ItServiceRequest model, Map rpcParams, ) { final ref = _ref; if (ref == null) return; final currentModels = List.from( ref.read(offlinePendingItServiceRequestsProvider), ); currentModels.add(model); ref.read(offlinePendingItServiceRequestsProvider.notifier).state = List.unmodifiable(currentModels); final currentRaw = List>.from( ref.read(offlinePendingItServiceRequestRawProvider), ); currentRaw.add(rpcParams); ref.read(offlinePendingItServiceRequestRawProvider.notifier).state = List.unmodifiable(currentRaw); mirrorBatchToBrick([model], tag: 'offline_isr'); } void _queueRequestUpdate(String requestId, Map fields) { final ref = _ref; if (ref == null) return; final current = Map>.from( ref.read(offlinePendingItServiceRequestUpdatesProvider), ); current[requestId] = {...?current[requestId], ...fields}; ref.read(offlinePendingItServiceRequestUpdatesProvider.notifier).state = current; } void _queuePendingAction(Map action) { final ref = _ref; if (ref == null) return; final current = List>.from( ref.read(offlinePendingIsrActionsProvider), ); // Replace existing entry for the same request+user if present final existing = current.indexWhere( (a) => a['request_id'] == action['request_id'] && a['user_id'] == action['user_id'], ); if (existing >= 0) { current[existing] = action; } else { current.add(action); } ref.read(offlinePendingIsrActionsProvider.notifier).state = List.unmodifiable(current); } } // --------------------------------------------------------------------------- // Field-patch helper (avoids copyWith pattern on a model with 25+ fields) // --------------------------------------------------------------------------- ItServiceRequest _applyUpdateFields( ItServiceRequest r, Map f, ) { DateTime? parseOpt(String key) { if (!f.containsKey(key)) return null; final v = f[key]; return v == null ? null : AppTime.parse(v as String); } return ItServiceRequest( id: r.id, requestNumber: r.requestNumber, services: f.containsKey('services') ? (f['services'] as List).cast() : r.services, servicesOther: f.containsKey('services_other') ? f['services_other'] as String? : r.servicesOther, eventName: (f['event_name'] as String?) ?? r.eventName, eventDetails: f.containsKey('event_details') ? f['event_details'] as String? : r.eventDetails, eventDate: f.containsKey('event_date') ? parseOpt('event_date') : r.eventDate, eventEndDate: f.containsKey('event_end_date') ? parseOpt('event_end_date') : r.eventEndDate, dryRunDate: f.containsKey('dry_run_date') ? parseOpt('dry_run_date') : r.dryRunDate, dryRunEndDate: f.containsKey('dry_run_end_date') ? parseOpt('dry_run_end_date') : r.dryRunEndDate, contactPerson: f.containsKey('contact_person') ? f['contact_person'] as String? : r.contactPerson, contactNumber: f.containsKey('contact_number') ? f['contact_number'] as String? : r.contactNumber, remarks: f.containsKey('remarks') ? f['remarks'] as String? : r.remarks, officeId: f.containsKey('office_id') ? f['office_id'] as String? : r.officeId, requestedBy: f.containsKey('requested_by') ? f['requested_by'] as String? : r.requestedBy, requestedByUserId: f.containsKey('requested_by_user_id') ? f['requested_by_user_id'] as String? : r.requestedByUserId, approvedBy: f.containsKey('approved_by') ? f['approved_by'] as String? : r.approvedBy, approvedByUserId: f.containsKey('approved_by_user_id') ? f['approved_by_user_id'] as String? : r.approvedByUserId, approvedAt: f.containsKey('approved_at') ? parseOpt('approved_at') : r.approvedAt, status: (f['status'] as String?) ?? r.status, outsidePremiseAllowed: (f['outside_premise_allowed'] as bool?) ?? r.outsidePremiseAllowed, cancellationReason: f.containsKey('cancellation_reason') ? f['cancellation_reason'] as String? : r.cancellationReason, cancelledAt: f.containsKey('cancelled_at') ? parseOpt('cancelled_at') : r.cancelledAt, creatorId: r.creatorId, createdAt: r.createdAt, updatedAt: r.updatedAt, completedAt: f.containsKey('completed_at') ? parseOpt('completed_at') : r.completedAt, dateTimeReceived: f.containsKey('date_time_received') ? parseOpt('date_time_received') : r.dateTimeReceived, dateTimeChecked: f.containsKey('date_time_checked') ? parseOpt('date_time_checked') : r.dateTimeChecked, ); }