|
|
|
|
import 'dart:developer';
|
|
|
|
|
import 'dart:async';
|
|
|
|
|
import 'package:flutter/foundation.dart';
|
|
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
|
import 'package:hmg_qline/config/dependency_injection.dart';
|
|
|
|
|
import 'package:hmg_qline/constants/app_constants.dart';
|
|
|
|
|
import 'package:hmg_qline/models/generic_response_model.dart';
|
|
|
|
|
import 'package:hmg_qline/models/global_config_model.dart';
|
|
|
|
|
import 'package:hmg_qline/models/kiosk_language_config_model.dart';
|
|
|
|
|
import 'package:hmg_qline/models/kiosk_queue_model.dart';
|
|
|
|
|
import 'package:hmg_qline/models/kiosk_ticket_model.dart';
|
|
|
|
|
import 'package:hmg_qline/models/prayers_widget_model.dart';
|
|
|
|
|
import 'package:hmg_qline/models/rss_feed_model.dart';
|
|
|
|
|
import 'package:hmg_qline/models/weathers_widget_model.dart';
|
|
|
|
|
import 'package:hmg_qline/repositories/screen_details_repo.dart';
|
|
|
|
|
import 'package:hmg_qline/repositories/signalR_repo.dart';
|
|
|
|
|
import 'package:hmg_qline/services/cache_service.dart';
|
|
|
|
|
import 'package:hmg_qline/services/connectivity_service.dart';
|
|
|
|
|
import 'package:hmg_qline/services/logger_service.dart';
|
|
|
|
|
import 'package:hmg_qline/utilities/enums.dart';
|
|
|
|
|
import 'package:hmg_qline/utilities/extensions.dart';
|
|
|
|
|
import 'package:hmg_qline/utilities/native_method_handler.dart';
|
|
|
|
|
import 'package:hmg_qline/view_models/queuing_view_model.dart';
|
|
|
|
|
import 'package:hmg_qline/views/view_helpers/info_components.dart';
|
|
|
|
|
import 'package:qr_code_scanner_plus/qr_code_scanner_plus.dart';
|
|
|
|
|
import 'package:wakelock_plus/wakelock_plus.dart';
|
|
|
|
|
// import 'package:timezone/browser.dart' as tz;
|
|
|
|
|
|
|
|
|
|
class ScreenConfigViewModel extends ChangeNotifier {
|
|
|
|
|
final ScreenDetailsRepo screenDetailsRepo;
|
|
|
|
|
final CacheService cacheService;
|
|
|
|
|
final ConnectivityService connectivityService;
|
|
|
|
|
final LoggerService loggerService;
|
|
|
|
|
final NativeMethodChannelService nativeMethodChannelService;
|
|
|
|
|
|
|
|
|
|
ScreenConfigViewModel({
|
|
|
|
|
required this.screenDetailsRepo,
|
|
|
|
|
required this.cacheService,
|
|
|
|
|
required this.connectivityService,
|
|
|
|
|
required this.loggerService,
|
|
|
|
|
required this.nativeMethodChannelService,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
Future<void> initializeScreenConfigVM() async {
|
|
|
|
|
appStartTime = DateTime.now(); // Record when app actually started
|
|
|
|
|
await getGlobalConfigurationsByIP();
|
|
|
|
|
await getLastTimeUpdatedFromCache();
|
|
|
|
|
await getLastTimeLogsClearedFromCache();
|
|
|
|
|
listenNetworkConnectivity();
|
|
|
|
|
initializeTimer();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Future<void> onAppResumed() async {
|
|
|
|
|
final uptimeHours = DateTime.now().difference(appStartTime).inHours;
|
|
|
|
|
loggerService.logToFile(
|
|
|
|
|
message: "[didChangeAppLifecycleState] : [onAppResumed] - App uptime: ${uptimeHours}h",
|
|
|
|
|
source: "onAppResumed -> screen_config_view_model.dart",
|
|
|
|
|
type: LogTypeEnum.data,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Re-enable wakelock when resuming
|
|
|
|
|
try {
|
|
|
|
|
await WakelockPlus.enable();
|
|
|
|
|
} catch (e) {
|
|
|
|
|
loggerService.logError("Failed to enable wakelock on resume: $e");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Verify connections are still alive
|
|
|
|
|
syncHubConnectionState();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Future<void> onAppPaused() async {
|
|
|
|
|
final uptimeHours = DateTime.now().difference(appStartTime).inHours;
|
|
|
|
|
loggerService.logToFile(
|
|
|
|
|
message: "[didChangeAppLifecycleState] : [onAppPaused] - App uptime: ${uptimeHours}h - WARNING: App going to background!",
|
|
|
|
|
source: "onAppPaused -> screen_config_view_model.dart",
|
|
|
|
|
type: LogTypeEnum.data,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Future<void> onAppDetached() async {
|
|
|
|
|
final uptimeHours = DateTime.now().difference(appStartTime).inHours;
|
|
|
|
|
loggerService.logToFile(
|
|
|
|
|
message: "[didChangeAppLifecycleState] : [onAppDetached] - App uptime: ${uptimeHours}h - CRITICAL: App being killed!",
|
|
|
|
|
source: "onAppDetached -> screen_config_view_model.dart",
|
|
|
|
|
type: LogTypeEnum.data,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Future<void> waitForIPAndInitializeConfigVM() async {
|
|
|
|
|
while (currentScreenIP == "") {
|
|
|
|
|
await getCurrentScreenIP();
|
|
|
|
|
if (currentScreenIP != "") {
|
|
|
|
|
initializeScreenConfigVM();
|
|
|
|
|
} else {
|
|
|
|
|
await Future.delayed(const Duration(seconds: 2));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool isInternetConnected = true;
|
|
|
|
|
|
|
|
|
|
updateIsInternetConnected(bool value) {
|
|
|
|
|
isInternetConnected = value;
|
|
|
|
|
notifyListeners();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool isHubConnected = true;
|
|
|
|
|
|
|
|
|
|
updateIsHubConnected(bool value) {
|
|
|
|
|
isHubConnected = value;
|
|
|
|
|
notifyListeners();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ViewState state = ViewState.idle;
|
|
|
|
|
|
|
|
|
|
void updateViewState(ViewState viewState) {
|
|
|
|
|
state = viewState;
|
|
|
|
|
notifyListeners();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
updateCurrentScreenRotation(ScreenOrientationEnum value) {
|
|
|
|
|
globalConfigurationsModel.orientationTypeEnum = value;
|
|
|
|
|
notifyListeners();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ScreenTypeEnum currentScreenTypeEnum = ScreenTypeEnum.waitingAreaScreen;
|
|
|
|
|
|
|
|
|
|
updateCurrentScreenTypeEnum(ScreenTypeEnum value) {
|
|
|
|
|
currentScreenTypeEnum = value;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QTypeEnum currentQTypeEnum = QTypeEnum.lab;
|
|
|
|
|
|
|
|
|
|
updateCurrentQTypeEnum(QTypeEnum value) {
|
|
|
|
|
currentQTypeEnum = value;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
String currentScreenIP = "";
|
|
|
|
|
|
|
|
|
|
Future<void> getCurrentScreenIP() async {
|
|
|
|
|
if (useTestIP) {
|
|
|
|
|
currentScreenIP = AppConstants.testIP;
|
|
|
|
|
} else {
|
|
|
|
|
currentScreenIP = await connectivityService.getCurrentScreenIP();
|
|
|
|
|
log("currentScreenIP: $currentScreenIP");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void listenNetworkConnectivity() {
|
|
|
|
|
return connectivityService.subscribeToConnectivityChange(
|
|
|
|
|
onInternetDisConnected: () {
|
|
|
|
|
updateIsInternetConnected(false);
|
|
|
|
|
updateIsHubConnected(false);
|
|
|
|
|
},
|
|
|
|
|
onInternetConnected: () {
|
|
|
|
|
updateIsInternetConnected(true);
|
|
|
|
|
QueuingViewModel queuingViewModel = getIt.get<QueuingViewModel>();
|
|
|
|
|
queuingViewModel.startHubConnection();
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
GlobalConfigurationsModel globalConfigurationsModel = GlobalConfigurationsModel();
|
|
|
|
|
|
|
|
|
|
Future<void> getGlobalConfigurationsByIP() async {
|
|
|
|
|
GlobalConfigurationsModel? response = await screenDetailsRepo.getGlobalScreenConfigurations(ipAddress: currentScreenIP);
|
|
|
|
|
|
|
|
|
|
if (response == null) {
|
|
|
|
|
loggerService.logError("response was: $response");
|
|
|
|
|
updateViewState(ViewState.error);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
updateGlobalConfigurationsModel(value: response);
|
|
|
|
|
updateCurrentScreenTypeEnum(globalConfigurationsModel.screenTypeEnum);
|
|
|
|
|
updateCurrentQTypeEnum(globalConfigurationsModel.qTypeEnum);
|
|
|
|
|
|
|
|
|
|
notifyListeners();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void updateGlobalConfigurationsModel({required var value, bool needNotify = false, bool shouldUpdateNextPrayer = false}) {
|
|
|
|
|
// Ensure the incoming value is the GlobalConfigurationsModel and set the Takhasusi flag based on current IP
|
|
|
|
|
if (value is GlobalConfigurationsModel) {
|
|
|
|
|
// IPs that belong to Takhasusi main
|
|
|
|
|
const takhasusiMainIPs = ['10.70.194.105', '10.70.194.87'];
|
|
|
|
|
try {
|
|
|
|
|
value.isFromTakhasusiMain = takhasusiMainIPs.contains(currentScreenIP);
|
|
|
|
|
} catch (_) {
|
|
|
|
|
value.isFromTakhasusiMain = false;
|
|
|
|
|
}
|
|
|
|
|
globalConfigurationsModel = value;
|
|
|
|
|
} else {
|
|
|
|
|
globalConfigurationsModel = value;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (needNotify) {
|
|
|
|
|
notifyListeners();
|
|
|
|
|
}
|
|
|
|
|
if (shouldUpdateNextPrayer) {
|
|
|
|
|
getNextPrayerToShow();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getInfoWidgetsDetailsFromServer();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Future<void> getInfoWidgetsDetailsFromServer() async {
|
|
|
|
|
if (globalConfigurationsModel.isWeatherReq) {
|
|
|
|
|
await getWeatherDetailsFromServer();
|
|
|
|
|
}
|
|
|
|
|
if (globalConfigurationsModel.isPrayerTimeReq) {
|
|
|
|
|
await getPrayerDetailsFromServer();
|
|
|
|
|
}
|
|
|
|
|
if (globalConfigurationsModel.isRssFeedReq) {
|
|
|
|
|
await getRssFeedDetailsFromServer();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int currentDate = DateTime.now().millisecondsSinceEpoch;
|
|
|
|
|
await cacheService.setLastTimeUpdatedInCache(lasTimeUpdated: currentDate.toString());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
RssFeedModel rssFeedModel = RssFeedModel();
|
|
|
|
|
|
|
|
|
|
Future<void> getRssFeedDetailsFromServer() async {
|
|
|
|
|
RssFeedModel? response = await screenDetailsRepo.getRssFeedDetailsByLanguageID(languageId: 0);
|
|
|
|
|
|
|
|
|
|
if (response == null) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
rssFeedModel = response;
|
|
|
|
|
notifyListeners();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
PrayersWidgetModel prayersWidgetModel = PrayersWidgetModel();
|
|
|
|
|
|
|
|
|
|
Future<void> getPrayerDetailsFromServer() async {
|
|
|
|
|
double testLatitude = 24.722136;
|
|
|
|
|
double testLongitude = 46.774303;
|
|
|
|
|
PrayersWidgetModel? response = await screenDetailsRepo.getPrayerDetailsByLatLong(
|
|
|
|
|
latitude: globalConfigurationsModel.projectLatitude == 0.0 ? testLatitude : globalConfigurationsModel.projectLatitude ?? testLatitude,
|
|
|
|
|
longitude: globalConfigurationsModel.projectLongitude == 0.0 ? testLongitude : globalConfigurationsModel.projectLongitude ?? testLongitude,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (response == null) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
prayersWidgetModel = response;
|
|
|
|
|
getNextPrayerToShow();
|
|
|
|
|
notifyListeners();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
WeathersWidgetModel weathersWidgetModel = WeathersWidgetModel();
|
|
|
|
|
|
|
|
|
|
Future<void> getWeatherDetailsFromServer() async {
|
|
|
|
|
int testCityKey = 297030;
|
|
|
|
|
WeathersWidgetModel? response = await screenDetailsRepo.getWeatherDetailsByCity(
|
|
|
|
|
cityId: ((globalConfigurationsModel.cityKey == null || globalConfigurationsModel.cityKey == 0) ? testCityKey : globalConfigurationsModel.cityKey).toString(),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (response == null) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
weathersWidgetModel = response;
|
|
|
|
|
notifyListeners();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
String nextPrayerTime = '';
|
|
|
|
|
String nextPrayerToShowEng = '';
|
|
|
|
|
String nextPrayerToShowArb = '';
|
|
|
|
|
|
|
|
|
|
void getNextPrayerToShow() async {
|
|
|
|
|
final current = DateTime.now();
|
|
|
|
|
log("Checking Namaz time Locally at $current and ${current.timeZoneName}");
|
|
|
|
|
|
|
|
|
|
// log("Data Before Check : ${prayersWidgetModel.fajr}");
|
|
|
|
|
// log("Data Before Check : ${globalConfigurationsModel.isPrayerTimeReq}");
|
|
|
|
|
|
|
|
|
|
if (globalConfigurationsModel.isPrayerTimeReq && prayersWidgetModel.fajr == null) {
|
|
|
|
|
await getPrayerDetailsFromServer();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// log("prayersWidgetModel.dhuhr: ${prayersWidgetModel.dhuhr!}");
|
|
|
|
|
// log("dhuhr: ${prayersWidgetModel.dhuhr!.toDateTimeFromInt()}");
|
|
|
|
|
// log("current: ${DateFormat('yyyy-MM-dd hh:mm a').format(DateTime.now().toLocal())}");
|
|
|
|
|
// log("current: ${DateTime.now().timeZoneName}");
|
|
|
|
|
|
|
|
|
|
if (prayersWidgetModel.fajr != null && prayersWidgetModel.fajr!.toDateTimeFromInt().isAfter(current)) {
|
|
|
|
|
final namazTime = prayersWidgetModel.fajr!.toFormattedDateTimeFromInt();
|
|
|
|
|
nextPrayerTime = namazTime;
|
|
|
|
|
nextPrayerToShowEng = "${globalConfigurationsModel.fajarTextEng}";
|
|
|
|
|
nextPrayerToShowArb = "${globalConfigurationsModel.fajarTextArb}";
|
|
|
|
|
notifyListeners();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (prayersWidgetModel.dhuhr != null && prayersWidgetModel.dhuhr!.toDateTimeFromInt().isAfter(current)) {
|
|
|
|
|
final namazTime = prayersWidgetModel.dhuhr!.toFormattedDateTimeFromInt();
|
|
|
|
|
nextPrayerTime = namazTime;
|
|
|
|
|
nextPrayerToShowEng = "${globalConfigurationsModel.dhuhrTextEng}";
|
|
|
|
|
nextPrayerToShowArb = "${globalConfigurationsModel.dhuhrTextArb}";
|
|
|
|
|
|
|
|
|
|
notifyListeners();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (prayersWidgetModel.asr != null && prayersWidgetModel.asr!.toDateTimeFromInt().isAfter(current)) {
|
|
|
|
|
final namazTime = prayersWidgetModel.asr!.toFormattedDateTimeFromInt();
|
|
|
|
|
nextPrayerTime = namazTime;
|
|
|
|
|
nextPrayerToShowEng = "${globalConfigurationsModel.asarTextEng}";
|
|
|
|
|
nextPrayerToShowArb = "${globalConfigurationsModel.asarTextArb}";
|
|
|
|
|
|
|
|
|
|
notifyListeners();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (prayersWidgetModel.maghrib != null && prayersWidgetModel.maghrib!.toDateTimeFromInt().isAfter(current)) {
|
|
|
|
|
final namazTime = prayersWidgetModel.maghrib!.toFormattedDateTimeFromInt();
|
|
|
|
|
nextPrayerTime = namazTime;
|
|
|
|
|
nextPrayerToShowEng = "${globalConfigurationsModel.maghribTextEng}";
|
|
|
|
|
nextPrayerToShowArb = "${globalConfigurationsModel.maghribTextArb}";
|
|
|
|
|
|
|
|
|
|
notifyListeners();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (prayersWidgetModel.isha != null && prayersWidgetModel.isha!.toDateTimeFromInt().isAfter(current)) {
|
|
|
|
|
final namazTime = prayersWidgetModel.isha!.toFormattedDateTimeFromInt();
|
|
|
|
|
nextPrayerTime = namazTime;
|
|
|
|
|
nextPrayerToShowEng = "${globalConfigurationsModel.ishaTextEng}";
|
|
|
|
|
nextPrayerToShowArb = "${globalConfigurationsModel.ishaTextArb}";
|
|
|
|
|
|
|
|
|
|
notifyListeners();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
final namazTime = prayersWidgetModel.fajr!.toFormattedDateTimeFromInt();
|
|
|
|
|
nextPrayerTime = namazTime;
|
|
|
|
|
nextPrayerToShowEng = "${globalConfigurationsModel.fajarTextEng}";
|
|
|
|
|
nextPrayerToShowArb = "${globalConfigurationsModel.fajarTextArb}";
|
|
|
|
|
|
|
|
|
|
notifyListeners();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int counter = 0;
|
|
|
|
|
DateTime lastChecked = DateTime.now();
|
|
|
|
|
DateTime appStartTime = DateTime.now(); // Track actual app start time
|
|
|
|
|
Timer? _midnightTimer;
|
|
|
|
|
Timer? _healthCheckTimer;
|
|
|
|
|
int healthCheckFailures = 0; // Track consecutive failures
|
|
|
|
|
|
|
|
|
|
Future<void> initializeTimer() async {
|
|
|
|
|
// Cancel any existing timer to avoid multiple timers running
|
|
|
|
|
_midnightTimer?.cancel();
|
|
|
|
|
_healthCheckTimer?.cancel();
|
|
|
|
|
|
|
|
|
|
if (!(globalConfigurationsModel.isWeatherReq) && !(globalConfigurationsModel.isPrayerTimeReq) && !(globalConfigurationsModel.isRssFeedReq)) {
|
|
|
|
|
// Even if widgets are not required, start a minimal health check
|
|
|
|
|
_startHealthCheckTimer();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Start the main periodic timer
|
|
|
|
|
_midnightTimer = Timer.periodic(
|
|
|
|
|
const Duration(minutes: 10),
|
|
|
|
|
(timer) async {
|
|
|
|
|
counter++;
|
|
|
|
|
DateTime now = DateTime.now();
|
|
|
|
|
log("counterValue: $counter");
|
|
|
|
|
|
|
|
|
|
if (globalConfigurationsModel.id == null) {
|
|
|
|
|
await getGlobalConfigurationsByIP();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Every Two hour, update RSS feed if required
|
|
|
|
|
if (counter % 12 == 0 && globalConfigurationsModel.isRssFeedReq) {
|
|
|
|
|
await getRssFeedDetailsFromServer();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
log("lastChecked: [${lastChecked.day}]");
|
|
|
|
|
log("now: [${now.day}]");
|
|
|
|
|
|
|
|
|
|
// At midnight, update weather and prayer details if required
|
|
|
|
|
if (now.day != lastChecked.day) {
|
|
|
|
|
if (now.difference(now.copyWith(hour: 0, minute: 0, second: 0, millisecond: 0, microsecond: 0)).inMinutes >= 5) {
|
|
|
|
|
await nativeMethodChannelService.smartRestart(forceRestart: true, cleanupFirst: true);
|
|
|
|
|
lastChecked = now;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
getNextPrayerToShow();
|
|
|
|
|
syncHubConnectionState();
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Start health check timer
|
|
|
|
|
_startHealthCheckTimer();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void _startHealthCheckTimer() {
|
|
|
|
|
// Health check every 5 minutes to keep app active and check connections
|
|
|
|
|
_healthCheckTimer = Timer.periodic(
|
|
|
|
|
const Duration(minutes: 5),
|
|
|
|
|
(timer) async {
|
|
|
|
|
try {
|
|
|
|
|
final now = DateTime.now();
|
|
|
|
|
final actualUptime = now.difference(appStartTime);
|
|
|
|
|
final uptimeHours = actualUptime.inHours;
|
|
|
|
|
final uptimeMinutes = actualUptime.inMinutes % 60;
|
|
|
|
|
|
|
|
|
|
log("Health check - App uptime: ${uptimeHours}h ${uptimeMinutes}m");
|
|
|
|
|
loggerService.logToFile(
|
|
|
|
|
message: "Health check performed - App uptime: ${uptimeHours}h ${uptimeMinutes}m (started: ${appStartTime.toString().substring(0, 19)})",
|
|
|
|
|
source: "_startHealthCheckTimer -> screen_config_view_model.dart",
|
|
|
|
|
type: LogTypeEnum.data,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Check if app has been running for dangerously long (>60 hours)
|
|
|
|
|
if (uptimeHours > 60) {
|
|
|
|
|
loggerService.logToFile(
|
|
|
|
|
message: "WARNING: App running for ${uptimeHours} hours - scheduling proactive restart",
|
|
|
|
|
source: "_startHealthCheckTimer -> screen_config_view_model.dart",
|
|
|
|
|
type: LogTypeEnum.data,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Sync hub connection state
|
|
|
|
|
try {
|
|
|
|
|
syncHubConnectionState();
|
|
|
|
|
healthCheckFailures = 0; // Reset on success
|
|
|
|
|
} catch (e) {
|
|
|
|
|
healthCheckFailures++;
|
|
|
|
|
loggerService.logError("Health check - Hub sync failed (${healthCheckFailures} consecutive): $e");
|
|
|
|
|
|
|
|
|
|
// If multiple consecutive failures, try to recover
|
|
|
|
|
if (healthCheckFailures >= 3) {
|
|
|
|
|
loggerService.logToFile(
|
|
|
|
|
message: "CRITICAL: ${healthCheckFailures} consecutive health check failures - attempting recovery",
|
|
|
|
|
source: "_startHealthCheckTimer -> screen_config_view_model.dart",
|
|
|
|
|
type: LogTypeEnum.data,
|
|
|
|
|
);
|
|
|
|
|
QueuingViewModel queuingViewModel = getIt.get<QueuingViewModel>();
|
|
|
|
|
await queuingViewModel.stopHubConnection();
|
|
|
|
|
await Future.delayed(const Duration(seconds: 2));
|
|
|
|
|
await queuingViewModel.startHubConnection();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Ensure wakelock is still enabled
|
|
|
|
|
try {
|
|
|
|
|
await WakelockPlus.enable();
|
|
|
|
|
} catch (e) {
|
|
|
|
|
loggerService.logError("Failed to enable wakelock: $e");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Log memory/performance indicators
|
|
|
|
|
loggerService.logToFile(
|
|
|
|
|
message: "Health check - Hub connected: $isHubConnected, Internet: $isInternetConnected",
|
|
|
|
|
source: "_startHealthCheckTimer -> screen_config_view_model.dart",
|
|
|
|
|
type: LogTypeEnum.connectivity,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Trigger a small notifyListeners to keep UI thread active
|
|
|
|
|
notifyListeners();
|
|
|
|
|
} catch (e, stackTrace) {
|
|
|
|
|
healthCheckFailures++;
|
|
|
|
|
loggerService.logError("Health check timer error (${healthCheckFailures} consecutive): $e\n$stackTrace");
|
|
|
|
|
|
|
|
|
|
// If health check itself is failing repeatedly, something is seriously wrong
|
|
|
|
|
if (healthCheckFailures >= 5) {
|
|
|
|
|
loggerService.logToFile(
|
|
|
|
|
message: "FATAL: Health check failing repeatedly - app may be in bad state",
|
|
|
|
|
source: "_startHealthCheckTimer -> screen_config_view_model.dart",
|
|
|
|
|
type: LogTypeEnum.data,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
void dispose() {
|
|
|
|
|
_midnightTimer?.cancel();
|
|
|
|
|
_healthCheckTimer?.cancel();
|
|
|
|
|
patientIdController.dispose();
|
|
|
|
|
|
|
|
|
|
super.dispose();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
DateTime currentLastTimeUpdated = DateTime.now();
|
|
|
|
|
|
|
|
|
|
Future<void> getLastTimeUpdatedFromCache() async {
|
|
|
|
|
DateTime? response = await cacheService.getLastTimeUpdatedFromCache();
|
|
|
|
|
if (response != null) {
|
|
|
|
|
currentLastTimeUpdated = response;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
DateTime? lastTimeLogsCleared = DateTime.now();
|
|
|
|
|
|
|
|
|
|
Future<void> getLastTimeLogsClearedFromCache() async {
|
|
|
|
|
lastTimeLogsCleared = await cacheService.getLastTimeLogsCleared();
|
|
|
|
|
if (lastTimeLogsCleared == null) {
|
|
|
|
|
await cacheService.setLastTimeLogsCleared(lastTimeCleared: DateTime.now().millisecondsSinceEpoch).whenComplete(() => lastTimeLogsCleared = DateTime.now());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// *************************** KIOSK FUNCTIONS *************************
|
|
|
|
|
|
|
|
|
|
KioskScreenStateEnums kioskScreenStateEnum = KioskScreenStateEnums.languageState;
|
|
|
|
|
|
|
|
|
|
void updateKioskScreenState(KioskScreenStateEnums state) {
|
|
|
|
|
kioskScreenStateEnum = state;
|
|
|
|
|
notifyListeners();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
KioskPatientTicket? kioskPatientTicket = KioskPatientTicket();
|
|
|
|
|
|
|
|
|
|
void updateTicketGeneratedFromKiosk(KioskPatientTicket? value) {
|
|
|
|
|
kioskPatientTicket = value;
|
|
|
|
|
notifyListeners();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
KioskLanguageConfigModel? currentSelectedKioskLanguage;
|
|
|
|
|
|
|
|
|
|
void updateCurrentSelectedKioskLanguageModel(KioskLanguageConfigModel value) {
|
|
|
|
|
currentSelectedKioskLanguage = value;
|
|
|
|
|
notifyListeners();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
late KioskQueueModel currentSelectedKioskQueueModel;
|
|
|
|
|
|
|
|
|
|
void updateCurrentSelectedKioskQueueModel(KioskQueueModel value) {
|
|
|
|
|
currentSelectedKioskQueueModel = value;
|
|
|
|
|
notifyListeners();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Future<bool> generateTicketForQueue({required KioskQueueModel kioskQueueModel, int patientId = 0}) async {
|
|
|
|
|
updateKioskScreenState(KioskScreenStateEnums.busyState);
|
|
|
|
|
KioskPatientTicket? kioskPatientTicket = await createTicketFromKiosk(
|
|
|
|
|
projectId: kioskQueueModel.projectID ?? 0,
|
|
|
|
|
queueId: kioskQueueModel.queueID ?? 0,
|
|
|
|
|
patientId: patientId,
|
|
|
|
|
);
|
|
|
|
|
if (kioskPatientTicket == null) {
|
|
|
|
|
updateKioskScreenState(KioskScreenStateEnums.languageState);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
updateTicketGeneratedFromKiosk(kioskPatientTicket);
|
|
|
|
|
updateKioskScreenState(KioskScreenStateEnums.ticketNumState);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Future<void> createTestTickets({required int numOfTicketsToCreate}) async {
|
|
|
|
|
int startTicket = 123457100;
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < numOfTicketsToCreate; i++) {
|
|
|
|
|
GenericRespModel? response = await screenDetailsRepo.createTestTickets(ticketNumber: startTicket);
|
|
|
|
|
startTicket = startTicket + 1;
|
|
|
|
|
if (response == null || response.messageStatus != 1) {
|
|
|
|
|
log("response null from createNextTickets");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
log("last ticket is: $startTicket ");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Future<KioskPatientTicket?> createTicketFromKiosk({required int projectId, required int queueId, int patientId = 0}) async {
|
|
|
|
|
try {
|
|
|
|
|
GenericRespModel? response = await screenDetailsRepo.createTicketFromKiosk(
|
|
|
|
|
projectId: projectId,
|
|
|
|
|
queueId: queueId,
|
|
|
|
|
patientId: patientId,
|
|
|
|
|
);
|
|
|
|
|
if (response == null || response.messageStatus != 1) {
|
|
|
|
|
loggerService.logError("response null from createTicketFromKiosk");
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
return response.data;
|
|
|
|
|
} catch (e) {
|
|
|
|
|
InfoComponents.showToast(e.toString());
|
|
|
|
|
loggerService.logError(e.toString());
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Future<void> acknowledgeTicket({required int ticketQueueID, required String ipAddress}) async {
|
|
|
|
|
GenericRespModel? response = await screenDetailsRepo.acknowledgeTicket(
|
|
|
|
|
ipAddress: ipAddress,
|
|
|
|
|
ticketQueueID: ticketQueueID,
|
|
|
|
|
qTypeEnum: globalConfigurationsModel.qTypeEnum,
|
|
|
|
|
);
|
|
|
|
|
if (response == null || response.messageStatus != 1) {
|
|
|
|
|
loggerService.logError("response null from acknowledgeTicket");
|
|
|
|
|
return;
|
|
|
|
|
} else {
|
|
|
|
|
loggerService.logInfo("response from acknowledgeTicket ${response.data}");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Future<void> acknowledgeTicketForAppointmentOnly({required int ticketQueueID, required String ipAddress, required CallTypeEnum callTypeEnum}) async {
|
|
|
|
|
GenericRespModel? response = await screenDetailsRepo.acknowledgeTicketForAppointment(
|
|
|
|
|
ticketId: ticketQueueID,
|
|
|
|
|
ipAddress: ipAddress,
|
|
|
|
|
callTypeEnum: callTypeEnum,
|
|
|
|
|
);
|
|
|
|
|
if (response == null || response.messageStatus != 1) {
|
|
|
|
|
loggerService.logError("response null from acknowledgeTicketForAppointmentOnly");
|
|
|
|
|
return;
|
|
|
|
|
} else {
|
|
|
|
|
loggerService.logInfo("response from acknowledgeTicketForAppointmentOnly ${response.data}");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ***************************** TEXT INPUT PATIENT ID FUNCTIONS *********************
|
|
|
|
|
|
|
|
|
|
final TextEditingController patientIdController = TextEditingController();
|
|
|
|
|
|
|
|
|
|
Future<void> onPatientIdSubmitted(String text) async {
|
|
|
|
|
int? patientId = int.tryParse(text);
|
|
|
|
|
if (patientId != null && patientId > 0) {
|
|
|
|
|
isGeneratingTicket = true;
|
|
|
|
|
await generateTicketForQueue(kioskQueueModel: currentSelectedKioskQueueModel, patientId: patientId);
|
|
|
|
|
patientIdController.clear();
|
|
|
|
|
await Future.delayed(const Duration(seconds: 2)).whenComplete(() => isGeneratingTicket = false);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// *************************** QR SCANNER FUNCTIONS *************************
|
|
|
|
|
|
|
|
|
|
Barcode? qrCodeResult;
|
|
|
|
|
QRViewController? qrViewController;
|
|
|
|
|
bool isGeneratingTicket = false;
|
|
|
|
|
|
|
|
|
|
Future<void> flipCamera() async {
|
|
|
|
|
await qrViewController!.flipCamera();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void onQRViewCreated({
|
|
|
|
|
required QRViewController controller,
|
|
|
|
|
required VoidCallback onSuccess,
|
|
|
|
|
required VoidCallback onFailure,
|
|
|
|
|
}) async {
|
|
|
|
|
qrViewController = controller;
|
|
|
|
|
|
|
|
|
|
controller.scannedDataStream.listen((scanData) async {
|
|
|
|
|
loggerService.logInfo("Found: ${scanData.code} and isGeneratingTicket: $isGeneratingTicket");
|
|
|
|
|
if (isGeneratingTicket) return;
|
|
|
|
|
await validateAndGenerateTicketFromQR(data: scanData, onFailure: onFailure, onSuccess: onSuccess);
|
|
|
|
|
});
|
|
|
|
|
await flipCamera();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Future<void> validateAndGenerateTicketFromQR({required var data, required VoidCallback onSuccess, required VoidCallback onFailure}) async {
|
|
|
|
|
if (data == null) return;
|
|
|
|
|
qrCodeResult = data;
|
|
|
|
|
notifyListeners();
|
|
|
|
|
|
|
|
|
|
final code = qrCodeResult!.code;
|
|
|
|
|
int? patientId = int.tryParse(code ?? '');
|
|
|
|
|
if (patientId != null && patientId > 0) {
|
|
|
|
|
isGeneratingTicket = true;
|
|
|
|
|
bool status = await generateTicketForQueue(kioskQueueModel: currentSelectedKioskQueueModel, patientId: patientId);
|
|
|
|
|
qrCodeResult = null;
|
|
|
|
|
notifyListeners();
|
|
|
|
|
|
|
|
|
|
if (status) {
|
|
|
|
|
log("status: $status");
|
|
|
|
|
onSuccess();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await Future.delayed(const Duration(seconds: 2)).whenComplete(() => isGeneratingTicket = false);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Future<void> reassemble() async {
|
|
|
|
|
if (qrViewController != null) {
|
|
|
|
|
if (defaultTargetPlatform == TargetPlatform.android) {
|
|
|
|
|
await qrViewController!.pauseCamera();
|
|
|
|
|
} else if (defaultTargetPlatform == TargetPlatform.iOS) {
|
|
|
|
|
await qrViewController!.resumeCamera();
|
|
|
|
|
}
|
|
|
|
|
await qrViewController!.flipCamera();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void syncHubConnectionState() async {
|
|
|
|
|
QueuingViewModel queuingViewModel = getIt.get<QueuingViewModel>();
|
|
|
|
|
bool newState = (queuingViewModel.signalrRepo as SignalrRepoImp).isConnected;
|
|
|
|
|
|
|
|
|
|
loggerService.logToFile(
|
|
|
|
|
message: "Hub Current Status : $newState",
|
|
|
|
|
source: "syncHubConnectionState-> screen_config_view_model.dart",
|
|
|
|
|
type: LogTypeEnum.connectivity,
|
|
|
|
|
);
|
|
|
|
|
log("[isHubConnected: $isHubConnected] , [newState: $newState]");
|
|
|
|
|
|
|
|
|
|
if (isHubConnected != newState) {
|
|
|
|
|
updateIsHubConnected(newState);
|
|
|
|
|
}
|
|
|
|
|
if (!isHubConnected) {
|
|
|
|
|
QueuingViewModel queuingViewModel = getIt.get<QueuingViewModel>();
|
|
|
|
|
bool? status = await queuingViewModel.startHubConnection();
|
|
|
|
|
// syncHubConnectionState will update isHubConnected
|
|
|
|
|
} else {
|
|
|
|
|
log("Hub is Connected!");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|