You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
909 lines
31 KiB
Dart
909 lines
31 KiB
Dart
import 'dart:async';
|
|
import 'dart:developer';
|
|
|
|
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:mobile_scanner/mobile_scanner.dart';
|
|
import 'package:wakelock_plus/wakelock_plus.dart';
|
|
import 'package:universal_html/html.dart' as html;
|
|
// 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();
|
|
|
|
// Platform-specific lifecycle
|
|
if (kIsWeb) {
|
|
_initializeWebLifecycle();
|
|
}
|
|
}
|
|
|
|
StreamSubscription? _visibilitySubscription;
|
|
|
|
void _initializeWebLifecycle() {
|
|
// Listen to page visibility changes
|
|
_visibilitySubscription = html.document.onVisibilityChange.listen((event) {
|
|
if (html.document.hidden!) {
|
|
onAppPaused();
|
|
} else {
|
|
onAppResumed();
|
|
}
|
|
});
|
|
|
|
// Prevent accidental tab close
|
|
html.window.onBeforeUnload.listen((event) {
|
|
event.preventDefault();
|
|
});
|
|
}
|
|
|
|
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");
|
|
}
|
|
|
|
// On web, wait a bit for network to stabilize after tab becomes visible
|
|
if (kIsWeb) {
|
|
await Future.delayed(const Duration(milliseconds: 500));
|
|
}
|
|
|
|
// Verify connections are still alive and reconnect if needed
|
|
syncHubConnectionState();
|
|
|
|
// On web, proactively try to reconnect if disconnected
|
|
if (kIsWeb && !isHubConnected) {
|
|
loggerService.logToFile(
|
|
message: "Web page resumed - attempting SignalR reconnection",
|
|
source: "onAppResumed -> screen_config_view_model.dart",
|
|
type: LogTypeEnum.connectivity,
|
|
);
|
|
QueuingViewModel queuingViewModel = getIt.get<QueuingViewModel>();
|
|
try {
|
|
// Try to stop existing connection (may be null/disconnected already)
|
|
try {
|
|
await queuingViewModel.stopHubConnection();
|
|
} catch (e) {
|
|
log("Stop connection error (expected if already disconnected): $e");
|
|
}
|
|
await Future.delayed(const Duration(seconds: 1));
|
|
// Start fresh connection
|
|
final result = await queuingViewModel.startHubConnection();
|
|
if (result == true) {
|
|
loggerService.logToFile(
|
|
message: "SignalR reconnected successfully on resume",
|
|
source: "onAppResumed -> screen_config_view_model.dart",
|
|
type: LogTypeEnum.connectivity,
|
|
);
|
|
}
|
|
} catch (e) {
|
|
loggerService.logError("Failed to reconnect on resume: $e");
|
|
}
|
|
}
|
|
}
|
|
|
|
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 {
|
|
int attempts = 0;
|
|
const maxAttempts = kIsWeb ? 3 : 30;
|
|
|
|
// On web, check localStorage immediately on startup (before any detection)
|
|
if (kIsWeb && currentScreenIP == "") {
|
|
try {
|
|
final storedIp = html.window.localStorage['device_ip'];
|
|
if (storedIp != null && storedIp.isNotEmpty && storedIp != 'null') {
|
|
currentScreenIP = storedIp;
|
|
log('✅ IP retrieved from localStorage on startup: $currentScreenIP');
|
|
initializeScreenConfigVM();
|
|
return;
|
|
} else {
|
|
log('⚠️ No IP found in localStorage on startup (storedIp: "$storedIp")');
|
|
}
|
|
} catch (e) {
|
|
log('❌ Error accessing localStorage on startup: $e');
|
|
loggerService.logError('localStorage access error: $e');
|
|
}
|
|
}
|
|
|
|
while (currentScreenIP == "") {
|
|
await getCurrentScreenIP();
|
|
if (currentScreenIP != "") {
|
|
initializeScreenConfigVM();
|
|
return;
|
|
} else {
|
|
attempts++;
|
|
if (attempts >= maxAttempts) {
|
|
if (kIsWeb) {
|
|
// On web, show configuration dialog
|
|
loggerService.logError(
|
|
"No IP auto-detected or configured after $attempts attempts.");
|
|
throw Exception("IP_CONFIGURATION_REQUIRED");
|
|
} else {
|
|
// On Android native, keep trying
|
|
loggerService.logError("Failed to get IP after $attempts attempts");
|
|
}
|
|
}
|
|
// Wait 4 seconds on first attempt to allow auto-detection
|
|
final waitTime = attempts == 1 ? 4 : 2;
|
|
await Future.delayed(Duration(seconds: waitTime));
|
|
}
|
|
}
|
|
}
|
|
|
|
bool isInternetConnected = true;
|
|
|
|
updateIsInternetConnected(bool value) {
|
|
isInternetConnected = value;
|
|
notifyListeners();
|
|
}
|
|
|
|
bool isHubConnected = false; // Start as disconnected until connection succeeds
|
|
|
|
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 {
|
|
|
|
|
|
log("Using TEST IP");
|
|
if (useTestIP) {
|
|
currentScreenIP = AppConstants.testIP;
|
|
log("🧪 Using TEST IP: $currentScreenIP");
|
|
} else {
|
|
currentScreenIP = await connectivityService.getCurrentScreenIP();
|
|
if (currentScreenIP.isEmpty) {
|
|
log("⚠️ WARNING: No IP address detected!");
|
|
if (kIsWeb) {
|
|
log("📌 For web, add IP to URL: ?ip=YOUR_IP_ADDRESS");
|
|
log(" Example: http://localhost:port/?ip=12.4.5.1");
|
|
}
|
|
} else {
|
|
log("✅ Current Screen IP: $currentScreenIP");
|
|
// On web, ensure IP is saved to localStorage
|
|
if (kIsWeb) {
|
|
try {
|
|
html.window.localStorage['device_ip'] = currentScreenIP;
|
|
log("💾 Saved IP to localStorage: $currentScreenIP");
|
|
} catch (e) {
|
|
log("❌ Failed to save IP to localStorage: $e");
|
|
loggerService.logError('localStorage save error: $e');
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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);
|
|
|
|
loggerService.logError("response was: $response");
|
|
|
|
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 projectID
|
|
if (value is GlobalConfigurationsModel) {
|
|
// Use projectID-based detection (projectID = 16 for Takhasusi Main)
|
|
try {
|
|
value.isFromTakhasusiMain =
|
|
value.projectID == AppConstants.projectIdTakhasusiMain;
|
|
|
|
// Debug logging for Takhasusi Main detection
|
|
log("=== TAKHASUSI MAIN CHECK (ProjectID-based) ===");
|
|
log("Current Screen IP: $currentScreenIP");
|
|
log("Project ID: ${value.projectID}");
|
|
log("Is Takhasusi Main (projectID == 16): ${value.isFromTakhasusiMain}");
|
|
if (value.isFromTakhasusiMain) {
|
|
log("✅ This device will use getTurnsByOrientationForOlderVersions()");
|
|
} else {
|
|
log("✅ This device will use getTurnsByOrientationForNewVersions()");
|
|
}
|
|
log("==========================================");
|
|
} 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 || state == ViewState.error) {
|
|
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();
|
|
|
|
// On web, if disconnected for 2 consecutive checks, force reconnection
|
|
if (kIsWeb && !isHubConnected && healthCheckFailures >= 2) {
|
|
loggerService.logToFile(
|
|
message:
|
|
"Web disconnected for ${healthCheckFailures} checks - forcing reconnection",
|
|
source:
|
|
"_startHealthCheckTimer -> screen_config_view_model.dart",
|
|
type: LogTypeEnum.connectivity,
|
|
);
|
|
QueuingViewModel queuingViewModel = getIt.get<QueuingViewModel>();
|
|
try {
|
|
await queuingViewModel.stopHubConnection();
|
|
} catch (e) {
|
|
log("Stop connection error during health check (may be expected): $e");
|
|
}
|
|
await Future.delayed(const Duration(seconds: 2));
|
|
await queuingViewModel.startHubConnection();
|
|
}
|
|
|
|
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>();
|
|
try {
|
|
await queuingViewModel.stopHubConnection();
|
|
} catch (e) {
|
|
log("Stop connection error during recovery (may be expected): $e");
|
|
}
|
|
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() {
|
|
_visibilitySubscription?.cancel();
|
|
_midnightTimer?.cancel();
|
|
_healthCheckTimer?.cancel();
|
|
patientIdController.dispose();
|
|
mobileScannerController?.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 *************************
|
|
|
|
MobileScannerController? mobileScannerController;
|
|
bool isGeneratingTicket = false;
|
|
|
|
void initializeMobileScanner() {
|
|
mobileScannerController = MobileScannerController(
|
|
facing: kIsWeb ? CameraFacing.front : CameraFacing.back,
|
|
detectionSpeed: DetectionSpeed.normal,
|
|
);
|
|
}
|
|
|
|
void onQRDetected(BarcodeCapture barcodes) async {
|
|
if (isGeneratingTicket) return;
|
|
|
|
final barcode = barcodes.barcodes.firstOrNull;
|
|
if (barcode == null || barcode.rawValue == null) return;
|
|
|
|
loggerService.logInfo(
|
|
"QR Found: ${barcode.rawValue} and isGeneratingTicket: $isGeneratingTicket");
|
|
|
|
final patientId = int.tryParse(barcode.rawValue!);
|
|
if (patientId != null && patientId > 0) {
|
|
isGeneratingTicket = true;
|
|
await generateTicketForQueue(
|
|
kioskQueueModel: currentSelectedKioskQueueModel,
|
|
patientId: patientId,
|
|
);
|
|
await Future.delayed(const Duration(seconds: 2))
|
|
.whenComplete(() => isGeneratingTicket = false);
|
|
}
|
|
}
|
|
|
|
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>();
|
|
try {
|
|
await queuingViewModel.startHubConnection();
|
|
// syncHubConnectionState will update isHubConnected
|
|
} catch (e) {
|
|
loggerService.logError("Failed to start hub connection in sync: $e");
|
|
}
|
|
} else {
|
|
log("Hub is Connected!");
|
|
}
|
|
}
|
|
}
|