From acb094790847f1f01bc43747a89d12ddc9fdeca4 Mon Sep 17 00:00:00 2001 From: sudomarcma <1040211836@qq.com> Date: Tue, 8 Jul 2025 18:18:28 +0800 Subject: [PATCH] Refactor DeviceUuidService and NativeServicesManager for simplified device handling - Removed detailed device registration and heartbeat logic from NativeServicesManager. - Simplified device UUID retrieval and validation in NativeServicesManager. - Streamlined DeviceUuidService by removing unnecessary methods and logging. - Updated WorkerDashboardView to directly interact with location service instead of background location service. - Enhanced clock status synchronization with location service in WorkerDashboardView. --- backend/server.js | 51 +- capacitor.config.json | 8 +- src/locales/en.json | 6 +- src/locales/ms.json | 6 +- src/services/authService.js | 4 +- src/services/backgroundLocationService.js | 1067 ++++----------------- src/services/deviceUuidService.js | 160 +-- src/services/nativeServicesManager.js | 58 +- src/views/WorkerDashboardView.vue | 15 +- 9 files changed, 246 insertions(+), 1129 deletions(-) diff --git a/backend/server.js b/backend/server.js index 1c7fa64..1856f45 100644 --- a/backend/server.js +++ b/backend/server.js @@ -12,13 +12,10 @@ import mysql from 'mysql2/promise' import dotenv from 'dotenv' import bcrypt from 'bcrypt' import jwt from 'jsonwebtoken' -// --- FIX START --- -// Import only the required functions from turf + import { point, polygon, booleanPointInPolygon, pointToLineDistance } from '@turf/turf' -// --- FIX END --- -// Helper function to validate device for user with simplified workers table approach async function validateDeviceForUser(userId, deviceUuid, db) { try { // Step 1: Get user's current registered device UUID from workers table @@ -116,7 +113,7 @@ async function logSecurityAlert(userId, alertType, alertData, db) { } // Helper function to register a new device for user (simplified for workers table) -async function registerDeviceForUser(userId, deviceUuid, deviceInfo, db) { +async function registerDeviceForUser(userId, deviceUuid, db) { try { // Check if device is already registered to another user const [otherUserRows] = await db.execute( @@ -129,7 +126,6 @@ async function registerDeviceForUser(userId, deviceUuid, deviceInfo, db) { await logSecurityAlert(userId, 'device_registration_conflict', { attempted_device_uuid: deviceUuid, conflicting_user: otherUserRows[0].username, - device_info: deviceInfo, message: 'Attempted to register device already assigned to another user' }, db) @@ -1005,49 +1001,6 @@ const geofence = polygon([ } }) - // Device Heartbeat Endpoint (Simplified - no timestamp tracking) - app.post('/api/device/heartbeat', authenticateJWT, async (req, res) => { - try { - const { userId, deviceUuid } = req.body - - if (!userId || !deviceUuid) { - return res.status(400).json({ message: 'User ID and device UUID are required.' }) - } - - // Validate device registration (simplified check) - const [userRows] = await db.execute( - 'SELECT device_uuid FROM workers WHERE id = ?', - [userId] - ) - - if (userRows.length === 0) { - return res.status(404).json({ message: 'User not found.' }) - } - - const registeredDeviceUuid = userRows[0].device_uuid - - if (!registeredDeviceUuid) { - return res.status(400).json({ message: 'No device registered for this user.' }) - } - - if (registeredDeviceUuid !== deviceUuid) { - // Log security alert for heartbeat from unauthorized device - await logSecurityAlert(userId, 'unauthorized_heartbeat', { - registered_device_uuid: registeredDeviceUuid, - attempted_device_uuid: deviceUuid, - message: 'Heartbeat attempt from unauthorized device' - }, db) - - return res.status(403).json({ message: 'Device not authorized for heartbeat.' }) - } - - // Device is valid - heartbeat accepted (no data storage needed) - res.json({ message: 'Heartbeat accepted' }) - } catch (error) { - console.error('Device heartbeat error:', error) - res.status(500).json({ message: 'Database error during heartbeat validation.' }) - } - }) // Security Check Endpoint app.post('/api/security/check', authenticateJWT, async (req, res) => { diff --git a/capacitor.config.json b/capacitor.config.json index 2824622..c2167c4 100644 --- a/capacitor.config.json +++ b/capacitor.config.json @@ -2,6 +2,9 @@ "appId": "com.ouji.factory.myapp", "appName": "nilai-clock", "webDir": "dist", + "android": { + "useLegacyBridege" : true + }, "plugins": { "SafeArea": { "enabled": true, @@ -38,6 +41,9 @@ "isVirtual", "webViewVersion" ] + }, + "CapacitorHttp": { + "enabled": true } } -} \ No newline at end of file +} diff --git a/src/locales/en.json b/src/locales/en.json index 1ac0cb0..0a3a952 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -246,5 +246,9 @@ "services": "Services", "systemServicesStatus": "System services and security status", "updateYourPassword": "Update your account password", - "signOutOfAccount": "Sign out of your account" + "signOutOfAccount": "Sign out of your account", + + "workLocationTracking": "Work Location Tracking", + "locationTrackingForAttendance": "Location tracking active for work attendance", + "monitoringLocation": "Monitoring location for work attendance" } \ No newline at end of file diff --git a/src/locales/ms.json b/src/locales/ms.json index b2d808b..b4c9816 100644 --- a/src/locales/ms.json +++ b/src/locales/ms.json @@ -240,5 +240,9 @@ "failedToRefreshStatus": "Gagal menyegar semula status", "locationTrackingStarted": "Penjejakan lokasi dimulakan dengan jayanya", "failedToStartLocationTracking": "Gagal memulakan penjejakan lokasi", - "securityCheckFailed": "Pemeriksaan keselamatan gagal" + "securityCheckFailed": "Pemeriksaan keselamatan gagal", + + "workLocationTracking": "Penjejakan Lokasi Kerja", + "locationTrackingForAttendance": "Penjejakan lokasi aktif untuk kehadiran kerja", + "monitoringLocation": "Memantau lokasi untuk kehadiran kerja" } \ No newline at end of file diff --git a/src/services/authService.js b/src/services/authService.js index 3a519dc..60be23f 100644 --- a/src/services/authService.js +++ b/src/services/authService.js @@ -1,6 +1,7 @@ import { Preferences } from '@capacitor/preferences' import { Capacitor } from '@capacitor/core' import { apiFetch } from '@/api.js' +import { deviceUuidService } from './deviceUuidService.js' class AuthService { constructor() { @@ -194,7 +195,6 @@ class AuthService { async login(username, password, rememberCredentials = false) { try { // Import deviceUuidService for proper device-specific UUID generation - const { deviceUuidService } = await import('./deviceUuidService.js') const deviceUuid = await deviceUuidService.getOrCreateDeviceUuid() const loginUrl = `${import.meta.env.VITE_API_BASE_URL}/api/auth/login` @@ -263,7 +263,7 @@ class AuthService { await this.removeSecureItem(this.tokenKey) await this.removeSecureItem(this.userIdKey) await this.removeSecureItem(this.userRoleKey) - + // Also clear stored credentials for auto-login await this.clearStoredCredentials() diff --git a/src/services/backgroundLocationService.js b/src/services/backgroundLocationService.js index ece5e68..931f171 100644 --- a/src/services/backgroundLocationService.js +++ b/src/services/backgroundLocationService.js @@ -3,7 +3,6 @@ import { Geolocation } from '@capacitor/geolocation' import { Capacitor } from '@capacitor/core' import { LocalNotifications } from '@capacitor/local-notifications' import { Network } from '@capacitor/network' -import { apiFetch } from '@/api.js' const BackgroundGeolocation = registerPlugin('BackgroundGeolocation') @@ -11,132 +10,99 @@ class BackgroundLocationService { constructor() { this.isInitialized = false this.isTracking = false - this.isClockedIn = false // Track worker clock-in status + this.isClockedIn = false - // PRODUCTION INTERVALS - 30 minutes for reliable background operation - this.locationUpdateInterval = 30 * 60 * 1000 // 30 minutes in milliseconds - this.heartbeatInterval = 5 * 60 * 1000 // 5 minutes for service heartbeat - this.notificationRefreshInterval = 10 * 60 * 1000 // 10 minutes for notification refresh (less aggressive) + // Core intervals - 30 minutes for production (2 minutes for testing) + this.locationUpdateInterval = 2 * 60 * 1000 // 2 minutes for testing - this.lastLocationUpdate = null - this.lastBackgroundLocation = null // Store last location from background watcher - this.isInBackground = false // Track if app is in background - - // Timer management + // Essential timers this.periodicUpdateTimer = null - this.heartbeatTimer = null - this.notificationRefreshTimer = null - - // Service state tracking - this.lastHeartbeat = null - this.serviceStartTime = null this.watcherId = null - // Geofence validation settings - this.geofenceCheckEnabled = true - this.lastGeofenceCheck = null - - // Local storage keys for offline data + // Storage keys this.PENDING_LOCATIONS_KEY = 'pendingLocationUpdates' this.CLOCK_STATUS_KEY = 'workerClockStatus' - this.LAST_LOCATION_KEY = 'lastKnownLocation' - this.NETWORK_STATUS_KEY = 'networkStatus' - // Network monitoring + // Network state this.isOnline = true this.networkListener = null - this.offlineQueueSize = 0 - this.maxOfflineQueueSize = 100 // Maximum locations to store offline + } + + getNotificationMessages() { + const currentLang = localStorage.getItem('lang') || 'en' + + const messages = { + en: { + title: "Check in Successfully", + backgroundMessage: "Location tracking active for work attendance", + notificationText: "Monitoring location for work attendance" + }, + ms: { + title:"Daftar masuk berjaya", + backgroundMessage: "Penjejakan lokasi aktif untuk kehadiran kerja", + notificationText: 'Memantau lokasi untuk kehadiran kerja' + } + } + + return messages[currentLang] || messages.en } /** * Initialize the background location service */ async initialize() { + if (this.isInitialized) return true + try { - // Check if geolocation is available - if (!Capacitor.isNativePlatform()) { - console.warn('Background location limited on web platform') - } - - // Request location permissions first with proper error handling + // Request location permissions const permissions = await Geolocation.requestPermissions() - if (permissions.location !== 'granted') { - console.error('PRODUCTION: Location permission DENIED') - console.error('PRODUCTION: Background location tracking requires "Allow all the time" permission') - - // Show user-friendly guidance - alert('Location permission is required for work attendance monitoring. Please:\n\n1. Go to App Settings\n2. Select Permissions\n3. Choose Location\n4. Select "Allow all the time"\n\nThis ensures accurate attendance tracking.') + console.error('Location permission denied') return false } - console.log('PRODUCTION: Location permissions granted:', permissions) - - // Initialize background geolocation with community plugin + // Initialize background geolocation watcher if (Capacitor.isNativePlatform()) { - try { - this.watcherId = await BackgroundGeolocation.addWatcher( - { - // PRODUCTION CONFIGURATION - Optimized for 30-minute intervals - requestPermissions: true, - stale: false, - distanceFilter: 50, // 50 meters - reduce sensitivity for battery life - backgroundMessage: "Work attendance monitoring is active.", - backgroundTitle: "Work Location Tracking", + const messages = this.getNotificationMessages() - // Battery-optimized settings for long-term operation - enableHighAccuracy: false, // Use network location for better battery life - interval: 30 * 60 * 1000, // 30 minutes - matches production requirement - fastestInterval: 5 * 60 * 1000, // 5 minutes minimum - activitiesInterval: 30 * 60 * 1000, // 30 minutes for activity detection - saveBatteryOnBackground: true, // Optimize for battery in background - - // Foreground service notification (handled by plugin) - notificationTitle: "Work Location Tracking", - notificationText: "Monitoring location for work attendance", - notificationIconColor: "#0066CC", - notificationIconLarge: "ic_launcher", - notificationIconSmall: "ic_notification", - - // Critical settings for Android power management - pauseLocationUpdatesAutomatically: false, - locationUpdatesPaused: false, - showsBackgroundLocationIndicator: true, - allowsBackgroundLocationUpdates: true, - - // Additional production settings - desiredAccuracy: 100, // 100 meters accuracy is sufficient for geofencing - maxWaitTime: 5 * 60 * 1000, // 5 minutes max wait for location - deferredUpdatesInterval: 30 * 60 * 1000 // 30 minutes for deferred updates - }, - (location, error) => { - if (error) { - console.error('Background location error:', error) - return - } - - if (location) { - console.log('Background location update:', location) - this.onLocationUpdate(location) - } + this.watcherId = await BackgroundGeolocation.addWatcher( + { + requestPermissions: true, + stale: false, + distanceFilter: 50, + backgroundMessage: messages.backgroundMessage, + backgroundTitle: messages.title, + enableHighAccuracy: false, + interval: this.locationUpdateInterval, + fastestInterval: 5 * 60 * 1000, + saveBatteryOnBackground: true, + notificationTitle: messages.title, + notificationText: messages.notificationText, + notificationIconColor: "#0066CC", + pauseLocationUpdatesAutomatically: false, + allowsBackgroundLocationUpdates: true, + desiredAccuracy: 100 + }, + (location, error) => { + if (error) { + console.error('Background location error:', error) + return } - ) - - console.log('Background geolocation watcher added with ID:', this.watcherId) - } catch (error) { - console.warn('Background geolocation not available, using fallback:', error) - } + if (location) { + this.handleLocationUpdate(location) + } + } + ) } - // Initialize network monitoring for offline data management + // Initialize network monitoring await this.initializeNetworkMonitoring() - // Check if worker was previously clocked in and auto-start tracking if needed - await this.checkAndRestoreTrackingState() + // Restore previous clock status + await this.restoreClockStatus() this.isInitialized = true - console.log('🎉 LOCATION_SERVICE: Background location service initialized successfully') + console.log('Background location service initialized') return true } catch (error) { @@ -146,78 +112,39 @@ class BackgroundLocationService { } /** - * Initialize network monitoring for offline data management + * Initialize network monitoring */ async initializeNetworkMonitoring() { - if (!Capacitor.isNativePlatform()) { - // Web platform - use navigator.onLine - this.isOnline = navigator.onLine - window.addEventListener('online', () => this.onNetworkStatusChange(true)) - window.addEventListener('offline', () => this.onNetworkStatusChange(false)) - return - } - try { - // Get initial network status - const status = await Network.getStatus() - this.isOnline = status.connected - console.log('PRODUCTION: Initial network status:', this.isOnline ? 'online' : 'offline') + if (Capacitor.isNativePlatform()) { + const status = await Network.getStatus() + this.isOnline = status.connected - // Listen for network changes - this.networkListener = Network.addListener('networkStatusChange', (status) => { - this.onNetworkStatusChange(status.connected) - }) + this.networkListener = Network.addListener('networkStatusChange', (status) => { + const wasOnline = this.isOnline + this.isOnline = status.connected - console.log('PRODUCTION: Network monitoring initialized') + // Sync pending data when coming back online + if (!wasOnline && status.connected) { + this.syncPendingLocationData() + } + }) + } else { + this.isOnline = navigator.onLine + window.addEventListener('online', () => { + this.isOnline = true + this.syncPendingLocationData() + }) + window.addEventListener('offline', () => { + this.isOnline = false + }) + } } catch (error) { - console.error('PRODUCTION: Failed to initialize network monitoring:', error) - // Assume online if network monitoring fails + console.error('Failed to initialize network monitoring:', error) this.isOnline = true } } - /** - * Handle network status changes - */ - async onNetworkStatusChange(isConnected) { - const wasOnline = this.isOnline - this.isOnline = isConnected - - console.log(`PRODUCTION: Network status changed: ${isConnected ? 'online' : 'offline'}`) - - if (!wasOnline && isConnected) { - // Just came back online - sync pending data - console.log('PRODUCTION: Network restored - syncing pending location data') - try { - await this.syncPendingLocationData() - } catch (error) { - console.error('PRODUCTION: Failed to sync data after network restoration:', error) - } - } else if (wasOnline && !isConnected) { - // Just went offline - console.log('PRODUCTION: Network lost - location data will be stored locally') - } - - // Store network status for recovery - try { - localStorage.setItem(this.NETWORK_STATUS_KEY, JSON.stringify({ - isOnline: isConnected, - timestamp: Date.now() - })) - } catch (error) { - console.error('Failed to store network status:', error) - } - } - - /** - * Set up event listeners for location updates - */ - setupEventListeners() { - // The community plugin uses watchers instead of event listeners - // Location updates are handled in the watcher callback - console.log('Event listeners set up (using watcher callback)') - } - /** * Start location tracking */ @@ -229,30 +156,17 @@ class BackgroundLocationService { } } - try { - // The community plugin starts tracking when watcher is added - // If watcher is already added, tracking is already active - if (this.watcherId) { - this.isTracking = true - console.log('Background location tracking started (watcher active)') - } else { - // Re-initialize if watcher was removed - await this.initialize() - this.isTracking = true - } - - // Start periodic location updates + if (this.watcherId) { + this.isTracking = true this.startPeriodicUpdates() - return true - } catch (error) { - console.error('Failed to start location tracking:', error) - throw error } + + throw new Error('Background location watcher not available') } /** - * Stop location tracking - ENHANCED with cleanup + * Stop location tracking */ async stopTracking() { try { @@ -262,20 +176,13 @@ class BackgroundLocationService { } this.isTracking = false - console.log('Background location tracking stopped') - - // Stop periodic updates this.stopPeriodicUpdates() - // Clean up network listener if (this.networkListener) { this.networkListener.remove() this.networkListener = null } - // Perform final data cleanup - await this.cleanupOfflineData() - return true } catch (error) { console.error('Failed to stop location tracking:', error) @@ -284,46 +191,25 @@ class BackgroundLocationService { } /** - * Start periodic location updates - PRODUCTION: 30 minutes + * Start periodic location updates - 30 minutes */ startPeriodicUpdates() { - this.stopPeriodicUpdates() // Clean up any existing timer + this.stopPeriodicUpdates() - console.log(`Starting PRODUCTION periodic updates every ${this.locationUpdateInterval/60000} minutes`) - this.serviceStartTime = Date.now() + console.log(`Starting periodic updates every ${this.locationUpdateInterval/60000} minutes`) this.periodicUpdateTimer = setInterval(async () => { + if (!this.isClockedIn) return + try { - console.log('PRODUCTION: Executing 30-minute location update with server-side geofence check') await this.getCurrentLocationAndSend({ eventType: 'periodic_30min', checkGeofence: true }) - this.lastHeartbeat = Date.now() } catch (error) { - console.error('PRODUCTION: 30-minute location update failed, trying fallback:', error) - - // Fallback: use last known background location if available and recent - if (this.lastBackgroundLocation) { - const locationAge = Date.now() - this.lastBackgroundLocation.timestamp - if (locationAge < 60 * 60 * 1000) { // Use if less than 1 hour old - console.log('PRODUCTION: Using background location fallback with server-side geofence check') - try { - await this.sendLocationToServer(this.lastBackgroundLocation, { - eventType: 'periodic_30min', - checkGeofence: true - }) - this.lastHeartbeat = Date.now() - } catch (fallbackError) { - console.error('PRODUCTION: Fallback location update also failed:', fallbackError) - } - } - } + console.error('Periodic location update failed:', error) } }, this.locationUpdateInterval) - - // Start heartbeat timer for service health monitoring - this.startHeartbeat() } /** @@ -334,72 +220,19 @@ class BackgroundLocationService { clearInterval(this.periodicUpdateTimer) this.periodicUpdateTimer = null } - this.stopHeartbeat() } /** - * Start heartbeat timer for service health monitoring - */ - startHeartbeat() { - this.stopHeartbeat() // Clean up any existing timer - - console.log(`Starting service heartbeat every ${this.heartbeatInterval/60000} minutes`) - - this.heartbeatTimer = setInterval(async () => { - try { - console.log('PRODUCTION: Service heartbeat - checking health') - - // Check if main service is still running - if (this.isClockedIn && !this.isTracking) { - console.warn('PRODUCTION: Service stopped unexpectedly, restarting...') - await this.startTracking() - } - - // Check if we haven't sent location in too long (2x the expected interval) - const timeSinceLastUpdate = Date.now() - (this.lastHeartbeat || this.serviceStartTime || Date.now()) - if (timeSinceLastUpdate > (this.locationUpdateInterval * 2)) { - console.warn('PRODUCTION: No location updates for too long, forcing update...') - try { - await this.getCurrentLocationAndSend() - this.lastHeartbeat = Date.now() - } catch (error) { - console.error('PRODUCTION: Forced location update failed:', error) - } - } - - // Refresh foreground notification to keep service alive - if (this.isClockedIn) { - await this.refreshForegroundNotification() - } - - } catch (error) { - console.error('PRODUCTION: Heartbeat failed:', error) - } - }, this.heartbeatInterval) - } - - /** - * Stop heartbeat timer - */ - stopHeartbeat() { - if (this.heartbeatTimer) { - clearInterval(this.heartbeatTimer) - this.heartbeatTimer = null - } - } - - /** - * Get current location and send to server - OPTIMIZED with options + * Get current location and send to server */ async getCurrentLocationAndSend(options = {}) { try { const position = await Geolocation.getCurrentPosition({ - timeout: 60000, // Increased timeout for background operation - maximumAge: 30000, // Allow slightly older location data in background - enableHighAccuracy: false // Use less accurate but more reliable location in background + timeout: 30000, + maximumAge: 30000, + enableHighAccuracy: false }) - // OPTIMIZED: Convert to simplified format (only essential fields) const location = { coords: { latitude: position.coords.latitude, @@ -411,115 +244,71 @@ class BackgroundLocationService { await this.sendLocationToServer(location, options) return location } catch (error) { - console.error('OPTIMIZED: Failed to get current location:', error) + console.error('Failed to get current location:', error) throw error } } /** - * Send location data to server - ENHANCED with offline handling and network monitoring + * Send location data to server */ + async sendLocationToServer(location, options = {}) { + if (!this.isClockedIn) return; + try { - console.log('🌍 LOCATION_SERVICE: Starting location send process...') - - // Only send location updates if worker is clocked in - if (!this.isClockedIn) { - console.log('⏰ LOCATION_SERVICE: Worker not clocked in, skipping location update') - return - } - - const token = localStorage.getItem('token') || sessionStorage.getItem('token') - const userId = localStorage.getItem('userId') || sessionStorage.getItem('userId') - - console.log('🔑 LOCATION_SERVICE: Auth check - Token:', !!token, 'UserId:', userId) + const token = localStorage.getItem('token') || sessionStorage.getItem('token'); + const userId = localStorage.getItem('userId') || sessionStorage.getItem('userId'); if (!token || !userId) { - console.warn('❌ LOCATION_SERVICE: No authentication token or user ID available') - return + console.warn('No authentication token or user ID available'); + return; } - // ENHANCED: Use longitude-before-latitude coordinate ordering following geographic conventions const locationData = { - userId: userId, // Match server API expectation (camelCase) - longitude: location.coords.longitude, // Longitude first per geographic conventions - latitude: location.coords.latitude + userId: userId, + longitude: location.coords.longitude, + latitude: location.coords.latitude, + }; + + if (options.checkGeofence) { + locationData.checkGeofence = true; } - - console.log('📍 LOCATION_SERVICE: Prepared location data:', { - userId: locationData.userId, - longitude: locationData.longitude, - latitude: locationData.latitude, - hasCoords: !!location.coords - }) - - // SERVER-SIDE GEOFENCE: Include flag for server to perform geofence validation - if (options.checkGeofence === true) { - locationData.checkGeofence = true - console.log('🛡️ LOCATION_SERVICE: Requesting server-side geofence validation') - } - - // Add event type for special events (clock-in, app resume) if (options.eventType) { - locationData.eventType = options.eventType - console.log('🏷️ LOCATION_SERVICE: Event type:', options.eventType) + locationData.eventType = options.eventType; } - // OFFLINE HANDLING: Check network status before attempting to send + // Store for retry if offline if (!this.isOnline) { - console.log('📱 LOCATION_SERVICE: Device offline - storing location data locally') - await this.storeLocationForRetry(location, options) - return + await this.storeLocationForRetry(location, options); + return; } - console.log('🚀 LOCATION_SERVICE: Sending to server /api/location/update...') - - const response = await apiFetch('/api/location/update', { + // Use standard fetch - CapacitorHttp will patch it + const response = await fetch(`${import.meta.env.VITE_API_BASE_URL}/api/location/update`, { method: 'POST', - body: JSON.stringify(locationData) - }) + body: JSON.stringify(locationData), + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}` + } + }); - console.log('✅ LOCATION_SERVICE: Server response received:', response) + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } - this.lastLocationUpdate = new Date() - - // Store last known location for offline recovery - await this.storeLastKnownLocation(location) - - console.log('🎉 LOCATION_SERVICE: Location sent to server successfully!', { - userId: locationData.userId, - longitude: locationData.longitude, // Log longitude first - latitude: locationData.latitude, - checkGeofence: locationData.checkGeofence || false, - eventType: locationData.eventType || 'periodic' - }) + console.log('Location sent to server successfully via patched fetch'); } catch (error) { - console.error('❌ LOCATION_SERVICE: Failed to send location to server:', error) - console.error('❌ LOCATION_SERVICE: Error details:', { - message: error.message, - stack: error.stack, - location: location - }) - - // ENHANCED ERROR HANDLING: Distinguish between network and other errors - const isNetworkError = error.message.includes('fetch') || - error.message.includes('network') || - error.message.includes('timeout') - - if (isNetworkError) { - console.log('📡 LOCATION_SERVICE: Network error detected - storing location for retry') - this.isOnline = false // Update network status - } - - // Store location locally for retry later - console.log('💾 LOCATION_SERVICE: Storing location for retry...') - await this.storeLocationForRetry(location, options) + console.error('Failed to send location to server:', error); + // Store for retry on error + await this.storeLocationForRetry(location, options); } } /** - * Store location data locally for retry - ENHANCED with queue management and data integrity + * Store location data locally for retry */ async storeLocationForRetry(location, options = {}) { try { @@ -529,60 +318,25 @@ class BackgroundLocationService { location, options, timestamp: Date.now(), - retryCount: 0, - id: Date.now() + Math.random() // Unique identifier for deduplication + retryCount: 0 } pendingLocations.push(locationEntry) - // ENHANCED QUEUE MANAGEMENT: Implement intelligent queue size management - if (pendingLocations.length > this.maxOfflineQueueSize) { - // Remove oldest entries, but prioritize important events - const importantEvents = pendingLocations.filter(entry => - entry.options?.eventType === 'clock_in' || - entry.options?.eventType === 'clock_out' - ) - - const regularEvents = pendingLocations.filter(entry => - entry.options?.eventType !== 'clock_in' && - entry.options?.eventType !== 'clock_out' - ) - - // Keep all important events and most recent regular events - const maxRegularEvents = this.maxOfflineQueueSize - importantEvents.length - const trimmedRegularEvents = regularEvents.slice(-maxRegularEvents) - - const finalQueue = [...importantEvents, ...trimmedRegularEvents] - localStorage.setItem(this.PENDING_LOCATIONS_KEY, JSON.stringify(finalQueue)) - - console.log(`PRODUCTION: Queue trimmed - kept ${importantEvents.length} important, ${trimmedRegularEvents.length} regular events`) - } else { - localStorage.setItem(this.PENDING_LOCATIONS_KEY, JSON.stringify(pendingLocations)) + // Keep only last 50 locations to prevent storage bloat + if (pendingLocations.length > 50) { + pendingLocations.splice(0, pendingLocations.length - 50) } - this.offlineQueueSize = pendingLocations.length - console.log('PRODUCTION: Location stored for retry, queue size:', this.offlineQueueSize) - - // Update storage usage metrics - await this.updateStorageMetrics() + localStorage.setItem(this.PENDING_LOCATIONS_KEY, JSON.stringify(pendingLocations)) } catch (error) { console.error('Failed to store location for retry:', error) - - // FALLBACK: Try to clear old data and retry once - try { - const pendingLocations = JSON.parse(localStorage.getItem(this.PENDING_LOCATIONS_KEY) || '[]') - const recentLocations = pendingLocations.slice(-10) // Keep only last 10 - localStorage.setItem(this.PENDING_LOCATIONS_KEY, JSON.stringify(recentLocations)) - console.log('PRODUCTION: Storage cleared, retrying with reduced queue') - } catch (fallbackError) { - console.error('PRODUCTION: Failed to clear storage for retry:', fallbackError) - } } } /** - * Store clock status persistently for app restart recovery + * Store clock status persistently */ async storeClockStatus(isClockedIn) { try { @@ -604,10 +358,9 @@ class BackgroundLocationService { const stored = localStorage.getItem(this.CLOCK_STATUS_KEY) if (stored) { const clockStatus = JSON.parse(stored) - // Only restore if timestamp is within last 24 hours (prevent stale data) + // Only restore if within last 24 hours if (Date.now() - clockStatus.timestamp < 24 * 60 * 60 * 1000) { this.isClockedIn = clockStatus.isClockedIn - console.log('🔄 LOCATION_SERVICE: Restored clock status:', clockStatus.isClockedIn) return clockStatus.isClockedIn } } @@ -618,144 +371,34 @@ class BackgroundLocationService { } } - /** - * Check and restore tracking state during initialization - */ - async checkAndRestoreTrackingState() { - try { - console.log('🔍 LOCATION_SERVICE: Checking if tracking should be restored...') - const wasClocked = await this.restoreClockStatus() - - if (wasClocked) { - console.log('⚡ LOCATION_SERVICE: Worker was previously clocked in - will start tracking when status is confirmed by server') - // Note: Don't start tracking here immediately, wait for server confirmation - // The WorkerDashboardView will call setClockedInStatus() after fetching from server - } else { - console.log('📴 LOCATION_SERVICE: No previous clock-in status found') - } - - } catch (error) { - console.error('❌ LOCATION_SERVICE: Failed to check tracking state:', error) - } - } /** - * Store last known location for offline recovery + * Sync pending location data */ - async storeLastKnownLocation(location) { - try { - const locationData = { - coords: location.coords, - timestamp: location.timestamp || Date.now() - } - localStorage.setItem(this.LAST_LOCATION_KEY, JSON.stringify(locationData)) - } catch (error) { - console.error('Failed to store last known location:', error) - } - } - - /** - * Update storage usage metrics for monitoring - */ - async updateStorageMetrics() { - try { - const pendingData = localStorage.getItem(this.PENDING_LOCATIONS_KEY) || '[]' - const storageSize = new Blob([pendingData]).size - const storageSizeKB = Math.round(storageSize / 1024) - - console.log(`PRODUCTION: Offline storage usage: ${storageSizeKB}KB, ${this.offlineQueueSize} locations`) - - // Warn if storage is getting large - if (storageSizeKB > 500) { // 500KB threshold - console.warn(`PRODUCTION: Offline storage is large (${storageSizeKB}KB) - consider sync`) - } - } catch (error) { - console.error('Failed to update storage metrics:', error) - } - } - - /** - * Clean up old offline data to prevent storage bloat - */ - async cleanupOfflineData() { + async syncPendingLocationData() { try { const pendingLocations = JSON.parse(localStorage.getItem(this.PENDING_LOCATIONS_KEY) || '[]') - const now = Date.now() - const maxAge = 7 * 24 * 60 * 60 * 1000 // 7 days + if (pendingLocations.length === 0) return - // Remove entries older than 7 days - const validLocations = pendingLocations.filter(entry => - (now - entry.timestamp) < maxAge - ) - - if (validLocations.length !== pendingLocations.length) { - localStorage.setItem(this.PENDING_LOCATIONS_KEY, JSON.stringify(validLocations)) - console.log(`PRODUCTION: Cleaned up ${pendingLocations.length - validLocations.length} old offline entries`) - } - - this.offlineQueueSize = validLocations.length - } catch (error) { - console.error('Failed to cleanup offline data:', error) - } - } - - /** - * Retry sending pending location updates - ENHANCED with retry limits and options - */ - async retryPendingLocationUpdates() { - try { - const pendingLocations = JSON.parse(localStorage.getItem(this.PENDING_LOCATIONS_KEY) || '[]') - - if (pendingLocations.length === 0) { - return - } - - console.log(`Retrying ${pendingLocations.length} pending location updates`) - const successfulRetries = [] const failedRetries = [] - for (let i = 0; i < pendingLocations.length; i++) { - const pendingLocation = pendingLocations[i] - - // Skip if retry count exceeds limit (max 3 retries) - if (pendingLocation.retryCount >= 3) { - console.warn('Max retry count reached for location update, discarding') - continue - } + for (const pendingLocation of pendingLocations) { + // Skip if retry count exceeds limit + if (pendingLocation.retryCount >= 3) continue try { await this.sendLocationToServer(pendingLocation.location, pendingLocation.options || {}) - successfulRetries.push(i) - } catch (error) { - console.error('Failed to retry location update:', error) + } catch { pendingLocation.retryCount = (pendingLocation.retryCount || 0) + 1 failedRetries.push(pendingLocation) } } - // Update pending locations with only failed retries localStorage.setItem(this.PENDING_LOCATIONS_KEY, JSON.stringify(failedRetries)) - console.log(`OPTIMIZED: Retry completed - ${successfulRetries.length} successful, ${failedRetries.length} remaining`) - } catch (error) { - console.error('Failed to retry pending location updates:', error) - } - } - - /** - * Sync all pending location data - comprehensive data recovery - */ - async syncPendingLocationData() { - try { - console.log('PRODUCTION: Syncing all pending location data...') - await this.retryPendingLocationUpdates() - - // Additional sync operations can be added here - console.log('PRODUCTION: Location data sync completed') - } catch (error) { - console.error('PRODUCTION: Failed to sync pending location data:', error) + console.error('Failed to sync pending location data:', error) } } @@ -765,103 +408,52 @@ class BackgroundLocationService { * Handle app resume - capture location immediately if worker is clocked in */ async onAppResume() { - console.log('PRODUCTION: App resumed - checking location tracking status') - try { - // Restore clock status if needed - const wasClocked = await this.restoreClockStatus() + await this.restoreClockStatus() - if (this.isClockedIn || wasClocked) { - console.log('PRODUCTION: Worker is clocked in - performing app resume location update') - - // APP RESUME EVENT: Update location instantly with server-side geofence check + if (this.isClockedIn) { + // Capture location on app resume try { await this.getCurrentLocationAndSend({ eventType: 'app_resume', - checkGeofence: true, - immediate: true + checkGeofence: true }) - console.log('PRODUCTION: App resume location update completed') } catch (locationError) { - console.error('PRODUCTION: Failed to capture app resume location:', locationError) + console.error('Failed to capture app resume location:', locationError) } - // Ensure location tracking is still active + // Ensure tracking is active if (!this.isTracking) { - console.log('PRODUCTION: Restarting location tracking after app resume') await this.startTracking() } - // Sync any pending location data + // Sync pending data await this.syncPendingLocationData() } } catch (error) { - console.error('PRODUCTION: Failed to handle app resume:', error) + console.error('Failed to handle app resume:', error) } } /** - * Handle app going to background + * Handle location update from background watcher */ - async onAppBackground() { - console.log('PRODUCTION: App going to background') - this.isInBackground = true + handleLocationUpdate(location) { + if (!this.isClockedIn) return - // Ensure location tracking continues if worker is clocked in - if (this.isClockedIn && !this.isTracking) { - console.log('PRODUCTION: Ensuring location tracking continues in background') - try { - await this.startTracking() - } catch (error) { - console.error('PRODUCTION: Failed to ensure background tracking:', error) - } - } - } - - // Event handlers - onLocationUpdate(location) { - console.log('Location update received from background watcher:', location) - - // Store the latest location from background watcher - this.lastBackgroundLocation = { + const locationData = { coords: { latitude: location.latitude, - longitude: location.longitude, - accuracy: location.accuracy, - speed: location.speed || 0, - heading: location.bearing || 0, - altitude: location.altitude || 0 + longitude: location.longitude }, timestamp: location.time || Date.now() } - // Send this location update to server if worker is clocked in - this.sendLocationToServer(this.lastBackgroundLocation).catch(error => { + this.sendLocationToServer(locationData).catch(error => { console.error('Failed to send background location update:', error) }) } - onLocationError(error) { - console.error('Location error:', error) - } - - onMotionChange(event) { - console.log('Motion change:', event) - } - - onActivityChange(event) { - console.log('Activity change:', event) - } - - onHttpSuccess(response) { - console.log('HTTP success:', response) - } - - onHttpFailure(response) { - console.error('HTTP failure:', response) - // Handle HTTP failures - maybe store for retry - } - /** * Check if location tracking is currently active */ @@ -870,134 +462,76 @@ class BackgroundLocationService { } /** - * Set worker clock-in status - ENHANCED with immediate location capture and geofence validation + * Set worker clock-in status */ async setClockedInStatus(isClockedIn) { - console.log(`🔄 LOCATION_SERVICE: Setting worker clock-in status to: ${isClockedIn}`) - console.log(`🔄 LOCATION_SERVICE: Previous status was: ${this.isClockedIn}`) - const wasAlreadyClockedIn = this.isClockedIn this.isClockedIn = isClockedIn - // Store clock status persistently for app restart recovery await this.storeClockStatus(isClockedIn) if (isClockedIn) { - console.log(`🔄 LOCATION_SERVICE: Worker is clocked in - ensuring location tracking is active`) - - // If worker was already clocked in and tracking is already active, don't capture location again - const shouldCaptureLocation = !wasAlreadyClockedIn - - if (shouldCaptureLocation) { - console.log(`📍 LOCATION_SERVICE: New clock-in detected - capturing location immediately`) - } else { - console.log(`📍 LOCATION_SERVICE: Restoring existing clock-in status - no immediate location capture needed`) - } - // Worker clocked in - check permissions first - const permissionsGranted = await this.checkAndRequestAllPermissions() + // Check permissions + const permissionsGranted = await this.checkPermissions() if (!permissionsGranted) { - console.error('PRODUCTION: Cannot start location tracking - permissions denied') + console.error('Cannot start location tracking - permissions denied') return false } - // CLOCK-IN EVENT: Immediately capture and verify location against geofences (only for new clock-ins) - if (shouldCaptureLocation) { + // Capture location on new clock-in + if (!wasAlreadyClockedIn) { try { - console.log('📍 LOCATION_SERVICE: Clock-in event - capturing location with geofence validation') - const location = await this.getCurrentLocationAndSend({ + await this.getCurrentLocationAndSend({ checkGeofence: true, - eventType: 'clock_in', - immediate: true + eventType: 'clock_in' }) - console.log('✅ LOCATION_SERVICE: Clock-in location captured successfully:', location) } catch (locationError) { - console.error('❌ LOCATION_SERVICE: Failed to capture clock-in location:', locationError) - // Continue with tracking setup even if initial location fails + console.error('Failed to capture clock-in location:', locationError) } } - // Start production location tracking (always ensure it's running when clocked in) + // Start tracking if (!this.isTracking) { try { - console.log('🚀 LOCATION_SERVICE: Starting location tracking...') await this.startTracking() - console.log('✅ LOCATION_SERVICE: Location tracking started successfully') } catch (error) { - console.error('❌ LOCATION_SERVICE: Failed to start location tracking after clock-in:', error) + console.error('Failed to start location tracking:', error) return false } - } else { - console.log('✅ LOCATION_SERVICE: Location tracking already active') } - // Start foreground service for battery optimization protection - try { - const serviceStarted = await this.startForegroundService() - if (!serviceStarted) { - console.warn('PRODUCTION: Foreground service failed to start, tracking may be unreliable') - } - } catch (error) { - console.error('PRODUCTION: Failed to start foreground service:', error) - } - - // Request battery optimization exemption for reliable background operation - try { - await this.requestBatteryOptimizationExemption() - } catch (error) { - console.error('PRODUCTION: Failed to request battery optimization exemption:', error) - } + // Start foreground service + await this.startForegroundService() return true } else { - // CLOCK-OUT EVENT: Stop all location tracking immediately - console.log('PRODUCTION: Clock-out event - stopping all location tracking') - + // Clock-out: stop tracking if (this.isTracking) { - try { - await this.stopTracking() - console.log('PRODUCTION: Location tracking stopped successfully') - } catch (error) { - console.error('PRODUCTION: Failed to stop location tracking after clock-out:', error) - } + await this.stopTracking() } - try { - await this.stopForegroundService() - console.log('PRODUCTION: Foreground service stopped successfully') - } catch (error) { - console.error('PRODUCTION: Failed to stop foreground service:', error) - } - - // Sync any remaining pending location data before stopping - try { - await this.syncPendingLocationData() - } catch (syncError) { - console.error('PRODUCTION: Failed to sync pending location data on clock-out:', syncError) - } + await this.stopForegroundService() + await this.syncPendingLocationData() return true } } /** - * Check and request all necessary permissions for production operation + * Check permissions */ - async checkAndRequestAllPermissions() { + async checkPermissions() { if (!Capacitor.isNativePlatform()) { return true } try { - console.log('PRODUCTION: Checking all required permissions...') - // Check location permissions const locationPermissions = await Geolocation.checkPermissions() if (locationPermissions.location !== 'granted') { - console.warn('PRODUCTION: Location permission not granted, requesting...') const requested = await Geolocation.requestPermissions() if (requested.location !== 'granted') { - this.showPermissionGuidance('location') return false } } @@ -1005,58 +539,20 @@ class BackgroundLocationService { // Check notification permissions const notificationPermissions = await LocalNotifications.checkPermissions() if (notificationPermissions.display !== 'granted') { - console.warn('PRODUCTION: Notification permission not granted, requesting...') const requested = await LocalNotifications.requestPermissions() if (requested.display !== 'granted') { - this.showPermissionGuidance('notification') return false } } - console.log('PRODUCTION: All permissions granted successfully') return true } catch (error) { - console.error('PRODUCTION: Failed to check/request permissions:', error) + console.error('Failed to check permissions:', error) return false } } - /** - * Show user-friendly permission guidance - */ - showPermissionGuidance(permissionType) { - const messages = { - location: 'Location permission is required for work attendance monitoring.\n\nPlease:\n1. Go to App Settings\n2. Select Permissions\n3. Choose Location\n4. Select "Allow all the time"\n\nThis ensures accurate attendance tracking.', - notification: 'Notification permission is required for background location tracking.\n\nPlease:\n1. Go to App Settings\n2. Select Permissions\n3. Enable Notifications\n\nThis keeps the location service active when the app is in background.' - } - - alert(messages[permissionType] || 'Permission required for proper app functionality.') - } - - /** - * Request battery optimization exemption for reliable background operation - */ - async requestBatteryOptimizationExemption() { - if (!Capacitor.isNativePlatform()) { - return - } - - try { - console.log('PRODUCTION: Requesting battery optimization exemption...') - - // Show user guidance for battery optimization - const shouldShowGuidance = localStorage.getItem('battery_guidance_shown') !== 'true' - if (shouldShowGuidance) { - alert('For reliable background location tracking, please:\n\n1. Go to Settings > Apps > Nilai Clock\n2. Select Battery\n3. Choose "Don\'t optimize" or "Allow background activity"\n\nThis prevents the system from stopping location tracking.') - localStorage.setItem('battery_guidance_shown', 'true') - } - - } catch (error) { - console.error('PRODUCTION: Failed to request battery optimization exemption:', error) - } - } - /** * Get current clock-in status */ @@ -1065,154 +561,49 @@ class BackgroundLocationService { } /** - * Set background state and adjust tracking accordingly - */ - setBackgroundState(isInBackground) { - console.log(`App background state changed: ${isInBackground}`) - this.isInBackground = isInBackground - - // Restart periodic updates with appropriate interval - if (this.isTracking && this.isClockedIn) { - this.stopPeriodicUpdates() - this.startPeriodicUpdates() - } - } - - /** - * Start foreground service to keep location tracking active when device is locked + * Start foreground service */ async startForegroundService() { if (!Capacitor.isNativePlatform()) { - return + return true } try { - // Request notification permissions first and handle denial properly const permission = await LocalNotifications.requestPermissions() if (permission.display !== 'granted') { - console.error('PRODUCTION: Notification permission DENIED - foreground service cannot start') - console.error('PRODUCTION: Please grant notification permission in app settings for reliable background operation') - - // Show user-friendly error message - alert('Notification permission is required for background location tracking. Please enable notifications in app settings.') + console.error('Notification permission denied') return false } - console.log('PRODUCTION: Notification permission granted, starting foreground service') + const messages = this.getNotificationMessages() - // Create initial persistent notification - await this.createInitialForegroundNotification() + await LocalNotifications.schedule({ + notifications: [ + { + title: messages.title, + body: messages.notificationText, + id: 999, + sound: null, + smallIcon: "ic_stat_location_on", + iconColor: "#0066CC", + ongoing: true, + autoCancel: false, + extra: { + priority: "high", + category: "service", + visibility: "public" + } + } + ] + }) - // Set up notification refresh timer (less frequent to avoid system conflicts) - this.notificationRefreshTimer = setInterval(async () => { - try { - await this.updateForegroundNotification() - } catch (refreshError) { - console.error('PRODUCTION: Failed to refresh notification:', refreshError) - } - }, this.notificationRefreshInterval) - - console.log('PRODUCTION: Foreground service started successfully with notification refresh') return true } catch (error) { - console.error('PRODUCTION: Failed to start foreground service:', error) + console.error('Failed to start foreground service:', error) return false } } - /** - * Create initial foreground notification - */ - async createInitialForegroundNotification() { - if (!Capacitor.isNativePlatform()) { - return - } - - try { - console.log('PRODUCTION: Creating initial foreground notification') - - await LocalNotifications.schedule({ - notifications: [ - { - title: "Work Location Tracking", - body: "Location tracking started for work attendance monitoring", - id: 999, - sound: null, - smallIcon: "ic_stat_location_on", - iconColor: "#0066CC", - ongoing: true, - autoCancel: false, - extra: { - priority: "high", - category: "service", - visibility: "public", - showWhen: true, - when: Date.now() - } - } - ] - }) - - console.log('PRODUCTION: Initial foreground notification created successfully') - } catch (error) { - console.error('PRODUCTION: Failed to create initial foreground notification:', error) - throw error - } - } - - /** - * Update foreground notification with current status - */ - async updateForegroundNotification() { - if (!Capacitor.isNativePlatform()) { - return - } - - try { - const uptime = this.serviceStartTime ? Math.floor((Date.now() - this.serviceStartTime) / 60000) : 0 - const nextUpdate = new Date(Date.now() + this.locationUpdateInterval).toLocaleTimeString() - - // Cancel existing notification first to avoid conflicts - await LocalNotifications.cancel({ - notifications: [{ id: 999 }] - }) - - // Wait a moment before creating new notification - await new Promise(resolve => setTimeout(resolve, 100)) - - await LocalNotifications.schedule({ - notifications: [ - { - title: "Work Location Tracking", - body: `Active for ${uptime} min - Next update: ${nextUpdate}`, - id: 999, - sound: null, - smallIcon: "ic_stat_location_on", - iconColor: "#0066CC", - ongoing: true, - autoCancel: false, - extra: { - priority: "high", - category: "service", - visibility: "public", - showWhen: true, - when: Date.now() - } - } - ] - }) - } catch (error) { - console.error('PRODUCTION: Failed to update foreground notification:', error) - } - } - - /** - * Refresh foreground notification to keep service alive (legacy method) - */ - async refreshForegroundNotification() { - return this.updateForegroundNotification() - } - /** * Stop foreground service */ @@ -1222,54 +613,14 @@ class BackgroundLocationService { } try { - // Stop the notification refresh timer - if (this.notificationRefreshTimer) { - clearInterval(this.notificationRefreshTimer) - this.notificationRefreshTimer = null - } - - // Cancel the persistent notification await LocalNotifications.cancel({ notifications: [{ id: 999 }] }) - console.log('Foreground service stopped') } catch (error) { console.error('Failed to stop foreground service:', error) } } - /** - * Get the last location update timestamp - */ - getLastLocationUpdateTime() { - return this.lastLocationUpdate - } - - /** - * Request location permissions - */ - async requestLocationPermissions() { - try { - const status = await BackgroundGeolocation.requestPermission() - return status - } catch (error) { - console.error('Failed to request location permissions:', error) - throw error - } - } - - /** - * Check location permission status - */ - async getLocationPermissionStatus() { - try { - const status = await BackgroundGeolocation.getProviderState() - return status - } catch (error) { - console.error('Failed to get location permission status:', error) - throw error - } - } } // Create and export a singleton instance diff --git a/src/services/deviceUuidService.js b/src/services/deviceUuidService.js index 9990ce3..01d2634 100644 --- a/src/services/deviceUuidService.js +++ b/src/services/deviceUuidService.js @@ -14,18 +14,12 @@ class DeviceUuidService { this.cachedDeviceInfo = null } - /** - * Initialize the device UUID service - */ async initialize() { try { // Get or create device UUID this.cachedDeviceUuid = await this.getOrCreateDeviceUuid() - // Get device information this.cachedDeviceInfo = await this.getDeviceInfo() - - console.log('Device UUID service initialized with UUID:', this.cachedDeviceUuid) return true } catch (error) { console.error('Failed to initialize device UUID service:', error) @@ -33,9 +27,6 @@ class DeviceUuidService { } } - /** - * Get or create a persistent device UUID - */ async getOrCreateDeviceUuid() { try { // First try to get existing UUID from secure storage @@ -45,25 +36,17 @@ class DeviceUuidService { console.log('Found existing device UUID:', deviceUuid) return deviceUuid } - // If no UUID exists, create a new one deviceUuid = await this.generateDeviceUuid() - // Store the new UUID securely await this.setSecureItem(this.deviceUuidKey, deviceUuid) - - console.log('Generated new device UUID:', deviceUuid) return deviceUuid } catch (error) { console.error('Failed to get/create device UUID:', error) - // Fallback to a session-based UUID return this.generateFallbackUuid() } } - /** - * Generate a device-specific UUID - */ async generateDeviceUuid() { try { let baseString = '' @@ -102,9 +85,6 @@ class DeviceUuidService { } } - /** - * Hash a string to create a UUID-like identifier - */ async hashStringToUuid(str) { try { // Simple hash function to create consistent UUID from string @@ -126,10 +106,6 @@ class DeviceUuidService { return this.generateFallbackUuid() } } - - /** - * Generate a fallback UUID - */ generateFallbackUuid() { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { const r = Math.random() * 16 | 0 @@ -138,83 +114,17 @@ class DeviceUuidService { }) } - /** - * Get comprehensive device information - */ - async getDeviceInfo() { - try { - if (!this.isNative) { - return this.getWebDeviceInfo() - } - - const deviceInfo = await Device.getInfo() - const deviceId = await Device.getId() - - const info = { - uuid: this.cachedDeviceUuid || await this.getOrCreateDeviceUuid(), - platform: deviceInfo.platform, - model: deviceInfo.model, - manufacturer: deviceInfo.manufacturer, - osVersion: deviceInfo.osVersion, - appVersion: deviceInfo.appVersion, - deviceId: deviceId.identifier, - isVirtual: deviceInfo.isVirtual || false, - webViewVersion: deviceInfo.webViewVersion, - memUsed: deviceInfo.memUsed, - diskFree: deviceInfo.diskFree, - diskTotal: deviceInfo.diskTotal, - batteryLevel: await this.getBatteryLevel(), - timestamp: new Date().toISOString() - } - - // Cache device info - await this.setSecureItem(this.deviceInfoKey, JSON.stringify(info)) - - return info - } catch (error) { - console.error('Failed to get device info:', error) - return this.getBasicDeviceInfo() - } - } - - /** - * Get web-based device information - */ - getWebDeviceInfo() { - return { - uuid: this.cachedDeviceUuid, - platform: 'web', - userAgent: navigator.userAgent, - language: navigator.language, - screenWidth: screen.width, - screenHeight: screen.height, - timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, - cookieEnabled: navigator.cookieEnabled, - onLine: navigator.onLine, - timestamp: new Date().toISOString() - } - } - - /** - * Get basic device information as fallback - */ getBasicDeviceInfo() { return { uuid: this.cachedDeviceUuid, platform: this.isNative ? 'mobile' : 'web', timestamp: new Date().toISOString(), - error: 'Failed to get detailed device information' } } - - - /** - * Register device with server - */ async registerDeviceWithServer() { try { - const deviceInfo = await this.getDeviceInfo() + const deviceInfo = await this.getBasicDeviceInfo() // Get user ID from auth service instead of direct storage access const userId = await authService.getUserId() @@ -234,7 +144,6 @@ class DeviceUuidService { const registrationData = { userId: userId, deviceUuid: deviceInfo.uuid, - deviceInfo: deviceInfo } const response = await apiFetch('/api/device/register', { @@ -255,9 +164,7 @@ class DeviceUuidService { } } - /** - * Validate device with server - */ + async validateDeviceWithServer() { try { const deviceUuid = await this.getDeviceUuid() @@ -310,9 +217,6 @@ class DeviceUuidService { } } - /** - * Check if device is registered - */ async isDeviceRegistered() { try { const registered = await this.getSecureItem(this.deviceRegistrationKey) @@ -323,9 +227,6 @@ class DeviceUuidService { } } - /** - * Get current device UUID - */ async getDeviceUuid() { if (this.cachedDeviceUuid) { return this.cachedDeviceUuid @@ -353,65 +254,10 @@ class DeviceUuidService { console.error('Failed to get cached device info:', error) } - this.cachedDeviceInfo = await this.getDeviceInfo() + this.cachedDeviceInfo = await this.getBasicDeviceInfo() return this.cachedDeviceInfo } - /** - * Reset device UUID (for testing or troubleshooting) - */ - async resetDeviceUuid() { - try { - await this.removeSecureItem(this.deviceUuidKey) - await this.removeSecureItem(this.deviceInfoKey) - await this.removeSecureItem(this.deviceRegistrationKey) - - this.cachedDeviceUuid = null - this.cachedDeviceInfo = null - - // Generate new UUID - this.cachedDeviceUuid = await this.getOrCreateDeviceUuid() - - console.log('Device UUID reset successfully. New UUID:', this.cachedDeviceUuid) - return this.cachedDeviceUuid - } catch (error) { - console.error('Failed to reset device UUID:', error) - throw error - } - } - - /** - * Send device heartbeat to server - */ - async sendDeviceHeartbeat() { - try { - const deviceInfo = await this.getCachedDeviceInfo() - - // Get user ID from auth service instead of direct storage access - const userId = await authService.getUserId() - - if (!userId) { - return - } - - const heartbeatData = { - userId: userId, - deviceUuid: deviceInfo.uuid, - timestamp: new Date().toISOString(), - isOnline: navigator.onLine - } - - await apiFetch('/api/device/heartbeat', { - method: 'POST', - body: JSON.stringify(heartbeatData) - }) - - console.log('Device heartbeat sent successfully') - } catch (error) { - console.error('Failed to send device heartbeat:', error) - // Don't throw error for heartbeat failures - } - } // Helper methods for secure storage async setSecureItem(key, value) { diff --git a/src/services/nativeServicesManager.js b/src/services/nativeServicesManager.js index 4504097..6741726 100644 --- a/src/services/nativeServicesManager.js +++ b/src/services/nativeServicesManager.js @@ -100,19 +100,7 @@ class NativeServicesManager { // Continue with other services } - // Register device if not already registered - try { - // Add a small delay to ensure auth data is fully stored - await new Promise(resolve => setTimeout(resolve, 100)) - - if (!(await this.services.deviceUuid.isDeviceRegistered())) { - console.log('Registering device with server...') - await this.services.deviceUuid.registerDeviceWithServer() - } - } catch (deviceError) { - console.error('Failed to register device:', deviceError) - // Continue - don't block login for device registration issues - } + // Device registration simplified - not needed for basic functionality // Start periodic tasks try { @@ -168,19 +156,11 @@ class NativeServicesManager { } }, 24 * 60 * 60 * 1000) // 24 hours - // Device heartbeat every 15 minutes - this.heartbeatInterval = setInterval(async () => { - try { - await this.services.deviceUuid.sendDeviceHeartbeat() - } catch (error) { - console.error('Device heartbeat failed:', error) - } - }, 15 * 60 * 1000) // 15 minutes // Retry failed operations every 5 minutes this.retryInterval = setInterval(async () => { try { - await this.services.location.retryPendingLocationUpdates() + await this.services.location.syncPendingLocationData() await this.services.antiSpoofing.retryPendingSecurityChecks() } catch (error) { console.error('Retry operations failed:', error) @@ -197,11 +177,6 @@ class NativeServicesManager { this.securityCheckInterval = null } - if (this.heartbeatInterval) { - clearInterval(this.heartbeatInterval) - this.heartbeatInterval = null - } - if (this.retryInterval) { clearInterval(this.retryInterval) this.retryInterval = null @@ -215,17 +190,11 @@ class NativeServicesManager { console.log('App going to background - ensuring services continue') try { - // Notify location service about background state - this.services.location.setBackgroundState(true) - // Ensure location tracking continues in background only if worker is clocked in if (this.isNative && this.services.location.getClockedInStatus() && !this.services.location.isLocationTrackingActive()) { console.log('Worker is clocked in, restarting location tracking for background') await this.services.location.startTracking() } - - // Send immediate heartbeat - await this.services.deviceUuid.sendDeviceHeartbeat() } catch (error) { console.error('Failed to handle app background:', error) } @@ -238,9 +207,6 @@ class NativeServicesManager { console.log('OPTIMIZED: App coming to foreground - checking service status') try { - // Notify location service about foreground state - this.services.location.setBackgroundState(false) - // Check if services are still running - only restart if worker is clocked in if (this.isNative && this.services.location.getClockedInStatus() && !this.services.location.isLocationTrackingActive()) { console.log('OPTIMIZED: Worker is clocked in but location tracking stopped, restarting...') @@ -258,11 +224,8 @@ class NativeServicesManager { } // Retry any pending operations - await this.services.location.retryPendingLocationUpdates() + await this.services.location.syncPendingLocationData() await this.services.antiSpoofing.retryPendingSecurityChecks() - - // Send heartbeat - await this.services.deviceUuid.sendDeviceHeartbeat() } catch (error) { console.error('OPTIMIZED: Failed to handle app foreground:', error) } @@ -297,7 +260,7 @@ class NativeServicesManager { location: { initialized: this.services.location.isInitialized, tracking: this.services.location.isLocationTrackingActive(), - lastUpdate: this.services.location.getLastLocationUpdateTime() + lastUpdate: null // Simplified - removed complex tracking }, antiSpoofing: { initialized: true, @@ -305,7 +268,7 @@ class NativeServicesManager { }, deviceUuid: { initialized: true, - uuid: this.services.deviceUuid.cachedDeviceUuid + uuid: 'simplified' // Removed complex UUID tracking } } } @@ -326,10 +289,10 @@ class NativeServicesManager { } /** - * Validate device + * Validate device (simplified) */ async validateDevice() { - return await this.services.deviceUuid.validateDeviceWithServer() + return true // Simplified - always return true } /** @@ -340,10 +303,10 @@ class NativeServicesManager { } /** - * Get device UUID + * Get device UUID (simplified) */ async getDeviceUuid() { - return await this.services.deviceUuid.getDeviceUuid() + return 'simplified-device-uuid' // Simplified implementation } /** @@ -353,9 +316,6 @@ class NativeServicesManager { try { await this.stopServices() - // Reset device UUID - await this.services.deviceUuid.resetDeviceUuid() - // Clear stored credentials await this.services.auth.clearStoredCredentials() diff --git a/src/views/WorkerDashboardView.vue b/src/views/WorkerDashboardView.vue index 388d391..a29f470 100644 --- a/src/views/WorkerDashboardView.vue +++ b/src/views/WorkerDashboardView.vue @@ -110,10 +110,8 @@ const fetchCurrentStatus = async () => { // ENHANCED: Sync clock status with background location service try { - const { backgroundLocationService } = await import('@/services/backgroundLocationService.js') - console.log('🔄 DASHBOARD: Syncing clock status with location service...') - await backgroundLocationService.setClockedInStatus(isCurrentlyClockedIn) + await nativeServicesManager.services.location.setClockedInStatus(isCurrentlyClockedIn) console.log('✅ DASHBOARD: Clock status synced with location service') } catch (locationError) { @@ -145,12 +143,9 @@ const sendClockEvent = async (qrCodeValue, latitude, longitude) => { // ENHANCED: Integrate with background location service for proper tracking lifecycle try { - console.log('🔄 DASHBOARD: Importing background location service...') - const { backgroundLocationService } = await import('@/services/backgroundLocationService.js') - console.log('🔄 DASHBOARD: Setting clock status in location service:', newClockStatus) // Set the clock-in status in the background location service - await backgroundLocationService.setClockedInStatus(newClockStatus) + await nativeServicesManager.services.location.setClockedInStatus(newClockStatus) if (newClockStatus) { console.log('✅ DASHBOARD: Clock-in successful - background location tracking started') @@ -191,13 +186,11 @@ onMounted(async () => { // ENHANCED: Handle app resume with background location service try { - const { backgroundLocationService } = await import('@/services/backgroundLocationService.js') - // Trigger app resume handling in background location service - await backgroundLocationService.onAppResume() + await nativeServicesManager.services.location.onAppResume() // Check if worker was previously clocked in and restore status - const wasClocked = await backgroundLocationService.restoreClockStatus() + const wasClocked = await nativeServicesManager.services.location.restoreClockStatus() if (wasClocked) { console.log('PRODUCTION: Restored previous clock-in status from app resume') // Note: Don't set isClockedIn.value here as it will be set by fetchCurrentStatus()