import 'dart:async'; import 'dart:developer'; import 'package:flutter/cupertino.dart'; import 'package:hmg_qline/config/dependency_injection.dart'; import 'package:hmg_qline/constants/app_constants.dart'; import 'package:hmg_qline/models/global_config_model.dart'; import 'package:hmg_qline/models/ticket_model.dart'; import 'package:hmg_qline/repositories/screen_details_repo.dart'; import 'package:hmg_qline/repositories/signalR_repo.dart'; import 'package:hmg_qline/services/audio_service.dart'; import 'package:hmg_qline/services/cache_service.dart'; import 'package:hmg_qline/services/logger_service.dart'; import 'package:hmg_qline/services/text_to_speech_service.dart'; import 'package:hmg_qline/services/crash_handler_service.dart'; import 'package:hmg_qline/utilities/enums.dart'; import 'package:hmg_qline/view_models/screen_config_view_model.dart'; class QueuingViewModel extends ChangeNotifier { final ScreenDetailsRepo screenDetailsRepo; final SignalrRepo signalrRepo; final CacheService cacheService; final AudioService audioService; final TextToSpeechService textToSpeechService; final LoggerService loggerService; QueuingViewModel({ required this.screenDetailsRepo, required this.signalrRepo, required this.cacheService, required this.audioService, required this.textToSpeechService, required this.loggerService, }); Future initializeQueueingVM() async { try { await startHubConnection(); initializeAudioPlayer(); initializeTextToSpeech(); } catch (e, st) { await getIt.get().handleCrash(error: e, stackTrace: st, context: 'initializeQueueingVM -> queuing_view_model.dart'); } } Future startHubConnection() async { try { ScreenConfigViewModel screenConfigViewModel = getIt.get(); return await signalrRepo.startHubConnection( deviceIp: screenConfigViewModel.currentScreenIP, onHubTicketCall: onHubTicketCall, onHubConfigCall: onHubConfigCall, onHubReconnected: onHubReconnected, onHubDisconnected: onHubDisconnected, onHubConnecting: onHubConnecting, ); } catch (e, st) { await getIt.get().handleCrash(error: e, stackTrace: st, context: 'startHubConnection -> queuing_view_model.dart'); return false; } } Future stopHubConnection() async { return await signalrRepo.closeConnection(); } initializeAudioPlayer() { audioService.listenAudioPlayerEvents(onToneCompleted: onToneCompleted); } initializeTextToSpeech() { textToSpeechService.listenToTextToSpeechEvents(onVoiceCompleted: onVoiceCompleted); } Future onHubConfigCall(var response) async { loggerService.logToFile(message: response.toString(), source: "onHubConfigCall -> queueing_view_model.dart ", type: LogTypeEnum.data); if (response != null && response.isNotEmpty) { var data = response.first['data']; GlobalConfigurationsModel globalConfigurationsModel = GlobalConfigurationsModel.fromJson( json: data, qType: response.first['qType'] ?? 1, screenType: response.first['screenType'] ?? 1, ); log("onHubConfigCall: ${globalConfigurationsModel.toString()}"); ScreenConfigViewModel screenConfigViewModel = getIt.get(); screenConfigViewModel.updateGlobalConfigurationsModel(value: globalConfigurationsModel, needNotify: true, shouldUpdateNextPrayer: true); } } Future onHubTicketCall(List? response) async { try { loggerService.logToFile(message: response.toString(), source: "onHubTicketCall -> queueing_view_model.dart ", type: LogTypeEnum.data); log("onHubTicketCall: $response"); log("isCallingInProgress: $isCallingInProgress"); if (response != null && response.isNotEmpty) { TicketDetailsModel ticketDetailsModel = TicketDetailsModel.fromJson(response.first as Map); addNewTicket(ticketDetailsModel); } } catch (e, st) { await getIt.get().handleCrash(error: e, stackTrace: st, context: 'onHubTicketCall -> queuing_view_model.dart'); } } Future onHubReconnected(var response) async { log("onHubConnected: $response"); loggerService.logToFile(message: response.toString(), source: "onHubReconnected -> queueing_view_model.dart ", type: LogTypeEnum.connectivity); ScreenConfigViewModel screenConfigViewModel = getIt.get(); screenConfigViewModel.updateIsHubConnected(true); screenConfigViewModel.notifyListeners(); } Future onHubDisconnected(var response) async { log("onHubDisconnected: $response"); loggerService.logToFile(message: response.toString(), source: "onHubDisconnected -> queueing_view_model.dart ", type: LogTypeEnum.connectivity); ScreenConfigViewModel screenConfigViewModel = getIt.get(); screenConfigViewModel.updateIsHubConnected(false); screenConfigViewModel.notifyListeners(); } Future onHubConnecting(var response) async => log("onHubConnecting: $response"); Future onToneCompleted() async { GlobalConfigurationsModel globalConfigurationsModel = getIt.get().globalConfigurationsModel; await textToSpeechService.speechText(globalConfigurationsModel: globalConfigurationsModel, ticket: currentTickets.first, isMute: !(globalConfigurationsModel.isVoiceReq)); } Future onVoiceCompleted() async { log("isSpeechCompleted::: $isSpeechCompleted"); if (isSpeechCompleted) { waitAndCallNextTicketIfAvailable(); } } waitAndCallNextTicketIfAvailable() { GlobalConfigurationsModel globalConfigurationsModel = getIt.get().globalConfigurationsModel; Timer(Duration(seconds: globalConfigurationsModel.concurrentCallDelaySec), () async { if (queueTickets.isNotEmpty) { int index = itemAlreadyAvailableAtIndex(ticketToSearch: queueTickets.first, listToSearchIn: currentTickets); if (index != -1) { currentTickets.removeAt(index); } currentTickets.insert(0, queueTickets.first); if (currentTickets.length > globalConfigurationsModel.screenMaxDisplayPatients) { currentTickets.removeLast(); } notifyListeners(); queueTickets.removeAt(0); callTicketOnScreen(ticketData: currentTickets.first.ticketModel); } else { isCallingInProgress = false; } }); } int itemAlreadyAvailableAtIndex({required TicketDetailsModel ticketToSearch, required List listToSearchIn}) { int isFoundAt = -1; try { isFoundAt = listToSearchIn.indexWhere((ticket) { log("ticket.ticketModel!.queueNo: ${ticket.ticketModel!.queueNo}"); log("ticketToSearch.ticketModel!.queueNo: ${ticketToSearch.ticketModel!.queueNo}"); return ticket.ticketModel!.queueNo == ticketToSearch.ticketModel!.queueNo; }); log("itemAlreadyAvailableAtIndex: $isFoundAt"); return isFoundAt; } catch (e) { log("Error isItemAlreadyAvailableInList: ${e.toString()}"); return isFoundAt; } } bool isCallingInProgress = false; // Tickets Management List currentTickets = []; List queueTickets = []; void addNewTicket(TicketDetailsModel ticket) { if (!isCallingInProgress) { int index = itemAlreadyAvailableAtIndex(ticketToSearch: ticket, listToSearchIn: currentTickets); if (index != -1) { currentTickets.removeAt(index); } currentTickets.insert(0, ticket); GlobalConfigurationsModel globalConfigurationsModel = getIt.get().globalConfigurationsModel; if (currentTickets.length > globalConfigurationsModel.screenMaxDisplayPatients) { currentTickets.removeLast(); } callTicketOnScreen(ticketData: currentTickets.first.ticketModel); } else { int index = itemAlreadyAvailableAtIndex(ticketToSearch: ticket, listToSearchIn: queueTickets); if (index != -1) { queueTickets.removeAt(index); } queueTickets.add(ticket); log("inQueue Length: ${queueTickets.length}"); } notifyListeners(); } Future callTicketOnScreen({required TicketData? ticketData}) async { if (ticketData == null) return; ScreenConfigViewModel screenConfigViewModel = getIt.get(); GlobalConfigurationsModel globalConfigurationsModel = screenConfigViewModel.globalConfigurationsModel; if (globalConfigurationsModel.qTypeEnum == QTypeEnum.appointment) { screenConfigViewModel.acknowledgeTicketForAppointmentOnly( ticketQueueID: ticketData.id ?? 0, ipAddress: screenConfigViewModel.currentScreenIP, callTypeEnum: ticketData.callTypeEnum, ); } else { screenConfigViewModel.acknowledgeTicket(ticketQueueID: ticketData.id ?? 0, ipAddress: screenConfigViewModel.currentScreenIP, ); } log("globalConfigurationsModel: ${globalConfigurationsModel.toString()}"); isCallingInProgress = true; await audioService.playTone(path: AppAssets.callTone, isMute: !(globalConfigurationsModel.isToneReq)); } List> memoryHog = []; void triggerOOM() { while (true) { // Repeatedly add large chunks (e.g., 10 million integers) to a list // This prevents the Garbage Collector (GC) from reclaiming the memory memoryHog.add(List.filled(10000000, 9)); } } /// Throws an exception from an async context (await) to test runZonedGuarded handling. Future triggerAsyncCrash() async { await Future.delayed(const Duration(milliseconds: 50)); throw Exception('Test asynchronous crash from QueuingViewModel.triggerAsyncCrash'); } /// Creates an unawaited Future that throws to simulate an unhandled async error. void triggerUnhandledFutureCrash() { Future.delayed(const Duration(milliseconds: 50), () { throw Exception('Test unhandled future crash from QueuingViewModel.triggerUnhandledFutureCrash'); }); } }