1044 lines
36 KiB
Dart
1044 lines
36 KiB
Dart
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<Database> get database async {
|
|
_db ??= await _open();
|
|
return _db!;
|
|
}
|
|
|
|
Future<Database> _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<void> _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<void> _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<int> insertProject(Project p) async =>
|
|
(await database).insert('projects', p.toMap());
|
|
|
|
Future<void> updateProject(Project p) async => (await database)
|
|
.update('projects', p.toMap(), where: 'id = ?', whereArgs: [p.id]);
|
|
|
|
Future<void> archiveProject(int id) async => (await database).update(
|
|
'projects',
|
|
{'status': 'archived', 'updated_at': DateTime.now().toIso8601String()},
|
|
where: 'id = ?',
|
|
whereArgs: [id]);
|
|
|
|
Future<List<Project>> 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<Project?> 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<Project?> 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<Map<String, int>> 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<int> insertTrack(Track track) async {
|
|
final db = await database;
|
|
return db.insert('tracks', track.toMap());
|
|
}
|
|
|
|
Future<void> updateTrack(Track track) async {
|
|
final db = await database;
|
|
await db.update(
|
|
'tracks',
|
|
track.toMap(),
|
|
where: 'id = ?',
|
|
whereArgs: [track.id],
|
|
);
|
|
}
|
|
|
|
Future<void> deleteTrack(int id) async {
|
|
final db = await database;
|
|
await db.delete('tracks', where: 'id = ?', whereArgs: [id]);
|
|
}
|
|
|
|
Future<List<Track>> listTracks() async {
|
|
final db = await database;
|
|
final rows = await db.query('tracks', orderBy: 'start_time DESC');
|
|
return rows.map(Track.fromMap).toList();
|
|
}
|
|
|
|
Future<Track?> 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<void> 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<List<TrackPoint>> 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<List<({double lat, double lon})>> 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<int> insertPendingPoint(Map<String, dynamic> 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<List<Map<String, dynamic>>> 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<int> 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<void> markPointSynced(int id) async {
|
|
final db = await database;
|
|
await db.update(
|
|
'pending_points',
|
|
{'sync_status': 'synced'},
|
|
where: 'id = ?',
|
|
whereArgs: [id],
|
|
);
|
|
}
|
|
|
|
Future<void> 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<void> 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<int> 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<void> 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<void> 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<List<NoteItem>> listNoteItems(int? projectId, {NoteType? type}) async {
|
|
final db = await database;
|
|
|
|
String? where;
|
|
List<Object?> 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<NoteItem?> 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<void> deleteAllNoteItems(int projectId) async {
|
|
final db = await database;
|
|
await db.delete(
|
|
'note_items',
|
|
where: 'project_id = ?',
|
|
whereArgs: [projectId],
|
|
);
|
|
}
|
|
|
|
// -------- NoteItemPhoto
|
|
|
|
Future<int> insertNotePhoto(NoteItemPhoto photo) async {
|
|
final db = await database;
|
|
return db.insert('note_item_photos', photo.toMap());
|
|
}
|
|
|
|
Future<void> updateNotePhoto(NoteItemPhoto photo) async {
|
|
final db = await database;
|
|
await db.update(
|
|
'note_item_photos',
|
|
photo.toMap(),
|
|
where: 'id = ?',
|
|
whereArgs: [photo.id],
|
|
);
|
|
}
|
|
|
|
Future<void> deleteNotePhoto(int id) async {
|
|
final db = await database;
|
|
await db.delete('note_item_photos', where: 'id = ?', whereArgs: [id]);
|
|
}
|
|
|
|
Future<List<NoteItemPhoto>> 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<void> deleteAllNotePhotos(int noteItemId) async {
|
|
final db = await database;
|
|
await db.delete('note_item_photos',
|
|
where: 'note_item_id = ?', whereArgs: [noteItemId]);
|
|
}
|
|
|
|
Future<int> 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<int> insertNoteAudio(NoteItemAudio audio) async {
|
|
final db = await database;
|
|
return db.insert('note_item_audios', audio.toMap());
|
|
}
|
|
|
|
Future<void> updateNoteAudio(NoteItemAudio audio) async {
|
|
final db = await database;
|
|
await db.update('note_item_audios', audio.toMap(),
|
|
where: 'id = ?', whereArgs: [audio.id]);
|
|
}
|
|
|
|
Future<void> deleteNoteAudio(int id) async {
|
|
final db = await database;
|
|
await db.delete('note_item_audios', where: 'id = ?', whereArgs: [id]);
|
|
}
|
|
|
|
Future<List<NoteItemAudio>> 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<int> 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<void> insertImportedLayer(ImportedLayerMeta meta) async {
|
|
final db = await database;
|
|
await db.insert('imported_layers', meta.toMap(),
|
|
conflictAlgorithm: ConflictAlgorithm.replace);
|
|
}
|
|
|
|
Future<void> updateImportedLayer(ImportedLayerMeta meta) async {
|
|
final db = await database;
|
|
await db.update('imported_layers', meta.toMap(),
|
|
where: 'id = ?', whereArgs: [meta.id]);
|
|
}
|
|
|
|
Future<void> deleteImportedLayer(String id) async {
|
|
final db = await database;
|
|
await db.delete('imported_layers', where: 'id = ?', whereArgs: [id]);
|
|
}
|
|
|
|
Future<List<ImportedLayerMeta>> 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<int> insertMeasuredPoint(MeasuredPoint point) async {
|
|
final db = await database;
|
|
return db.insert('measured_points', point.toMap());
|
|
}
|
|
|
|
Future<void> deleteMeasuredPoint(int id) async {
|
|
final db = await database;
|
|
await db.delete('measured_points', where: 'id = ?', whereArgs: [id]);
|
|
}
|
|
|
|
Future<void> deleteAllMeasuredPoints(int projectId) async {
|
|
final db = await database;
|
|
await db.delete('measured_points',
|
|
where: 'project_id = ?', whereArgs: [projectId]);
|
|
}
|
|
|
|
Future<List<MeasuredPoint>> 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<void> _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<int> 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<int> insertStakeoutPoints(List<StakeoutPoint> 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<void> 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<void> 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<List<StakeoutPoint>> 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<List<Map<String, dynamic>>> 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<List<Map<String, dynamic>>> 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<List<Map<String, dynamic>>> 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<List<Map<String, dynamic>>> 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<List<Map<String, dynamic>>> 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<List<Map<String, dynamic>>> 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<int> 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<void> markSyncedByUuid(String table, List<String> 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<String?> 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<void> 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<int> upsertProjectFromRemote(Map<String, dynamic> remote) async {
|
|
final db = await database;
|
|
final existing = await getProjectByUuid(remote['id'] as String);
|
|
|
|
final map = <String, dynamic>{
|
|
'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<void> applyRemoteRow(
|
|
String table, Map<String, dynamic> 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<int?> 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<void> insertRemoteTrackPoints(List<Map<String, dynamic>> 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<int> 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<void> upsertImportedLayerRaw(Map<String, dynamic> map) async {
|
|
final db = await database;
|
|
await db.insert('imported_layers', map,
|
|
conflictAlgorithm: ConflictAlgorithm.replace);
|
|
}
|
|
}
|