Files
votianlt/app/lib/services/database_service.dart
Sven Carstensen 704d1e7378 feat: Adressbuch mit Kundennummer, Update-Flow und interne Einträge
- Menüpunkt "Kunden" in "Adressbuch" umbenannt und App-Label
  "Verfügbare Jobs" zu "Auftragsliste" geändert (alle 10 Sprachen)
- Fortlaufende Kundennummer (usrId) ab 10000 über neuen
  SequenceGeneratorService und Counter-Dokument in misc-Collection
- Abholung/Lieferstation-Dialog: Änderungen an verknüpften
  Stammdaten aktualisieren den bestehenden Adressbuch-Eintrag
  statt einen neuen zu erzeugen; Checkbox-Label wechselt zu
  "Adresse im Adressbuch aktualisieren"
- Geänderte Adressen ohne Checkbox werden als interner Customer
  (internal=true) gesichert und im Adressbuch ausgeblendet
- E-Mail in AddCustomer und in Stations-Dialogen kein Pflichtfeld
  mehr; "(Login)" aus profile.email entfernt
- Manuelles Beenden eines Auftrags öffnet neue Seite
  JobManualCompleteView statt eines Dialogs
2026-04-20 12:42:56 +02:00

1700 lines
51 KiB
Dart

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<void>? _initializingCompleter;
bool get isInitialized => _store != null;
/// Initialize ObjectBox database
Future<void> initialize() async {
if (_store != null) {
return;
}
if (_initializingCompleter != null) {
return _initializingCompleter!.future;
}
final completer = Completer<void>();
_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<void> ensureInitialized() async {
if (isInitialized) {
return;
}
await initialize();
}
/// Log database statistics
Future<void> _logDatabaseStats() async {
try {
if (_store == null) return;
final jobCount = _store!.box<JobEntity>().count();
final taskStatusCount = _store!.box<TaskStatusEntity>().count();
final userDataCount = _store!.box<UserDataEntity>().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<void> saveJobs(List<Job> 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<JobEntity>();
final taskStatusBox = _store!.box<TaskStatusEntity>();
// 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<void> 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<JobEntity>();
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<List<Job>> 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<JobEntity>();
final jobEntities = jobBox.getAll();
final List<Job> jobs = [];
for (final entity in jobEntities) {
try {
final jobData = jsonDecode(entity.jobData);
final job = Job.fromJson(Map<String, dynamic>.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<ChatMessageEntity>();
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<String, int> 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<void> 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<TaskStatusEntity>();
// 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<bool> loadTaskStatus(String taskId) async {
try {
if (_store == null) {
developer.log('Database not initialized', name: 'DatabaseService');
return false;
}
final taskStatusBox = _store!.box<TaskStatusEntity>();
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<Map<String, bool>> loadAllTaskStatuses() async {
try {
if (_store == null) {
developer.log('Database not initialized', name: 'DatabaseService');
return {};
}
final taskStatusBox = _store!.box<TaskStatusEntity>();
final entities = taskStatusBox.getAll();
final Map<String, bool> 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<void> 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<String?> 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<void> 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<bool> 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<Map<String, bool>> loadSeenJobsForIds(Iterable<String> jobIds) async {
final Map<String, bool> map = {};
try {
if (_store == null) {
developer.log('Database not initialized', name: 'DatabaseService');
return map;
}
if (jobIds.isEmpty) return map;
final userDataBox = _store!.box<UserDataEntity>();
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<void> clearAllData() async {
try {
if (_store == null) {
developer.log('Database not initialized', name: 'DatabaseService');
return;
}
developer.log('Clearing all database data...', name: 'DatabaseService');
_store!.box<JobEntity>().removeAll();
_store!.box<TaskStatusEntity>().removeAll();
_store!.box<UserDataEntity>().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<void> 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<JobEntity>();
final taskStatusBox = _store!.box<TaskStatusEntity>();
final photoBox = _store!.box<PhotoEntity>();
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<void> 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<JobEntity>();
final taskStatusBox = _store!.box<TaskStatusEntity>();
// 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<void> 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<JobEntity>();
final userDataBox = _store!.box<UserDataEntity>();
final taskStatusBox = _store!.box<TaskStatusEntity>();
final photoBox = _store!.box<PhotoEntity>();
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 = <String>[
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<void> saveTaskPhotos(String taskId, List<String> 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<List<String>> 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<void> savePhotosForTask(
String taskId,
List<String> base64Photos,
) async {
try {
if (_store == null) {
developer.log('Database not initialized', name: 'DatabaseService');
return;
}
final photoBox = _store!.box<PhotoEntity>();
// 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<void> 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');
}
}
/// Save signature note (Bemerkung) for a task into user_data table
Future<void> saveTaskSignatureNote(String taskId, String note) async {
try {
if (_store == null) {
developer.log('Database not initialized', name: 'DatabaseService');
return;
}
final key = 'task_signature_note:$taskId';
await saveKeyValue(key, note);
} catch (e, st) {
developer.log(
'Error saving task signature note: $e',
name: 'DatabaseService',
);
developer.log('Stack trace: $st', name: 'DatabaseService');
}
}
/// Load signature note (Bemerkung) for a task from user_data table
Future<String?> loadTaskSignatureNote(String taskId) async {
try {
if (_store == null) {
developer.log('Database not initialized', name: 'DatabaseService');
return null;
}
return await loadKeyValue('task_signature_note:$taskId');
} catch (e, st) {
developer.log(
'Error loading task signature note: $e',
name: 'DatabaseService',
);
developer.log('Stack trace: $st', name: 'DatabaseService');
return null;
}
}
/// Load signature SVG for a task from user_data table
Future<String?> 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<void> saveTaskBarcodes(String taskId, List<String> 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<List<String>> 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<void> 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<void> 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<UserDataEntity>();
// 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<String?> loadKeyValue(String key) async {
try {
if (_store == null) {
developer.log('Database not initialized', name: 'DatabaseService');
return null;
}
final userDataBox = _store!.box<UserDataEntity>();
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<void> deleteKeyValue(String key) async {
try {
if (_store == null) {
developer.log('Database not initialized', name: 'DatabaseService');
return;
}
final userDataBox = _store!.box<UserDataEntity>();
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<void> 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<void> deleteCredentials() async {
await deleteKeyValue('auth_email');
await deleteKeyValue('auth_password');
developer.log('Credentials deleted', name: 'DatabaseService');
}
// Chat messages persistence -------------------------------------------------
Future<void> 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<ChatMessageEntity>();
// 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<void> 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<ChatMessageEntity>();
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<void> removePendingDuplicates(
String conversationKey,
ChatMessage message,
) async {
try {
await ensureInitialized();
if (_store == null) {
developer.log('Database not initialized', name: 'DatabaseService');
return;
}
final chatBox = _store!.box<ChatMessageEntity>();
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<List<ChatMessage>> loadChatMessages({String? conversationKey}) async {
try {
await ensureInitialized();
if (_store == null) {
developer.log('Database not initialized', name: 'DatabaseService');
return [];
}
final chatBox = _store!.box<ChatMessageEntity>();
List<ChatMessageEntity> 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<Map<String, List<ChatMessage>>> loadAllChatMessagesGrouped() async {
try {
await ensureInitialized();
if (_store == null) {
developer.log('Database not initialized', name: 'DatabaseService');
return {};
}
final chatBox = _store!.box<ChatMessageEntity>();
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<String, List<ChatMessage>> grouped = {};
for (final entity in entities) {
final key = entity.conversationKey;
final message = _chatMessageFromEntity(entity);
final list = grouped.putIfAbsent(key, () => <ChatMessage>[]);
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<void> markConversationRead(String conversationKey) async {
try {
await ensureInitialized();
if (_store == null) {
developer.log('Database not initialized', name: 'DatabaseService');
return;
}
final chatBox = _store!.box<ChatMessageEntity>();
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<void> deleteChatMessage(String messageId) async {
try {
if (_store == null) {
developer.log('Database not initialized', name: 'DatabaseService');
return;
}
final chatBox = _store!.box<ChatMessageEntity>();
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<void> deleteChatMessagesForJob({
String? jobId,
String? jobNumber,
Iterable<String>? 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
? <String>[]
: 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<ChatMessageEntity>();
final entitiesToDelete = <ChatMessageEntity>[];
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 = <int>{};
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<int> getTotalUnreadMessageCount() async {
try {
if (_store == null) {
developer.log('Database not initialized', name: 'DatabaseService');
return 0;
}
final chatBox = _store!.box<ChatMessageEntity>();
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<void> queueMessage(QueuedMessage message) async {
try {
await ensureInitialized();
if (_store == null) {
developer.log('Database not initialized', name: 'DatabaseService');
return;
}
final box = _store!.box<QueuedMessageEntity>();
// 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<List<QueuedMessage>> getQueuedMessages() async {
try {
await ensureInitialized();
if (_store == null) {
developer.log('Database not initialized', name: 'DatabaseService');
return [];
}
final box = _store!.box<QueuedMessageEntity>();
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<void> removeQueuedMessage(String messageId) async {
try {
await ensureInitialized();
if (_store == null) {
developer.log('Database not initialized', name: 'DatabaseService');
return;
}
final box = _store!.box<QueuedMessageEntity>();
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<void> updateMessageRetryCount(String messageId, int retryCount) async {
try {
await ensureInitialized();
if (_store == null) {
developer.log('Database not initialized', name: 'DatabaseService');
return;
}
final box = _store!.box<QueuedMessageEntity>();
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<void> clearQueuedMessages() async {
try {
await ensureInitialized();
if (_store == null) {
developer.log('Database not initialized', name: 'DatabaseService');
return;
}
_store!.box<QueuedMessageEntity>().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<String?> 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<ChatMessageEntity>();
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<void> 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<String?> 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;
}
}