import 'dart:io'; import 'package:path_provider/path_provider.dart'; import 'package:sqflite/sqflite.dart'; import 'package:path/path.dart' as p; import 'package:terepi_seged/enums/note_type.dart'; import 'package:terepi_seged/models/imported_layer_meta.dart'; import 'package:terepi_seged/models/measured_point.dart'; import 'package:terepi_seged/models/note_item.dart'; import 'package:terepi_seged/models/note_item_audio.dart'; import 'package:terepi_seged/models/note_item_photo.dart'; import 'package:terepi_seged/models/stakeout_point.dart'; import 'package:terepi_seged/models/track.dart'; import 'package:terepi_seged/services/device_identity_service.dart'; import 'package:uuid/uuid.dart'; import '../models/project.dart'; class AppDatabase { AppDatabase._(); static final instance = AppDatabase._(); static Database? _db; Future get database async { _db ??= await _open(); return _db!; } Future _open() async { final oldDb = p.join(await getDatabasesPath(), 'terepi_seged.db'); if (await File(oldDb).exists()) { await File(oldDb).delete(); } final directory = await getExternalStorageDirectory(); final dbDir = Directory(p.join(directory!.path, 'database')); if (!await dbDir.exists()) { await dbDir.create(recursive: true); } final path = p.join(dbDir.path, 'terepi_seged.db'); return openDatabase(path, version: 3, onConfigure: (db) => db.execute('PRAGMA foreign_keys = ON'), onCreate: _onCreate, onUpgrade: _onUpgrade); } Future _onCreate(Database db, int _) async { // ── Projects ──────────────────────────────────────────────────── await db.execute(''' CREATE TABLE IF NOT EXISTS projects ( id INTEGER PRIMARY KEY AUTOINCREMENT, uuid TEXT NOT NULL UNIQUE, name TEXT NOT NULL, client TEXT NOT NULL DEFAULT '', description TEXT NOT NULL DEFAULT '', crs TEXT NOT NULL DEFAULT 'eov', color TEXT NOT NULL DEFAULT '#185FA5', status TEXT NOT NULL DEFAULT 'active', is_default INTEGER NOT NULL DEFAULT 0, is_local_only INTEGER NOT NULL DEFAULT 1, last_synced_at TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL ) '''); // ── Bemért pontok ──────────────────────────────────────────────── await db.execute(''' CREATE TABLE IF NOT EXISTS measured_points ( id INTEGER PRIMARY KEY AUTOINCREMENT, project_id INTEGER NOT NULL REFERENCES projects(id) ON DELETE CASCADE, name TEXT NOT NULL, eov_y REAL, eov_x REAL, eov_z REAL, latitude REAL, longitude REAL, altitude REAL, accuracy REAL, fix_quality INTEGER, timestamp TEXT NOT NULL, note TEXT NOT NULL DEFAULT '' ) '''); await db .execute('CREATE INDEX idx_mp_project ON measured_points(project_id)'); // ── Track-ek ───────────────────────────────────────────────────── await db.execute(''' CREATE TABLE IF NOT EXISTS tracks ( id INTEGER PRIMARY KEY AUTOINCREMENT, project_id INTEGER REFERENCES projects(id) ON DELETE CASCADE, name TEXT NOT NULL, start_time TEXT NOT NULL, end_time TEXT, status TEXT NOT NULL DEFAULT 'recording', source TEXT NOT NULL DEFAULT 'Telefon GPS', distance_m REAL NOT NULL DEFAULT 0, point_count INTEGER NOT NULL DEFAULT 0, is_local_only INTEGER NOT NULL DEFAULT 0, sync_status TEXT NOT NULL DEFAULT 'pending', supabase_id TEXT ) '''); await db.execute('CREATE INDEX idx_tracks_project ON tracks(project_id)'); // ── Track pontok ───────────────────────────────────────────────── await db.execute(''' CREATE TABLE IF NOT EXISTS track_points ( id INTEGER PRIMARY KEY AUTOINCREMENT, track_id INTEGER NOT NULL REFERENCES tracks(id) ON DELETE CASCADE, latitude REAL NOT NULL, longitude REAL NOT NULL, altitude REAL, accuracy REAL, speed REAL, heading REAL, timestamp TEXT NOT NULL ) '''); await db.execute( 'CREATE INDEX idx_tp_track ON track_points(track_id, timestamp)'); // ── Terepbejárás elemek ────────────────────────────────────────── await db.execute(''' CREATE TABLE IF NOT EXISTS note_items ( id INTEGER PRIMARY KEY AUTOINCREMENT, project_id INTEGER REFERENCES projects(id) ON DELETE CASCADE, type TEXT NOT NULL, points_json TEXT NOT NULL, color TEXT NOT NULL DEFAULT '#185FA5', opacity REAL NOT NULL DEFAULT 0.5, stroke_width REAL NOT NULL DEFAULT 3.0, stroke_color TEXT NOT NULL DEFAULT '#FFD700', label TEXT NOT NULL DEFAULT '', created_at TEXT NOT NULL ) '''); await db .execute('CREATE INDEX idx_notes_project ON note_items(project_id)'); await db.execute(''' CREATE TABLE IF NOT EXISTS note_item_photos ( id INTEGER PRIMARY KEY AUTOINCREMENT, note_item_id INTEGER NOT NULL REFERENCES note_items(id) ON DELETE CASCADE, local_path TEXT NOT NULL, storage_path TEXT, caption TEXT NOT NULL DEFAULT '', latitude REAL, longitude REAL, created_at TEXT NOT NULL ) '''); await db.execute( 'CREATE INDEX idx_photos_note ON note_item_photos(note_item_id)'); await db.execute(''' CREATE TABLE IF NOT EXISTS note_item_audios ( id INTEGER PRIMARY KEY AUTOINCREMENT, note_item_id INTEGER NOT NULL REFERENCES note_items(id) ON DELETE CASCADE, local_path TEXT NOT NULL, caption TEXT NOT NULL DEFAULT '', duration_seconds INTEGER NOT NULL DEFAULT 0, latitude REAL, longitude REAL, created_at TEXT NOT NULL ) '''); await db.execute( 'CREATE INDEX idx_audios_note ON note_item_audios(note_item_id)'); await db.execute(''' CREATE TABLE IF NOT EXISTS pending_points ( id INTEGER PRIMARY KEY AUTOINCREMENT, point_number INTEGER NOT NULL, gnss_number TEXT, latitude REAL NOT NULL, longitude REAL NOT NULL, altitude REAL, height_of_geoid REAL, eov_x REAL, eov_y REAL, pole_height REAL, horizontal_error REAL, vertical_error REAL, description TEXT, is_deleted INTEGER NOT NULL DEFAULT 0, project_id INTEGER NOT NULL DEFAULT 2, created_at TEXT NOT NULL, sync_status TEXT NOT NULL DEFAULT 'pending' ) '''); await db.execute( 'CREATE INDEX IF NOT EXISTS idx_pp_status ' 'ON pending_points(sync_status)', ); await db.execute(''' CREATE TABLE IF NOT EXISTS imported_layers ( id TEXT PRIMARY KEY, name TEXT NOT NULL, source_type TEXT NOT NULL, local_path TEXT NOT NULL, storage_path TEXT, color_hex TEXT, opacity REAL, stroke_width REAL, is_visible INTEGER NOT NULL DEFAULT 1, project_id INTEGER, imported_at TEXT NOT NULL, synced_at TEXT ) '''); await db.execute( 'CREATE INDEX idx_imp_layers_project ON imported_layers(project_id)'); await _createStakeoutTable(db); // Alap projekt létrehozása az első indításhoz final now = DateTime.now().toIso8601String(); await db.insert('projects', { 'uuid': const Uuid().v4(), 'name': 'Alapértelmezett projekt', 'is_default': 1, 'is_local_only': 0, 'status': 'active', 'created_at': now, 'updated_at': now, }); } Future _onUpgrade(Database db, int oldVersion, int newVersion) async { if (oldVersion < 2) { await db.execute(''' ALTER TABLE imported_layers ADD COLUMN color_hex TEXT; '''); await db.execute(''' ALTER TABLE imported_layers ADD COLUMN opacity REAL; '''); await db.execute(''' ALTER TABLE imported_layers ADD COLUMN stroke_width REAL; '''); } if (oldVersion < 3) { await _createStakeoutTable(db); } } // ── Projects CRUD ───────────────────────────────────────────────── Future insertProject(Project p) async => (await database).insert('projects', p.toMap()); Future updateProject(Project p) async => (await database) .update('projects', p.toMap(), where: 'id = ?', whereArgs: [p.id]); Future archiveProject(int id) async => (await database).update( 'projects', {'status': 'archived', 'updated_at': DateTime.now().toIso8601String()}, where: 'id = ?', whereArgs: [id]); Future> listProjects({bool includeArchived = false}) async { final rows = await (await database).query( 'projects', where: includeArchived ? null : "status = 'active'", orderBy: 'updated_at DESC', ); return rows.map(Project.fromMap).toList(); } Future getProject(int id) async { final rows = await (await database) .query('projects', where: 'id = ?', whereArgs: [id], limit: 1); return rows.isEmpty ? null : Project.fromMap(rows.first); } /// Projekt keresése a globális uuid alapján — közös projekthez /// csatlakozáskor ezzel derül ki, megvan-e már lokálisan. Future getProjectByUuid(String uuid) async { final rows = await (await database) .query('projects', where: 'uuid = ?', whereArgs: [uuid], limit: 1); return rows.isEmpty ? null : Project.fromMap(rows.first); } // Projekt statisztikák — a listázáshoz Future> getProjectStats(int projectId) async { final db = await database; final points = Sqflite.firstIntValue(await db.rawQuery( 'SELECT COUNT(*) FROM measured_points WHERE project_id = ?', [projectId])) ?? 0; final tracks = Sqflite.firstIntValue(await db.rawQuery( 'SELECT COUNT(*) FROM tracks WHERE project_id = ?', [projectId])) ?? 0; final notes = Sqflite.firstIntValue(await db.rawQuery( 'SELECT COUNT(*) FROM note_items WHERE project_id = ?', [projectId])) ?? 0; return {'points': points, 'tracks': tracks, 'notes': notes}; } Future insertTrack(Track track) async { final db = await database; return db.insert('tracks', track.toMap()); } Future updateTrack(Track track) async { final db = await database; await db.update( 'tracks', track.toMap(), where: 'id = ?', whereArgs: [track.id], ); } Future deleteTrack(int id) async { final db = await database; await db.delete('tracks', where: 'id = ?', whereArgs: [id]); } Future> listTracks() async { final db = await database; final rows = await db.query('tracks', orderBy: 'start_time DESC'); return rows.map(Track.fromMap).toList(); } Future getTrack(int id) async { final db = await database; final rows = await db.query( 'tracks', where: 'id = ?', whereArgs: [id], limit: 1, ); return rows.isEmpty ? null : Track.fromMap(rows.first); } // ═══════════════════════════════════════════════════════════════ // TRACK POINTS // ═══════════════════════════════════════════════════════════════ Future addPoint(TrackPoint point, double newDistance) async { final db = await database; try { await db.transaction((txn) async { await txn.insert('track_points', point.toMap()); await txn.rawUpdate(''' UPDATE tracks SET distance_m = ?, point_count = point_count + 1 WHERE id = ? ''', [newDistance, point.trackId]); }); } catch (e) { print( 'addPoint hiba: $e - trackId=${point.trackId} dist=$newDistance'); } } Future> getPoints(int trackId) async { final db = await database; final rows = await db.query( 'track_points', where: 'track_id = ?', whereArgs: [trackId], orderBy: 'timestamp ASC', ); return rows.map(TrackPoint.fromMap).toList(); } Future> getLatLons(int trackId) async { final db = await database; final rows = await db.query( 'track_points', columns: ['latitude', 'longitude'], where: 'track_id = ?', whereArgs: [trackId], orderBy: 'timestamp ASC', ); return rows .map((r) => ( lat: r['latitude'] as double, lon: r['longitude'] as double, )) .toList(); } // ═══════════════════════════════════════════════════════════════ // PENDING POINTS (szinkron queue) // ═══════════════════════════════════════════════════════════════ Future insertPendingPoint(Map point) async { final db = await database; return db.insert('pending_points', { 'point_number': point['pointNumber'], 'gnss_number': point['gnssNumber'], 'latitude': point['latitude'], 'longitude': point['longitude'], 'altitude': point['altitude'], 'height_of_geoid': point['heightOfGeoid'], 'eov_x': point['eovX'], 'eov_y': point['eovY'], 'pole_height': point['poleHeight'], 'horizontal_error': point['horizontalError'], 'vertical_error': point['verticalError'], 'description': point['description'], 'is_deleted': (point['isDeleted'] == true) ? 1 : 0, 'project_id': point['projectId'] ?? 2, 'created_at': DateTime.now().toIso8601String(), 'sync_status': 'pending', }); } Future>> getPendingPoints() async { final db = await database; // JOIN a projects táblával — lokális projektek kiszűrve return db.rawQuery(''' SELECT pp.* FROM pending_points pp LEFT JOIN projects pr ON pr.id = pp.project_id WHERE pp.sync_status = 'pending' AND (pr.is_local_only = 0 OR pr.is_local_only IS NULL) ORDER BY pp.id ASC '''); } Future getPendingCount() async { final db = await database; final result = await db.rawQuery( "SELECT COUNT(*) AS cnt FROM pending_points " "WHERE sync_status = 'pending'", ); return (result.first['cnt'] as int?) ?? 0; } Future markPointSynced(int id) async { final db = await database; await db.update( 'pending_points', {'sync_status': 'synced'}, where: 'id = ?', whereArgs: [id], ); } Future markPointError(int id) async { final db = await database; await db.update( 'pending_points', {'sync_status': 'error'}, where: 'id = ?', whereArgs: [id], ); } /// Sikeresen szinkronizált pontok törlése (takarítás) Future purgeSyncedPoints() async { final db = await database; await db.delete( 'pending_points', where: 'sync_status = ?', whereArgs: ['synced'], ); } // ------------------- Terepbejárás pontok, vonalak, területek /// Elem mentése - visszaadja a kapott AQLite id-t Future insertNoteItem(NoteItem item) async { final db = await database; return db.insert('note_items', item.toMap()); } /// Elem frissítése (szín, label, koordináták módosítása után). Future updateNoteItem(NoteItem item) async { final db = await database; await db.update( 'note_items', item.toMap(), where: 'id = ?', whereArgs: [item.id], ); } /// Egy elem törlése. Future deleteNoteItem(int id) async { final db = await database; await db.delete( 'note_items', where: 'id = ?', whereArgs: [id], ); } /// Projekt összes eleme — opcionálisan típus szerint szűrve. Future> listNoteItems(int? projectId, {NoteType? type}) async { final db = await database; String? where; List whereArgs = []; if (projectId != null) { where = type != null ? 'project_id = ? AND type = ?' : 'project_id = ?'; whereArgs = type != null ? [projectId, type.name] : [projectId]; } else { // Projekt nélküli elemek where = type != null ? 'type = ?' : null; whereArgs = type != null ? [type.name] : []; } final rows = await db.query( 'note_items', where: where, whereArgs: whereArgs, orderBy: 'created_at ASC', ); return rows.map(NoteItem.fromMap).toList(); } /// Egyetlen elem lekérése id alapján. Future getNoteItem(int id) async { final db = await database; final rows = await db.query( 'note_items', where: 'id = ?', whereArgs: [id], limit: 1, ); return rows.isEmpty ? null : NoteItem.fromMap(rows.first); } /// Projekt összes elemének törlése. Future deleteAllNoteItems(int projectId) async { final db = await database; await db.delete( 'note_items', where: 'project_id = ?', whereArgs: [projectId], ); } // -------- NoteItemPhoto Future insertNotePhoto(NoteItemPhoto photo) async { final db = await database; return db.insert('note_item_photos', photo.toMap()); } Future updateNotePhoto(NoteItemPhoto photo) async { final db = await database; await db.update( 'note_item_photos', photo.toMap(), where: 'id = ?', whereArgs: [photo.id], ); } Future deleteNotePhoto(int id) async { final db = await database; await db.delete('note_item_photos', where: 'id = ?', whereArgs: [id]); } Future> listNotePhotos(int noteItemId) async { final db = await database; final rows = await db.query( 'note_item_photos', where: 'note_item_id = ?', whereArgs: [noteItemId], orderBy: 'created_at ASC', ); return rows.map(NoteItemPhoto.fromMap).toList(); } Future deleteAllNotePhotos(int noteItemId) async { final db = await database; await db.delete('note_item_photos', where: 'note_item_id = ?', whereArgs: [noteItemId]); } Future countNotePhotos(int noteItemId) async { final db = await database; final res = await db.rawQuery( 'SELECT COUNT(*) FROM note_item_photos WHERE note_item_id = ?', [noteItemId], ); return Sqflite.firstIntValue(res) ?? 0; } // -------------- NoteItemAudio Future insertNoteAudio(NoteItemAudio audio) async { final db = await database; return db.insert('note_item_audios', audio.toMap()); } Future updateNoteAudio(NoteItemAudio audio) async { final db = await database; await db.update('note_item_audios', audio.toMap(), where: 'id = ?', whereArgs: [audio.id]); } Future deleteNoteAudio(int id) async { final db = await database; await db.delete('note_item_audios', where: 'id = ?', whereArgs: [id]); } Future> listNoteAudios(int noteItemId) async { final db = await database; final rows = await db.query( 'note_item_audios', where: 'note_item_id = ?', whereArgs: [noteItemId], orderBy: 'created_at ASC', ); return rows.map(NoteItemAudio.fromMap).toList(); } Future countNoteAudios(int noteItemId) async { final db = await database; final res = await db.rawQuery( 'SELECT COUNT(*) FROM note_item_audios WHERE note_item_id = ?', [noteItemId], ); return Sqflite.firstIntValue(res) ?? 0; } // ----------- Layer meta adatok Future insertImportedLayer(ImportedLayerMeta meta) async { final db = await database; await db.insert('imported_layers', meta.toMap(), conflictAlgorithm: ConflictAlgorithm.replace); } Future updateImportedLayer(ImportedLayerMeta meta) async { final db = await database; await db.update('imported_layers', meta.toMap(), where: 'id = ?', whereArgs: [meta.id]); } Future deleteImportedLayer(String id) async { final db = await database; await db.delete('imported_layers', where: 'id = ?', whereArgs: [id]); } Future> listImportedLayers({int? projectId}) async { final db = await database; final rows = await db.query( 'imported_layers', where: projectId != null ? 'project_id = ?' : null, whereArgs: projectId != null ? [projectId] : null, orderBy: 'imported_at DESC', ); return rows.map(ImportedLayerMeta.fromMap).toList(); } // ------------ Bemért pontok Future insertMeasuredPoint(MeasuredPoint point) async { final db = await database; return db.insert('measured_points', point.toMap()); } Future deleteMeasuredPoint(int id) async { final db = await database; await db.delete('measured_points', where: 'id = ?', whereArgs: [id]); } Future deleteAllMeasuredPoints(int projectId) async { final db = await database; await db.delete('measured_points', where: 'project_id = ?', whereArgs: [projectId]); } Future> listMeasuredPoints(int projectId) async { final db = await database; final rows = await db.query( 'measured_points', where: 'project_id = ?', whereArgs: [projectId], orderBy: 'timestamp ASC', ); return rows.map(MeasuredPoint.fromMap).toList(); } // ═══════════════════════════════════════════════════════════════ // KITŰZÉS (stakeout_points) — szeizmikus line/station modell // ═══════════════════════════════════════════════════════════════ Future _createStakeoutTable(Database db) async { await db.execute(''' CREATE TABLE IF NOT EXISTS stakeout_points ( id INTEGER PRIMARY KEY AUTOINCREMENT, uuid TEXT NOT NULL UNIQUE, project_id INTEGER NOT NULL, line_id TEXT NOT NULL DEFAULT '', station INTEGER NOT NULL, name TEXT NOT NULL, point_type TEXT NOT NULL DEFAULT 'geofon', source TEXT NOT NULL DEFAULT 'csv', plan_eov_y REAL NOT NULL, plan_eov_x REAL NOT NULL, plan_eov_z REAL, plan_lat REAL NOT NULL, plan_lon REAL NOT NULL, status TEXT NOT NULL DEFAULT 'pending', measured_eov_y REAL, measured_eov_x REAL, measured_eov_z REAL, measured_lat REAL, measured_lon REAL, dev_inline REAL, dev_crossline REAL, dev_dz REAL, fix_quality INTEGER, accuracy REAL, tilt_deg REAL, staked_at TEXT, is_offset INTEGER NOT NULL DEFAULT 0, parent_uuid TEXT, offset_dist REAL, offset_bearing REAL, created_by TEXT, device_id TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL, deleted_at TEXT, sync_status TEXT NOT NULL DEFAULT 'pending' ) '''); await db.execute('CREATE INDEX IF NOT EXISTS idx_sp_proj_line ' 'ON stakeout_points(project_id, line_id, station)'); await db.execute('CREATE INDEX IF NOT EXISTS idx_sp_status ' 'ON stakeout_points(status)'); await db.execute('CREATE INDEX IF NOT EXISTS idx_sp_sync ' 'ON stakeout_points(sync_status)'); } Future insertStakeoutPoint(StakeoutPoint p) async { final db = await database; final map = p.toMap(); map['device_id'] ??= DeviceIdentityService.to.deviceId; return db.insert('stakeout_points', map); } /// Tömeges beszúrás importhoz — tranzakcióban; az azonos /// (projekt, vonal, station) sorokat kihagyja. Visszaadja a /// ténylegesen beszúrt darabszámot. Future insertStakeoutPoints(List points) async { final db = await database; var inserted = 0; await db.transaction((txn) async { for (final p in points) { final dup = await txn.query('stakeout_points', columns: ['id'], where: 'project_id = ? AND line_id = ? AND station = ? ' 'AND deleted_at IS NULL', whereArgs: [p.projectId, p.lineId, p.station], limit: 1); if (dup.isNotEmpty) continue; final map = p.toMap(); map['device_id'] ??= DeviceIdentityService.to.deviceId; await txn.insert('stakeout_points', map); inserted++; } }); return inserted; } Future updateStakeoutPoint(StakeoutPoint p) async { final db = await database; final map = p.toMap() ..['updated_at'] = DateTime.now().toIso8601String() ..['sync_status'] = 'pending'; await db.update('stakeout_points', map, where: 'id = ?', whereArgs: [p.id]); } /// Soft delete — a törlés is szinkronizálható lesz (4. ütem). Future softDeleteStakeoutPoint(int id) async { final db = await database; final now = DateTime.now().toIso8601String(); await db.update('stakeout_points', {'deleted_at': now, 'updated_at': now, 'sync_status': 'pending'}, where: 'id = ?', whereArgs: [id]); } Future> listStakeoutPoints(int projectId) async { final db = await database; final rows = await db.query('stakeout_points', where: 'project_id = ? AND deleted_at IS NULL', whereArgs: [projectId], orderBy: 'line_id ASC, station ASC'); return rows.map(StakeoutPoint.fromMap).toList(); } /// Feltöltésre váró kitűzési pontok — a projekt uuid-jával együtt. /// A csak lokális projektek SQL-szinten kizárva. Future>> pendingStakeoutPoints() async { final db = await database; return db.rawQuery(''' SELECT sp.*, pr.uuid AS project_uuid FROM stakeout_points sp JOIN projects pr ON pr.id = sp.project_id WHERE sp.sync_status = 'pending' AND pr.is_local_only = 0 ORDER BY sp.id ASC '''); } // ═══════════════════════════════════════════════════════════════ // SZINKRON-SEGÉDEK — a TsSyncService használja őket // ═══════════════════════════════════════════════════════════════ // // FONTOS: minden lekérdezés kihagyja az is_local_only = 1 projekteket // és azok adatait — a "csak lokális" projekt SOHA nem hagyja el az // eszközt, semmilyen szinkron-útvonalon. /// Feltöltésre váró (nem lokális) projektek. Future>> pendingProjects() async { final db = await database; return db.query('projects', where: "is_local_only = 0 AND sync_status = 'pending'"); } /// Feltöltésre váró bemért pontok — a projekt uuid-jával együtt. Future>> pendingMeasuredPoints() async { final db = await database; return db.rawQuery(''' SELECT mp.*, pr.uuid AS project_uuid FROM measured_points mp JOIN projects pr ON pr.id = mp.project_id WHERE mp.sync_status = 'pending' AND pr.is_local_only = 0 ORDER BY mp.id ASC '''); } /// Feltöltésre váró trackek — a projekt uuid-jával együtt. Future>> pendingTracks() async { final db = await database; return db.rawQuery(''' SELECT t.*, pr.uuid AS project_uuid FROM tracks t JOIN projects pr ON pr.id = t.project_id WHERE t.sync_status = 'pending' AND pr.is_local_only = 0 ORDER BY t.id ASC '''); } /// Feltöltésre váró track-pontok, batch-ben — track- és projekt-uuid-val. /// Csak lezárt vagy futó, NEM törölt, NEM lokális trackek pontjai. Future>> pendingTrackPoints( {int limit = 500}) async { final db = await database; return db.rawQuery(''' SELECT tp.*, t.uuid AS track_uuid, pr.uuid AS project_uuid FROM track_points tp JOIN tracks t ON t.id = tp.track_id JOIN projects pr ON pr.id = t.project_id WHERE tp.sync_status = 'pending' AND t.deleted_at IS NULL AND t.sync_status != 'pending' AND pr.is_local_only = 0 ORDER BY tp.id ASC LIMIT ? ''', [limit]); } /// Feltöltésre váró terepbejárás-elemek (csak projekthez rendeltek). Future>> pendingNoteItems() async { final db = await database; return db.rawQuery(''' SELECT n.*, pr.uuid AS project_uuid FROM note_items n JOIN projects pr ON pr.id = n.project_id WHERE n.sync_status = 'pending' AND pr.is_local_only = 0 ORDER BY n.id ASC '''); } /// Összes feltöltésre váró sor száma (UI-jelvényhez). Future pendingSyncCount() async { final db = await database; final res = await db.rawQuery(''' SELECT (SELECT COUNT(*) FROM projects p WHERE p.is_local_only = 0 AND p.sync_status = 'pending') + (SELECT COUNT(*) FROM measured_points mp JOIN projects pr ON pr.id = mp.project_id WHERE mp.sync_status = 'pending' AND pr.is_local_only = 0) + (SELECT COUNT(*) FROM tracks t JOIN projects pr ON pr.id = t.project_id WHERE t.sync_status = 'pending' AND pr.is_local_only = 0) + (SELECT COUNT(*) FROM track_points tp JOIN tracks t ON t.id = tp.track_id JOIN projects pr ON pr.id = t.project_id WHERE tp.sync_status = 'pending' AND pr.is_local_only = 0) AS cnt '''); return (res.first['cnt'] as int?) ?? 0; } /// Sorok megjelölése szinkronizáltként, uuid alapján (500-as darabokban, /// az SQLite paraméter-limit miatt). Future markSyncedByUuid(String table, List uuids) async { if (uuids.isEmpty) return; final db = await database; for (var i = 0; i < uuids.length; i += 500) { final chunk = uuids.sublist(i, i + 500 > uuids.length ? uuids.length : i + 500); final placeholders = List.filled(chunk.length, '?').join(','); await db.rawUpdate( "UPDATE $table SET sync_status = 'synced' WHERE uuid IN ($placeholders)", chunk, ); } } // ── Szinkron-kurzor (projektenként) ────────────────────────────── Future getProjectCursor(int localProjectId) async { final db = await database; final rows = await db.query('projects', columns: ['last_pulled_at'], where: 'id = ?', whereArgs: [localProjectId], limit: 1); return rows.isEmpty ? null : rows.first['last_pulled_at'] as String?; } Future setProjectCursor(int localProjectId, String cursor) async { final db = await database; await db.update('projects', {'last_pulled_at': cursor}, where: 'id = ?', whereArgs: [localProjectId]); } // ── Távoli sorok lokális alkalmazása ───────────────────────────── /// Távoli projekt-sor lokális upsertje uuid alapján. /// Visszaadja a lokális id-t. Csatlakozáskor és letöltéskor is ez fut. Future upsertProjectFromRemote(Map remote) async { final db = await database; final existing = await getProjectByUuid(remote['id'] as String); final map = { 'uuid': remote['id'], 'name': remote['name'] ?? '', 'client': remote['client'] ?? '', 'description': remote['description'] ?? '', 'crs': remote['crs'] ?? 'eov', 'color': remote['color'] ?? '#185FA5', 'status': remote['status'] ?? 'active', 'is_local_only': 0, 'updated_at': remote['updated_at'] ?? DateTime.now().toIso8601String(), 'deleted_at': remote['deleted_at'], 'sync_status': 'synced', 'last_synced_at': DateTime.now().toIso8601String(), }; if (existing == null) { map['created_at'] = remote['created_at'] ?? DateTime.now().toIso8601String(); return db.insert('projects', map); } // Lokális, még fel nem töltött módosítás nyer (push fut előbb). final localPending = await db.query('projects', columns: ['sync_status'], where: 'id = ?', whereArgs: [existing.id], limit: 1); if (localPending.first['sync_status'] == 'pending') return existing.id!; await db.update('projects', map, where: 'id = ?', whereArgs: [existing.id]); return existing.id!; } /// Általános távoli-sor alkalmazás uuid-kulcsú táblára /// (measured_points / tracks / note_items). /// /// LWW-szabály: ha a lokális sor 'pending' (van el nem küldött helyi /// módosítás), a távolit kihagyjuk — a push úgyis előbb fut, mint a pull. Future applyRemoteRow( String table, Map localMap) async { final db = await database; final uuid = localMap['uuid'] as String; final rows = await db.query(table, columns: ['id', 'sync_status'], where: 'uuid = ?', whereArgs: [uuid], limit: 1); if (rows.isEmpty) { // Ismeretlen sor tombstone-ja: nincs mit törölni, kihagyjuk. if (localMap['deleted_at'] != null) return; await db.insert(table, localMap); return; } if (rows.first['sync_status'] == 'pending') return; await db.update(table, localMap, where: 'id = ?', whereArgs: [rows.first['id']]); } /// Track lokális id-ja uuid alapján (a track-pontok letöltéséhez). Future getTrackIdByUuid(String uuid) async { final db = await database; final rows = await db.query('tracks', columns: ['id'], where: 'uuid = ?', whereArgs: [uuid], limit: 1); return rows.isEmpty ? null : rows.first['id'] as int; } /// Távoli track-pontok beszúrása — a uuid unique index miatt az /// ismétlődések csendben kimaradnak (idempotens). Future insertRemoteTrackPoints(List> maps) async { if (maps.isEmpty) return; final db = await database; final batch = db.batch(); for (final m in maps) { batch.insert('track_points', m, conflictAlgorithm: ConflictAlgorithm.ignore); } await batch.commit(noResult: true); // A point_count/distance a saját eszközön mérésnél frissül; letöltött // pontoknál a track sora már a helyes összesítőkkel érkezik a szerverről. } // ── Réteg-szinkron segédek ─────────────────────────────────────── Future getImportedLayerVersion(String id) async { final db = await database; final rows = await db.query('imported_layers', columns: ['version'], where: 'id = ?', whereArgs: [id], limit: 1); return rows.isEmpty ? -1 : (rows.first['version'] as int? ?? 0); } Future upsertImportedLayerRaw(Map map) async { final db = await database; await db.insert('imported_layers', map, conflictAlgorithm: ConflictAlgorithm.replace); } }