import 'dart:async'; import 'dart:convert'; import 'package:path/path.dart'; import 'package:path_provider/path_provider.dart'; import 'package:votianlt_app/services/developer.dart' as developer; import '../models/chat_message.dart'; import '../models/job.dart'; import '../models/queued_message.dart'; import '../objectbox.g.dart'; import '../entities/job_entity.dart'; import '../entities/task_status_entity.dart'; import '../entities/user_data_entity.dart'; import '../entities/photo_entity.dart'; import '../entities/queued_message_entity.dart'; import '../entities/chat_message_entity.dart'; class DatabaseService { static final DatabaseService _instance = DatabaseService._internal(); factory DatabaseService() => _instance; DatabaseService._internal(); Store? _store; Completer? _initializingCompleter; bool get isInitialized => _store != null; /// Initialize ObjectBox database Future initialize() async { if (_store != null) { return; } if (_initializingCompleter != null) { return _initializingCompleter!.future; } final completer = Completer(); _initializingCompleter = completer; try { developer.log( 'Initializing ObjectBox database...', name: 'DatabaseService', ); // Get database path final docsDir = await getApplicationDocumentsDirectory(); final path = join(docsDir.path, 'objectbox'); // Open ObjectBox store _store = await openStore(directory: path); developer.log( 'ObjectBox database initialized successfully', name: 'DatabaseService', ); await _logDatabaseStats(); completer.complete(); } catch (e, stackTrace) { developer.log( 'Error initializing ObjectBox database: $e', name: 'DatabaseService', ); developer.log('Stack trace: $stackTrace', name: 'DatabaseService'); if (!completer.isCompleted) { completer.completeError(e); } rethrow; } finally { _initializingCompleter = null; } } Future ensureInitialized() async { if (isInitialized) { return; } await initialize(); } /// Log database statistics Future _logDatabaseStats() async { try { if (_store == null) return; final jobCount = _store!.box().count(); final taskStatusCount = _store!.box().count(); final userDataCount = _store!.box().count(); developer.log( 'Database stats - Jobs: $jobCount, Task statuses: $taskStatusCount, User data: $userDataCount', name: 'DatabaseService', ); } catch (e) { developer.log( 'Error getting database stats: $e', name: 'DatabaseService', ); } } /// Save jobs to database Future saveJobs(List jobs) async { try { if (_store == null) { developer.log('Database not initialized', name: 'DatabaseService'); return; } developer.log( 'Saving ${jobs.length} jobs to database...', name: 'DatabaseService', ); final now = DateTime.now(); final jobBox = _store!.box(); final taskStatusBox = _store!.box(); // Clear existing jobs and related task statuses before inserting new ones jobBox.removeAll(); taskStatusBox.removeAll(); // Save new jobs for (final job in jobs) { final normalized = job.normalized(); final jobEntity = JobEntity( jobId: normalized.id, jobData: jsonEncode(normalized.toJson()), createdAt: now, updatedAt: now, ); jobBox.put(jobEntity); // Also persist task completion states from JSON (adopt status on load) // Only set completed=true entries to avoid overwriting local progress with false for (final task in normalized.tasks) { if (task.completed) { final taskStatusEntity = TaskStatusEntity( taskId: task.id, completed: true, completedAt: now, createdAt: now, updatedAt: now, ); taskStatusBox.put(taskStatusEntity); } } } developer.log( 'Jobs and task statuses saved successfully', name: 'DatabaseService', ); } catch (e, stackTrace) { developer.log('Error saving jobs: $e', name: 'DatabaseService'); developer.log('Stack trace: $stackTrace', name: 'DatabaseService'); } } /// Delete a single job by ID Future deleteJob(String jobId) async { try { if (_store == null) { developer.log('Database not initialized', name: 'DatabaseService'); return; } developer.log( 'Deleting job $jobId from database...', name: 'DatabaseService', ); final jobBox = _store!.box(); final query = jobBox.query(JobEntity_.jobId.equals(jobId)).build(); final entities = query.find(); query.close(); if (entities.isNotEmpty) { jobBox.remove(entities.first.id); developer.log( 'Job $jobId deleted successfully', name: 'DatabaseService', ); } else { developer.log( 'Job $jobId not found in database', name: 'DatabaseService', ); } } catch (e, stackTrace) { developer.log('Error deleting job: $e', name: 'DatabaseService'); developer.log('Stack trace: $stackTrace', name: 'DatabaseService'); } } /// Load jobs from database Future> loadJobs() async { try { if (_store == null) { developer.log('Database not initialized', name: 'DatabaseService'); return []; } developer.log('Loading jobs from database...', name: 'DatabaseService'); final jobBox = _store!.box(); final jobEntities = jobBox.getAll(); final List jobs = []; for (final entity in jobEntities) { try { final jobData = jsonDecode(entity.jobData); final job = Job.fromJson(Map.from(jobData)); jobs.add(job); } catch (e) { developer.log('Error parsing job data: $e', name: 'DatabaseService'); } } // Sort by created_at DESC jobs.sort((a, b) => b.createdAt.compareTo(a.createdAt)); developer.log( 'Loaded ${jobs.length} jobs from database', name: 'DatabaseService', ); // Log message types for job-related messages in database if (jobs.isNotEmpty) { try { final chatBox = _store!.box(); final query = chatBox .query( (ChatMessageEntity_.jobId.notNull() | ChatMessageEntity_.jobNumber.notNull()), ) .build(); final messagesWithJobs = query.find(); query.close(); developer.log( 'Found ${messagesWithJobs.length} messages related to jobs in database', name: 'DatabaseService', ); // Group by message type and log final Map messageTypeCount = {}; for (final msg in messagesWithJobs) { final messageType = msg.messageType; final jobId = msg.jobId; final jobNumber = msg.jobNumber; messageTypeCount[messageType] = (messageTypeCount[messageType] ?? 0) + 1; developer.log( 'Job-related message: messageType=$messageType, jobId=$jobId, jobNumber=$jobNumber', name: 'DatabaseService', ); } // Summary log if (messageTypeCount.isNotEmpty) { developer.log( 'Message type summary for jobs in database: $messageTypeCount', name: 'DatabaseService', ); } } catch (e) { developer.log( 'Error logging job message types: $e', name: 'DatabaseService', ); } } return jobs; } catch (e, stackTrace) { developer.log('Error loading jobs: $e', name: 'DatabaseService'); developer.log('Stack trace: $stackTrace', name: 'DatabaseService'); return []; } } /// Save task completion status Future saveTaskStatus(String taskId, bool completed) async { try { if (_store == null) { developer.log('Database not initialized', name: 'DatabaseService'); return; } final now = DateTime.now(); final taskStatusBox = _store!.box(); // Find existing entity by taskId final query = taskStatusBox.query(TaskStatusEntity_.taskId.equals(taskId)).build(); final existing = query.findFirst(); query.close(); if (existing != null) { existing.completed = completed; existing.completedAt = completed ? now : null; existing.updatedAt = now; taskStatusBox.put(existing); } else { final entity = TaskStatusEntity( taskId: taskId, completed: completed, completedAt: completed ? now : null, createdAt: now, updatedAt: now, ); taskStatusBox.put(entity); } developer.log( 'Task status saved: $taskId = $completed', name: 'DatabaseService', ); } catch (e, stackTrace) { developer.log('Error saving task status: $e', name: 'DatabaseService'); developer.log('Stack trace: $stackTrace', name: 'DatabaseService'); } } /// Load task completion status Future loadTaskStatus(String taskId) async { try { if (_store == null) { developer.log('Database not initialized', name: 'DatabaseService'); return false; } final taskStatusBox = _store!.box(); final query = taskStatusBox.query(TaskStatusEntity_.taskId.equals(taskId)).build(); final entity = query.findFirst(); query.close(); if (entity != null) { return entity.completed; } return false; } catch (e, stackTrace) { developer.log('Error loading task status: $e', name: 'DatabaseService'); developer.log('Stack trace: $stackTrace', name: 'DatabaseService'); return false; } } /// Load all task completion statuses Future> loadAllTaskStatuses() async { try { if (_store == null) { developer.log('Database not initialized', name: 'DatabaseService'); return {}; } final taskStatusBox = _store!.box(); final entities = taskStatusBox.getAll(); final Map statuses = {}; for (final entity in entities) { statuses[entity.taskId] = entity.completed; } developer.log( 'Loaded ${statuses.length} task statuses from database', name: 'DatabaseService', ); return statuses; } catch (e, stackTrace) { developer.log('Error loading task statuses: $e', name: 'DatabaseService'); developer.log('Stack trace: $stackTrace', name: 'DatabaseService'); return {}; } } /// Save user ID Future saveUserId(String userId) async { try { if (_store == null) { developer.log('Database not initialized', name: 'DatabaseService'); return; } await saveKeyValue('userId', userId); developer.log('User ID saved: $userId', name: 'DatabaseService'); } catch (e, stackTrace) { developer.log('Error saving user ID: $e', name: 'DatabaseService'); developer.log('Stack trace: $stackTrace', name: 'DatabaseService'); } } /// Load user ID Future loadUserId() async { try { if (_store == null) { developer.log('Database not initialized', name: 'DatabaseService'); return null; } final userId = await loadKeyValue('userId'); if (userId != null) { developer.log('User ID loaded: $userId', name: 'DatabaseService'); return userId; } developer.log('No user ID found in database', name: 'DatabaseService'); return null; } catch (e, stackTrace) { developer.log('Error loading user ID: $e', name: 'DatabaseService'); developer.log('Stack trace: $stackTrace', name: 'DatabaseService'); return null; } } /// Mark a job as seen (persistently) Future setJobSeen(String jobId) async { try { if (_store == null) { developer.log('Database not initialized', name: 'DatabaseService'); return; } await saveKeyValue('job_seen:$jobId', '1'); } catch (e, stackTrace) { developer.log('Error setting job seen: $e', name: 'DatabaseService'); developer.log('Stack trace: $stackTrace', name: 'DatabaseService'); } } /// Check if a job has been seen Future isJobSeen(String jobId) async { try { if (_store == null) { developer.log('Database not initialized', name: 'DatabaseService'); return false; } final value = await loadKeyValue('job_seen:$jobId'); return value != null; } catch (e, stackTrace) { developer.log('Error checking job seen: $e', name: 'DatabaseService'); developer.log('Stack trace: $stackTrace', name: 'DatabaseService'); return false; } } /// Load seen flags for a set of job IDs (batch) Future> loadSeenJobsForIds(Iterable jobIds) async { final Map map = {}; try { if (_store == null) { developer.log('Database not initialized', name: 'DatabaseService'); return map; } if (jobIds.isEmpty) return map; final userDataBox = _store!.box(); final keys = jobIds.map((id) => 'job_seen:$id').toList(); for (final key in keys) { final query = userDataBox.query(UserDataEntity_.key.equals(key)).build(); final entity = query.findFirst(); query.close(); final jobId = key.replaceFirst('job_seen:', ''); map[jobId] = entity != null; } } catch (e, stackTrace) { developer.log('Error loading seen jobs: $e', name: 'DatabaseService'); developer.log('Stack trace: $stackTrace', name: 'DatabaseService'); } return map; } /// Clear all data (for logout) Future clearAllData() async { try { if (_store == null) { developer.log('Database not initialized', name: 'DatabaseService'); return; } developer.log('Clearing all database data...', name: 'DatabaseService'); _store!.box().removeAll(); _store!.box().removeAll(); _store!.box().removeAll(); developer.log('All database data cleared', name: 'DatabaseService'); } catch (e, stackTrace) { developer.log( 'Error clearing database data: $e', name: 'DatabaseService', ); developer.log('Stack trace: $stackTrace', name: 'DatabaseService'); } } /// Clear all jobs and related data (task statuses, photos). /// Preserves user credentials, chat messages, and other user data. /// Called after reconnection before notifying server. Future clearAllJobsAndRelatedData() async { try { if (_store == null) { developer.log('Database not initialized', name: 'DatabaseService'); return; } developer.log( 'Clearing all jobs and related data...', name: 'DatabaseService', ); final jobBox = _store!.box(); final taskStatusBox = _store!.box(); final photoBox = _store!.box(); final jobCount = jobBox.count(); final taskStatusCount = taskStatusBox.count(); final photoCount = photoBox.count(); jobBox.removeAll(); taskStatusBox.removeAll(); photoBox.removeAll(); // Note: Chat messages are intentionally NOT deleted here // to preserve chat history across reconnections. developer.log( 'Cleared $jobCount jobs, $taskStatusCount task statuses, $photoCount photos (chat messages preserved)', name: 'DatabaseService', ); } catch (e, stackTrace) { developer.log( 'Error clearing jobs and related data: $e', name: 'DatabaseService', ); developer.log('Stack trace: $stackTrace', name: 'DatabaseService'); } } /// Upsert a single job and update its related task statuses Future saveOrUpdateJob(Job job) async { try { if (_store == null) { developer.log('Database not initialized', name: 'DatabaseService'); return; } final now = DateTime.now(); final normalized = job.normalized(); final jobBox = _store!.box(); final taskStatusBox = _store!.box(); // Find existing job entity by jobId final jobQuery = jobBox.query(JobEntity_.jobId.equals(normalized.id)).build(); final existingJob = jobQuery.findFirst(); jobQuery.close(); if (existingJob != null) { existingJob.jobData = jsonEncode(normalized.toJson()); existingJob.updatedAt = now; jobBox.put(existingJob); } else { final jobEntity = JobEntity( jobId: normalized.id, jobData: jsonEncode(normalized.toJson()), createdAt: now, updatedAt: now, ); jobBox.put(jobEntity); } // Update task_status for this job only: // 1) Remove any existing statuses for the tasks of this job (to avoid stale entries) final taskIds = normalized.tasks.map((t) => t.id).toList(); if (taskIds.isNotEmpty) { for (final taskId in taskIds) { final query = taskStatusBox .query(TaskStatusEntity_.taskId.equals(taskId)) .build(); final entities = query.find(); query.close(); for (final entity in entities) { taskStatusBox.remove(entity.id); } } } // 2) Insert completed=true entries for tasks coming as completed from JSON for (final t in normalized.tasks) { if (t.completed) { final taskStatusEntity = TaskStatusEntity( taskId: t.id, completed: true, completedAt: now, createdAt: now, updatedAt: now, ); taskStatusBox.put(taskStatusEntity); } } developer.log( 'Upserted job ${normalized.id} and updated ${taskIds.length} task statuses', name: 'DatabaseService', ); } catch (e, st) { developer.log('Error in saveOrUpdateJob: $e', name: 'DatabaseService'); developer.log('Stack trace: $st', name: 'DatabaseService'); } } Future deleteJobAndRelatedData(Job job) async { try { await ensureInitialized(); if (_store == null) { developer.log('Database not initialized', name: 'DatabaseService'); return; } final trimmedJobId = job.id.trim(); final jobBox = _store!.box(); final userDataBox = _store!.box(); final taskStatusBox = _store!.box(); final photoBox = _store!.box(); if (trimmedJobId.isNotEmpty) { // Delete job final jobQuery = jobBox.query(JobEntity_.jobId.equals(trimmedJobId)).build(); final jobEntities = jobQuery.find(); jobQuery.close(); for (final entity in jobEntities) { jobBox.remove(entity.id); } // Delete job_seen flag final seenQuery = userDataBox .query(UserDataEntity_.key.equals('job_seen:$trimmedJobId')) .build(); final seenEntities = seenQuery.find(); seenQuery.close(); for (final entity in seenEntities) { userDataBox.remove(entity.id); } } final taskIds = job.tasks .map((task) => task.id.trim()) .where((id) => id.isNotEmpty) .toList(); if (taskIds.isNotEmpty) { for (final taskId in taskIds) { // Delete task status final taskQuery = taskStatusBox .query(TaskStatusEntity_.taskId.equals(taskId)) .build(); final taskEntities = taskQuery.find(); taskQuery.close(); for (final entity in taskEntities) { taskStatusBox.remove(entity.id); } // Delete photos final photoQuery = photoBox.query(PhotoEntity_.taskId.equals(taskId)).build(); final photoEntities = photoQuery.find(); photoQuery.close(); for (final entity in photoEntities) { photoBox.remove(entity.id); } // Delete user data related to task (photos, signatures, barcodes) final allUserData = userDataBox.getAll(); for (final entity in allUserData) { if (entity.key.contains(':$taskId')) { userDataBox.remove(entity.id); } } } } final trimmedJobNumber = job.jobNumber.trim().isEmpty ? null : job.jobNumber.trim(); final conversationKeys = [ if (trimmedJobId.isNotEmpty) 'job:${trimmedJobId.toLowerCase()}', if (trimmedJobNumber != null) 'job_number:${trimmedJobNumber.toLowerCase()}', ]; await deleteChatMessagesForJob( jobId: trimmedJobId.isNotEmpty ? trimmedJobId : null, jobNumber: trimmedJobNumber, conversationKeys: conversationKeys, ); developer.log( 'Deleted job $trimmedJobId and related local data', name: 'DatabaseService', ); } catch (e, st) { developer.log( 'Error deleting job ${job.id}: $e', name: 'DatabaseService', ); developer.log('Stack trace: $st', name: 'DatabaseService'); } } /// Save Base64-encoded photos for a task into user_data table (legacy list storage) Future saveTaskPhotos(String taskId, List base64Photos) async { try { if (_store == null) { developer.log('Database not initialized', name: 'DatabaseService'); return; } final key = 'task_photos:$taskId'; final value = jsonEncode(base64Photos); await saveKeyValue(key, value); developer.log( 'Saved ${base64Photos.length} photos for task $taskId', name: 'DatabaseService', ); } catch (e, st) { developer.log('Error saving task photos: $e', name: 'DatabaseService'); developer.log('Stack trace: $st', name: 'DatabaseService'); } } /// Load Base64-encoded photos for a task from user_data table Future> loadTaskPhotos(String taskId) async { try { if (_store == null) { developer.log('Database not initialized', name: 'DatabaseService'); return []; } final key = 'task_photos:$taskId'; final raw = await loadKeyValue(key); if (raw == null) return []; final decoded = jsonDecode(raw); if (decoded is List) { return decoded.map((e) => e.toString()).toList(); } return []; } catch (e, st) { developer.log('Error loading task photos: $e', name: 'DatabaseService'); developer.log('Stack trace: $st', name: 'DatabaseService'); return []; } } /// Save photos into the dedicated photos collection (one row per photo) Future savePhotosForTask( String taskId, List base64Photos, ) async { try { if (_store == null) { developer.log('Database not initialized', name: 'DatabaseService'); return; } final photoBox = _store!.box(); // Clear existing photos for this task first final query = photoBox.query(PhotoEntity_.taskId.equals(taskId)).build(); final existingPhotos = query.find(); query.close(); for (final photo in existingPhotos) { photoBox.remove(photo.id); } final now = DateTime.now(); for (int i = 0; i < base64Photos.length; i++) { final data = base64Photos[i]; final photoEntity = PhotoEntity( taskId: taskId, photoIndex: i, data: data, createdAt: now, ); photoBox.put(photoEntity); } developer.log( 'Saved ${base64Photos.length} photos into collection for task $taskId', name: 'DatabaseService', ); } catch (e, st) { developer.log( 'Error saving photos for task to collection: $e', name: 'DatabaseService', ); developer.log('Stack trace: $st', name: 'DatabaseService'); } } /// Save signature SVG for a task into user_data table Future saveTaskSignature(String taskId, String svg) async { try { if (_store == null) { developer.log('Database not initialized', name: 'DatabaseService'); return; } final key = 'task_signature_svg:$taskId'; await saveKeyValue(key, svg); developer.log( 'Saved signature SVG for task $taskId', name: 'DatabaseService', ); } catch (e, st) { developer.log( 'Error saving task signature SVG: $e', name: 'DatabaseService', ); developer.log('Stack trace: $st', name: 'DatabaseService'); } } /// Load signature SVG for a task from user_data table Future loadTaskSignature(String taskId) async { try { if (_store == null) { developer.log('Database not initialized', name: 'DatabaseService'); return null; } // Try new SVG key first; fallback to legacy PNG key if present final svgKey = 'task_signature_svg:$taskId'; final legacyKey = 'task_signature:$taskId'; String? result = await loadKeyValue(svgKey); result ??= await loadKeyValue(legacyKey); return result; } catch (e, st) { developer.log( 'Error loading task signature (SVG): $e', name: 'DatabaseService', ); developer.log('Stack trace: $st', name: 'DatabaseService'); return null; } } /// Save barcodes for a task into user_data table Future saveTaskBarcodes(String taskId, List barcodes) async { try { if (_store == null) { developer.log('Database not initialized', name: 'DatabaseService'); return; } final key = 'task_barcodes:$taskId'; final value = jsonEncode(barcodes); await saveKeyValue(key, value); developer.log( 'Saved ${barcodes.length} barcodes for task $taskId', name: 'DatabaseService', ); } catch (e, st) { developer.log('Error saving task barcodes: $e', name: 'DatabaseService'); developer.log('Stack trace: $st', name: 'DatabaseService'); } } /// Load barcodes for a task from user_data table Future> loadTaskBarcodes(String taskId) async { try { if (_store == null) { developer.log('Database not initialized', name: 'DatabaseService'); return []; } final key = 'task_barcodes:$taskId'; final raw = await loadKeyValue(key); if (raw == null) return []; final decoded = jsonDecode(raw); if (decoded is List) { return decoded.map((e) => e.toString()).toList(); } return []; } catch (e, st) { developer.log('Error loading task barcodes: $e', name: 'DatabaseService'); developer.log('Stack trace: $st', name: 'DatabaseService'); return []; } } /// Close database connections Future close() async { try { _store?.close(); _store = null; developer.log('Database connection closed', name: 'DatabaseService'); } catch (e, stackTrace) { developer.log('Error closing database: $e', name: 'DatabaseService'); developer.log('Stack trace: $stackTrace', name: 'DatabaseService'); } } /// Generic helpers: save and load key-value pairs in user_data table Future saveKeyValue(String key, String value) async { try { if (_store == null) { developer.log('Database not initialized', name: 'DatabaseService'); return; } final now = DateTime.now(); final userDataBox = _store!.box(); // Find existing entity by key final query = userDataBox.query(UserDataEntity_.key.equals(key)).build(); final existing = query.findFirst(); query.close(); if (existing != null) { existing.value = value; existing.updatedAt = now; userDataBox.put(existing); } else { final entity = UserDataEntity( key: key, value: value, createdAt: now, updatedAt: now, ); userDataBox.put(entity); } developer.log('Saved key "$key"', name: 'DatabaseService'); } catch (e, st) { developer.log('Error saving key "$key": $e', name: 'DatabaseService'); developer.log('Stack trace: $st', name: 'DatabaseService'); } } Future loadKeyValue(String key) async { try { if (_store == null) { developer.log('Database not initialized', name: 'DatabaseService'); return null; } final userDataBox = _store!.box(); final query = userDataBox.query(UserDataEntity_.key.equals(key)).build(); final entity = query.findFirst(); query.close(); return entity?.value; } catch (e, st) { developer.log('Error loading key "$key": $e', name: 'DatabaseService'); developer.log('Stack trace: $st', name: 'DatabaseService'); return null; } } Future deleteKeyValue(String key) async { try { if (_store == null) { developer.log('Database not initialized', name: 'DatabaseService'); return; } final userDataBox = _store!.box(); final query = userDataBox.query(UserDataEntity_.key.equals(key)).build(); final entity = query.findFirst(); query.close(); if (entity != null) { userDataBox.remove(entity.id); developer.log('Deleted key "$key"', name: 'DatabaseService'); } } catch (e, st) { developer.log('Error deleting key "$key": $e', name: 'DatabaseService'); developer.log('Stack trace: $st', name: 'DatabaseService'); } } // Credentials persistence ---------------------------------------------------- /// Save login credentials for auto-login on app restart Future saveCredentials(String email, String password) async { final normalizedEmail = email.trim(); if (normalizedEmail.isEmpty || password.isEmpty) { developer.log( 'Skipping credential save because email or password is empty', name: 'DatabaseService', ); return; } await saveKeyValue('auth_email', normalizedEmail); await saveKeyValue('auth_password', password); developer.log( 'Credentials saved for $normalizedEmail', name: 'DatabaseService', ); } /// Load saved login credentials /// Returns null if no credentials are stored Future<({String email, String password})?> loadCredentials() async { final email = await loadKeyValue('auth_email'); final password = await loadKeyValue('auth_password'); final normalizedEmail = email?.trim(); if (normalizedEmail != null && normalizedEmail.isNotEmpty && password != null && password.isNotEmpty) { developer.log( 'Credentials loaded for $normalizedEmail', name: 'DatabaseService', ); return (email: normalizedEmail, password: password); } if ((email != null && email.isNotEmpty) || (password != null && password.isNotEmpty)) { developer.log( 'Stored credentials are incomplete or empty - removing them', name: 'DatabaseService', ); await deleteCredentials(); } developer.log('No valid credentials found', name: 'DatabaseService'); return null; } /// Delete saved login credentials (on logout) Future deleteCredentials() async { await deleteKeyValue('auth_email'); await deleteKeyValue('auth_password'); developer.log('Credentials deleted', name: 'DatabaseService'); } // Chat messages persistence ------------------------------------------------- Future upsertChatMessage( ChatMessage message, String conversationKey, ) async { try { await ensureInitialized(); if (_store == null) { developer.log('Database not initialized', name: 'DatabaseService'); return; } final directionString = chatDirectionToString(message.direction); developer.log( '[DEBUG_LOG] Upserting message: id=${message.id}, direction=${message.direction} (stored as: $directionString), read=${message.read}', name: 'DatabaseService', ); final chatBox = _store!.box(); // Find existing entity by messageId final query = chatBox .query(ChatMessageEntity_.messageId.equals(message.id)) .build(); final existing = query.findFirst(); query.close(); if (existing != null) { existing.conversationKey = conversationKey; existing.content = message.content; existing.contentType = chatContentTypeToString(message.contentType); existing.createdAt = message.createdAt; existing.origin = directionString; existing.messageType = chatMessageTypeToString(message.messageType); existing.jobId = message.jobId; existing.jobNumber = message.jobNumber; existing.read = message.read; existing.pendingSync = message.pendingSync; chatBox.put(existing); } else { final entity = ChatMessageEntity( messageId: message.id, conversationKey: conversationKey, content: message.content, contentType: chatContentTypeToString(message.contentType), createdAt: message.createdAt, origin: directionString, messageType: chatMessageTypeToString(message.messageType), jobId: message.jobId, jobNumber: message.jobNumber, read: message.read, pendingSync: message.pendingSync, ); chatBox.put(entity); } } catch (e, st) { developer.log( 'Error saving chat message ${message.id}: $e', name: 'DatabaseService', ); developer.log('Stack trace: $st', name: 'DatabaseService'); } } Future migrateConversationKey(String fromKey, String toKey) async { if (fromKey == toKey) { return; } try { await ensureInitialized(); if (_store == null) { developer.log('Database not initialized', name: 'DatabaseService'); return; } final chatBox = _store!.box(); final query = chatBox .query(ChatMessageEntity_.conversationKey.equals(fromKey)) .build(); final entities = query.find(); query.close(); for (final entity in entities) { entity.conversationKey = toKey; chatBox.put(entity); } } catch (e, st) { developer.log( 'Error migrating conversation key from "$fromKey" to "$toKey": $e', name: 'DatabaseService', ); developer.log('Stack trace: $st', name: 'DatabaseService'); } } Future removePendingDuplicates( String conversationKey, ChatMessage message, ) async { try { await ensureInitialized(); if (_store == null) { developer.log('Database not initialized', name: 'DatabaseService'); return; } final chatBox = _store!.box(); final query = chatBox .query( ChatMessageEntity_.conversationKey.equals(conversationKey) & ChatMessageEntity_.pendingSync.equals(true) & ChatMessageEntity_.content.equals(message.content) & ChatMessageEntity_.contentType.equals( chatContentTypeToString(message.contentType), ) & ChatMessageEntity_.messageId.notEquals(message.id), ) .build(); final entities = query.find(); query.close(); for (final entity in entities) { chatBox.remove(entity.id); } } catch (e, st) { developer.log( 'Error removing pending duplicates: $e', name: 'DatabaseService', ); developer.log('Stack trace: $st', name: 'DatabaseService'); } } Future> loadChatMessages({String? conversationKey}) async { try { await ensureInitialized(); if (_store == null) { developer.log('Database not initialized', name: 'DatabaseService'); return []; } final chatBox = _store!.box(); List entities; if (conversationKey != null) { final query = chatBox .query( ChatMessageEntity_.conversationKey.equals(conversationKey), ) .order(ChatMessageEntity_.createdAt) .build(); entities = query.find(); query.close(); } else { entities = chatBox.getAll(); entities.sort((a, b) => a.createdAt.compareTo(b.createdAt)); } return entities.map(_chatMessageFromEntity).toList(); } catch (e, st) { developer.log('Error loading chat messages: $e', name: 'DatabaseService'); developer.log('Stack trace: $st', name: 'DatabaseService'); return []; } } Future>> loadAllChatMessagesGrouped() async { try { await ensureInitialized(); if (_store == null) { developer.log('Database not initialized', name: 'DatabaseService'); return {}; } final chatBox = _store!.box(); final entities = chatBox.getAll(); // Sort by conversation_key and created_at entities.sort((a, b) { final keyCompare = a.conversationKey.compareTo(b.conversationKey); if (keyCompare != 0) return keyCompare; return a.createdAt.compareTo(b.createdAt); }); final Map> grouped = {}; for (final entity in entities) { final key = entity.conversationKey; final message = _chatMessageFromEntity(entity); final list = grouped.putIfAbsent(key, () => []); list.add(message); } return grouped; } catch (e, st) { developer.log( 'Error loading grouped chat messages: $e', name: 'DatabaseService', ); developer.log('Stack trace: $st', name: 'DatabaseService'); return {}; } } Future markConversationRead(String conversationKey) async { try { await ensureInitialized(); if (_store == null) { developer.log('Database not initialized', name: 'DatabaseService'); return; } final chatBox = _store!.box(); final query = chatBox .query(ChatMessageEntity_.conversationKey.equals(conversationKey)) .build(); final entities = query.find(); query.close(); for (final entity in entities) { entity.read = true; chatBox.put(entity); } } catch (e, st) { developer.log( 'Error marking conversation read: $e', name: 'DatabaseService', ); developer.log('Stack trace: $st', name: 'DatabaseService'); } } Future deleteChatMessage(String messageId) async { try { if (_store == null) { developer.log('Database not initialized', name: 'DatabaseService'); return; } final chatBox = _store!.box(); final query = chatBox.query(ChatMessageEntity_.messageId.equals(messageId)).build(); final entities = query.find(); query.close(); for (final entity in entities) { chatBox.remove(entity.id); } } catch (e, st) { developer.log( 'Error deleting chat message "$messageId": $e', name: 'DatabaseService', ); developer.log('Stack trace: $st', name: 'DatabaseService'); } } Future deleteChatMessagesForJob({ String? jobId, String? jobNumber, Iterable? conversationKeys, }) async { try { await ensureInitialized(); if (_store == null) { developer.log('Database not initialized', name: 'DatabaseService'); return; } final trimmedJobId = jobId?.trim() ?? ''; final trimmedJobNumber = jobNumber?.trim() ?? ''; final keysList = conversationKeys == null ? [] : conversationKeys .map((key) => key.trim()) .where((key) => key.isNotEmpty) .toSet() .toList(); if (trimmedJobId.isEmpty && trimmedJobNumber.isEmpty && keysList.isEmpty) { developer.log( 'No chat messages matched deletion criteria for jobId=$jobId jobNumber=$jobNumber', name: 'DatabaseService', ); return; } final chatBox = _store!.box(); final entitiesToDelete = []; if (trimmedJobId.isNotEmpty) { final query = chatBox .query(ChatMessageEntity_.jobId.equals(trimmedJobId)) .build(); entitiesToDelete.addAll(query.find()); query.close(); } if (trimmedJobNumber.isNotEmpty) { final query = chatBox .query(ChatMessageEntity_.jobNumber.equals(trimmedJobNumber)) .build(); entitiesToDelete.addAll(query.find()); query.close(); } if (keysList.isNotEmpty) { for (final key in keysList) { final query = chatBox .query(ChatMessageEntity_.conversationKey.equals(key)) .build(); entitiesToDelete.addAll(query.find()); query.close(); } } // Remove duplicates by id final uniqueIds = {}; for (final entity in entitiesToDelete) { if (uniqueIds.add(entity.id)) { chatBox.remove(entity.id); } } developer.log( 'Deleted chat messages for jobId=$jobId jobNumber=$jobNumber (conversationKeys=${keysList.length})', name: 'DatabaseService', ); } catch (e, st) { developer.log( 'Error deleting chat messages for jobId=$jobId jobNumber=$jobNumber: $e', name: 'DatabaseService', ); developer.log('Stack trace: $st', name: 'DatabaseService'); } } Future getTotalUnreadMessageCount() async { try { if (_store == null) { developer.log('Database not initialized', name: 'DatabaseService'); return 0; } final chatBox = _store!.box(); final query = chatBox.query(ChatMessageEntity_.read.equals(false)).build(); final count = query.count(); query.close(); developer.log( '[DEBUG_LOG] Total unread message count: $count', name: 'DatabaseService', ); return count; } catch (e, st) { developer.log( 'Error getting total unread message count: $e', name: 'DatabaseService', ); developer.log('Stack trace: $st', name: 'DatabaseService'); return 0; } } ChatMessage _chatMessageFromEntity(ChatMessageEntity entity) { return ChatMessage( id: entity.messageId, content: entity.content, contentType: chatContentTypeFromString(entity.contentType), createdAt: entity.createdAt, direction: chatDirectionFromString(entity.origin), messageType: chatMessageTypeFromString(entity.messageType), jobId: entity.jobId, jobNumber: entity.jobNumber, read: entity.read, pendingSync: entity.pendingSync, ); } // Message Queue Management /// Save a failed message to the queue Future queueMessage(QueuedMessage message) async { try { await ensureInitialized(); if (_store == null) { developer.log('Database not initialized', name: 'DatabaseService'); return; } final box = _store!.box(); // Find existing entity by messageId final query = box.query(QueuedMessageEntity_.messageId.equals(message.id)).build(); final existing = query.findFirst(); query.close(); if (existing != null) { existing.topic = message.topic; existing.payload = jsonEncode(message.payload); existing.createdAt = message.createdAt; existing.retryCount = message.retryCount; box.put(existing); } else { final entity = QueuedMessageEntity( messageId: message.id, topic: message.topic, payload: jsonEncode(message.payload), createdAt: message.createdAt, retryCount: message.retryCount, ); box.put(entity); } developer.log( 'Queued message: ${message.id} for topic: ${message.topic}', name: 'DatabaseService', ); } catch (e, st) { developer.log('Error queuing message: $e', name: 'DatabaseService'); developer.log('Stack trace: $st', name: 'DatabaseService'); } } /// Get all queued messages Future> getQueuedMessages() async { try { await ensureInitialized(); if (_store == null) { developer.log('Database not initialized', name: 'DatabaseService'); return []; } final box = _store!.box(); final entities = box.getAll(); // Sort by created_at ASC entities.sort((a, b) => a.createdAt.compareTo(b.createdAt)); return entities.map((entity) { return QueuedMessage( id: entity.messageId, topic: entity.topic, payload: jsonDecode(entity.payload), createdAt: entity.createdAt, retryCount: entity.retryCount, ); }).toList(); } catch (e, st) { developer.log( 'Error getting queued messages: $e', name: 'DatabaseService', ); developer.log('Stack trace: $st', name: 'DatabaseService'); return []; } } /// Remove a successfully sent message from the queue Future removeQueuedMessage(String messageId) async { try { await ensureInitialized(); if (_store == null) { developer.log('Database not initialized', name: 'DatabaseService'); return; } final box = _store!.box(); final query = box.query(QueuedMessageEntity_.messageId.equals(messageId)).build(); final entities = query.find(); query.close(); for (final entity in entities) { box.remove(entity.id); } developer.log( 'Removed queued message: $messageId', name: 'DatabaseService', ); } catch (e, st) { developer.log( 'Error removing queued message: $e', name: 'DatabaseService', ); developer.log('Stack trace: $st', name: 'DatabaseService'); } } /// Update retry count for a message Future updateMessageRetryCount(String messageId, int retryCount) async { try { await ensureInitialized(); if (_store == null) { developer.log('Database not initialized', name: 'DatabaseService'); return; } final box = _store!.box(); final query = box.query(QueuedMessageEntity_.messageId.equals(messageId)).build(); final entity = query.findFirst(); query.close(); if (entity != null) { entity.retryCount = retryCount; box.put(entity); } developer.log( 'Updated retry count for message: $messageId to $retryCount', name: 'DatabaseService', ); } catch (e, st) { developer.log( 'Error updating message retry count: $e', name: 'DatabaseService', ); developer.log('Stack trace: $st', name: 'DatabaseService'); } } /// Clear all queued messages (for cleanup) Future clearQueuedMessages() async { try { await ensureInitialized(); if (_store == null) { developer.log('Database not initialized', name: 'DatabaseService'); return; } _store!.box().removeAll(); developer.log('Cleared all queued messages', name: 'DatabaseService'); } catch (e, st) { developer.log( 'Error clearing queued messages: $e', name: 'DatabaseService', ); developer.log('Stack trace: $st', name: 'DatabaseService'); } } Future updateChatMessagePendingSync( String messageId, bool pendingSync, ) async { try { await ensureInitialized(); if (_store == null) { developer.log('Database not initialized', name: 'DatabaseService'); return null; } final chatBox = _store!.box(); final query = chatBox.query(ChatMessageEntity_.messageId.equals(messageId)).build(); final entity = query.findFirst(); query.close(); if (entity == null) { return null; } entity.pendingSync = pendingSync; chatBox.put(entity); return entity.conversationKey; } catch (e, st) { developer.log( 'Error updating pendingSync for message $messageId: $e', name: 'DatabaseService', ); developer.log('Stack trace: $st', name: 'DatabaseService'); return null; } } // Language preference persistence ---------------------------------------------------- /// Save language preference Future saveLanguagePreference(String languageCode) async { await saveKeyValue('language_preference', languageCode); developer.log( 'Language preference saved: $languageCode', name: 'DatabaseService', ); } /// Load saved language preference /// Returns null if no preference is stored Future loadLanguagePreference() async { final languageCode = await loadKeyValue('language_preference'); if (languageCode != null) { developer.log( 'Language preference loaded: $languageCode', name: 'DatabaseService', ); return languageCode; } developer.log('No language preference found', name: 'DatabaseService'); return null; } }