Adatbázis új struktúra és könyvtár, projektrendszer bevezetése, ProjectService
This commit is contained in:
parent
8b4c25f475
commit
2364b2311c
@ -4,6 +4,7 @@ import 'package:flutter_dotenv/flutter_dotenv.dart';
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||||
import 'package:terepi_seged/routes/app_pages.dart';
|
import 'package:terepi_seged/routes/app_pages.dart';
|
||||||
|
import 'package:terepi_seged/services/app_database.dart';
|
||||||
import 'package:terepi_seged/services/coord_converter_service.dart';
|
import 'package:terepi_seged/services/coord_converter_service.dart';
|
||||||
import 'package:terepi_seged/services/gnss/gnss_device_service.dart';
|
import 'package:terepi_seged/services/gnss/gnss_device_service.dart';
|
||||||
import 'package:terepi_seged/services/gnss/gnss_service.dart';
|
import 'package:terepi_seged/services/gnss/gnss_service.dart';
|
||||||
@ -18,6 +19,8 @@ Future<void> main() async {
|
|||||||
url: dotenv.env['SUPABASE_URL']!,
|
url: dotenv.env['SUPABASE_URL']!,
|
||||||
anonKey: dotenv.env['SUPABASE_ANON_KEY']!);
|
anonKey: dotenv.env['SUPABASE_ANON_KEY']!);
|
||||||
|
|
||||||
|
await AppDatabase.instance.database;
|
||||||
|
|
||||||
await Get.putAsync<CoordConverterService>(
|
await Get.putAsync<CoordConverterService>(
|
||||||
() => CoordConverterService().init());
|
() => CoordConverterService().init());
|
||||||
Get.put(GnssDeviceService());
|
Get.put(GnssDeviceService());
|
||||||
|
|||||||
86
lib/models/project.dart
Normal file
86
lib/models/project.dart
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
enum ProjectStatus { active, archived }
|
||||||
|
|
||||||
|
enum ProjectCrs { eov, wgs84 }
|
||||||
|
|
||||||
|
class Project {
|
||||||
|
final int? id;
|
||||||
|
final String uuid;
|
||||||
|
final String name;
|
||||||
|
final String client; // megrendelő
|
||||||
|
final String description;
|
||||||
|
final ProjectCrs crs;
|
||||||
|
final String color; // hex, a UI-ban azonosításhoz
|
||||||
|
final ProjectStatus status;
|
||||||
|
final bool isLocalOnly;
|
||||||
|
final DateTime? lastSyncedAt;
|
||||||
|
final DateTime createdAt;
|
||||||
|
final DateTime updatedAt;
|
||||||
|
|
||||||
|
const Project({
|
||||||
|
this.id,
|
||||||
|
required this.uuid,
|
||||||
|
required this.name,
|
||||||
|
this.client = '',
|
||||||
|
this.description = '',
|
||||||
|
this.crs = ProjectCrs.eov,
|
||||||
|
this.color = '#185FA5',
|
||||||
|
this.status = ProjectStatus.active,
|
||||||
|
this.isLocalOnly = false,
|
||||||
|
this.lastSyncedAt,
|
||||||
|
required this.createdAt,
|
||||||
|
required this.updatedAt,
|
||||||
|
});
|
||||||
|
|
||||||
|
Project copyWith(
|
||||||
|
{String? name,
|
||||||
|
String? client,
|
||||||
|
String? description,
|
||||||
|
ProjectCrs? crs,
|
||||||
|
String? color,
|
||||||
|
ProjectStatus? status}) =>
|
||||||
|
Project(
|
||||||
|
id: id,
|
||||||
|
uuid: uuid,
|
||||||
|
name: name ?? this.name,
|
||||||
|
client: client ?? this.client,
|
||||||
|
description: description ?? this.description,
|
||||||
|
crs: crs ?? this.crs,
|
||||||
|
color: color ?? this.color,
|
||||||
|
status: status ?? this.status,
|
||||||
|
isLocalOnly: isLocalOnly,
|
||||||
|
lastSyncedAt: lastSyncedAt,
|
||||||
|
createdAt: createdAt,
|
||||||
|
updatedAt: DateTime.now(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> toMap() => {
|
||||||
|
if (id != null) 'id': id,
|
||||||
|
'uuid': uuid,
|
||||||
|
'name': name,
|
||||||
|
'client': client,
|
||||||
|
'description': description,
|
||||||
|
'crs': crs.name,
|
||||||
|
'color': color,
|
||||||
|
'status': status.name,
|
||||||
|
'isLocalOnly': isLocalOnly,
|
||||||
|
if (lastSyncedAt != null)
|
||||||
|
'last_synced_at': lastSyncedAt!.toIso8601String(),
|
||||||
|
'created_at': createdAt.toIso8601String(),
|
||||||
|
'updated_at': updatedAt.toIso8601String(),
|
||||||
|
};
|
||||||
|
|
||||||
|
factory Project.fromMap(Map<String, dynamic> m) => Project(
|
||||||
|
id: m['id'] as int?,
|
||||||
|
uuid: m['uuid'] as String,
|
||||||
|
name: m['name'] as String,
|
||||||
|
client: m['client'] as String? ?? '',
|
||||||
|
description: m['description'] as String? ?? '',
|
||||||
|
crs: ProjectCrs.values.byName(m['crs'] as String? ?? 'eov'),
|
||||||
|
color: m['color'] as String? ?? '#185FA5',
|
||||||
|
status: ProjectStatus.values.byName(m['status'] as String? ?? 'active'),
|
||||||
|
isLocalOnly: m['is__local_only'] as bool ?? true,
|
||||||
|
lastSyncedAt: DateTime.parse(m['last_synced_at'] as String),
|
||||||
|
createdAt: DateTime.parse(m['created_at'] as String),
|
||||||
|
updatedAt: DateTime.parse(m['updated_at'] as String),
|
||||||
|
);
|
||||||
|
}
|
||||||
367
lib/services/app_database.dart
Normal file
367
lib/services/app_database.dart
Normal file
@ -0,0 +1,367 @@
|
|||||||
|
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/models/track.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 path = p.join(directory!.path, 'database', 'terepi_seged.db');
|
||||||
|
|
||||||
|
return openDatabase(path,
|
||||||
|
version: 1, 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_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 NOT NULL 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
|
||||||
|
)
|
||||||
|
''');
|
||||||
|
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 NOT NULL 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 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)',
|
||||||
|
);
|
||||||
|
|
||||||
|
// 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',
|
||||||
|
'created_at': now,
|
||||||
|
'updated_at': now,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onUpgrade(Database db, int oldVersion, int newVersion) async {}
|
||||||
|
|
||||||
|
// ── 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 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;
|
||||||
|
await db.transaction((txn) async {
|
||||||
|
await txn.insert('track_points', point.toMap());
|
||||||
|
await txn.rawUpdate('''
|
||||||
|
UPDATE tracks
|
||||||
|
SET distance_meters = ?,
|
||||||
|
point_count = point_count + 1
|
||||||
|
WHERE id = ?
|
||||||
|
''', [newDistance, point.trackId]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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'],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
130
lib/services/project_service.dart
Normal file
130
lib/services/project_service.dart
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||||
|
import 'package:uuid/uuid.dart';
|
||||||
|
import '../models/project.dart';
|
||||||
|
import 'app_database.dart';
|
||||||
|
|
||||||
|
class ProjectService extends GetxService {
|
||||||
|
static ProjectService get to => Get.find();
|
||||||
|
|
||||||
|
final activeProject = Rxn<Project>();
|
||||||
|
final projects = <Project>[].obs;
|
||||||
|
|
||||||
|
int? get activeProjectId => activeProject.value?.id;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> onInit() async {
|
||||||
|
super.onInit();
|
||||||
|
await _loadProjects();
|
||||||
|
await _restoreActiveProject();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadProjects() async {
|
||||||
|
projects.value = await AppDatabase.instance.listProjects();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Az utoljára aktív projektet tárolja a SharedPreferences
|
||||||
|
Future<void> _restoreActiveProject() async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
final savedId = prefs.getInt('active_project_id');
|
||||||
|
|
||||||
|
if (savedId != null) {
|
||||||
|
final project = await AppDatabase.instance.getProject(savedId);
|
||||||
|
if (project != null && project.status == ProjectStatus.active) {
|
||||||
|
activeProject.value = project;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Fallback: az első aktív projekt
|
||||||
|
if (projects.isNotEmpty) {
|
||||||
|
await setActiveProject(projects.first);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> setActiveProject(Project project) async {
|
||||||
|
activeProject.value = project;
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
await prefs.setInt('active_project_id', project.id!);
|
||||||
|
|
||||||
|
// Frissítjük az updated_at-et hogy a lista tetejére kerüljön
|
||||||
|
await AppDatabase.instance.updateProject(project.copyWith());
|
||||||
|
await _loadProjects();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Project> createProject({
|
||||||
|
required String name,
|
||||||
|
String client = '',
|
||||||
|
String description = '',
|
||||||
|
ProjectCrs crs = ProjectCrs.eov,
|
||||||
|
String color = '#185FA5',
|
||||||
|
}) async {
|
||||||
|
final now = DateTime.now();
|
||||||
|
final project = Project(
|
||||||
|
uuid: const Uuid().v4(),
|
||||||
|
name: name,
|
||||||
|
client: client,
|
||||||
|
description: description,
|
||||||
|
crs: crs,
|
||||||
|
color: color,
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
);
|
||||||
|
final id = await AppDatabase.instance.insertProject(project);
|
||||||
|
final saved = project.copyWith();
|
||||||
|
await _loadProjects();
|
||||||
|
return await AppDatabase.instance.getProject(id) ?? saved;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Online projekt — szinkronizál Supabase-szel
|
||||||
|
Future<Project> createOnlineProject({
|
||||||
|
required String name,
|
||||||
|
String client = '',
|
||||||
|
ProjectCrs crs = ProjectCrs.eov,
|
||||||
|
String color = '#185FA5',
|
||||||
|
}) async {
|
||||||
|
final project = Project(
|
||||||
|
uuid: const Uuid().v4(),
|
||||||
|
name: name,
|
||||||
|
client: client,
|
||||||
|
crs: crs,
|
||||||
|
color: color,
|
||||||
|
isLocalOnly: false, // ← szinkronizált
|
||||||
|
createdAt: DateTime.now(),
|
||||||
|
updatedAt: DateTime.now(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Lokálisan mentjük
|
||||||
|
final id = await AppDatabase.instance.insertProject(project);
|
||||||
|
|
||||||
|
// Supabase-be is
|
||||||
|
await Supabase.instance.client.from('projects').insert(project.toMap());
|
||||||
|
|
||||||
|
await _loadProjects();
|
||||||
|
return await AppDatabase.instance.getProject(id) ?? project;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lokális projekt — soha nem kerül Supabase-be
|
||||||
|
Future<Project> createLocalProject({
|
||||||
|
required String name,
|
||||||
|
String client = '',
|
||||||
|
ProjectCrs crs = ProjectCrs.eov,
|
||||||
|
String color = '#185FA5',
|
||||||
|
}) async {
|
||||||
|
final project = Project(
|
||||||
|
uuid: const Uuid().v4(),
|
||||||
|
name: name,
|
||||||
|
client: client,
|
||||||
|
crs: crs,
|
||||||
|
color: color,
|
||||||
|
isLocalOnly: true, // ← csak lokális
|
||||||
|
createdAt: DateTime.now(),
|
||||||
|
updatedAt: DateTime.now(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Csak lokálisan
|
||||||
|
final id = await AppDatabase.instance.insertProject(project);
|
||||||
|
await _loadProjects();
|
||||||
|
return await AppDatabase.instance.getProject(id) ?? project;
|
||||||
|
}
|
||||||
|
}
|
||||||
155
lib/services/sync_service.dart
Normal file
155
lib/services/sync_service.dart
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||||
|
|
||||||
|
import 'app_database.dart';
|
||||||
|
|
||||||
|
enum SyncStatus { idle, syncing, error }
|
||||||
|
|
||||||
|
class SyncService extends GetxService {
|
||||||
|
static SyncService get to => Get.find();
|
||||||
|
|
||||||
|
final syncStatus = SyncStatus.idle.obs;
|
||||||
|
final pendingCount = 0.obs;
|
||||||
|
|
||||||
|
StreamSubscription? _connectivitySub;
|
||||||
|
final _supabase = Supabase.instance.client;
|
||||||
|
|
||||||
|
// AppDatabase — nincs külön DB fájl
|
||||||
|
AppDatabase get _db => AppDatabase.instance;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> onInit() async {
|
||||||
|
super.onInit();
|
||||||
|
await _updatePendingCount();
|
||||||
|
_startConnectivityListener();
|
||||||
|
|
||||||
|
if (await _hasConnectivity()) {
|
||||||
|
unawaited(_syncPendingItems());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> onClose() async {
|
||||||
|
await _connectivitySub?.cancel();
|
||||||
|
super.onClose();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Publikus API ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
Future<void> saveMeasuredPoint(Map<String, dynamic> point) async {
|
||||||
|
final projectId = point['projectId'] as int?;
|
||||||
|
final isLocalOnly = await _isLocalOnlyProject(projectId);
|
||||||
|
|
||||||
|
final localId = await _db.insertPendingPoint(point);
|
||||||
|
|
||||||
|
if (isLocalOnly) {
|
||||||
|
await _db.markPointSynced(localId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await _updatePendingCount();
|
||||||
|
|
||||||
|
if (await _hasConnectivity()) {
|
||||||
|
await _syncSinglePoint(localId, point);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Szinkronizálás ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
Future<void> _syncSinglePoint(int localId, Map<String, dynamic> point) async {
|
||||||
|
try {
|
||||||
|
await _supabase.from('TerepiSeged_MeasuredPoints').insert(point);
|
||||||
|
|
||||||
|
await _supabase
|
||||||
|
.from('TerepiSeged_Receiver')
|
||||||
|
.update({'isMeasured': true}).eq('pointNumber', point['pointNumber']);
|
||||||
|
|
||||||
|
await _db.markPointSynced(localId);
|
||||||
|
await _updatePendingCount();
|
||||||
|
} catch (_) {
|
||||||
|
await _db.markPointError(localId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _syncPendingItems() async {
|
||||||
|
if (syncStatus.value == SyncStatus.syncing) return;
|
||||||
|
|
||||||
|
final pending = await _db.getPendingPoints();
|
||||||
|
if (pending.isEmpty) return;
|
||||||
|
|
||||||
|
syncStatus.value = SyncStatus.syncing;
|
||||||
|
|
||||||
|
for (final row in pending) {
|
||||||
|
final localId = row['id'] as int;
|
||||||
|
try {
|
||||||
|
final supaPoint = {
|
||||||
|
'pointNumber': row['point_number'],
|
||||||
|
'gnssNumber': row['gnss_number'],
|
||||||
|
'latitude': row['latitude'],
|
||||||
|
'longitude': row['longitude'],
|
||||||
|
'altitude': row['altitude'],
|
||||||
|
'heightOfGeoid': row['height_of_geoid'],
|
||||||
|
'eovX': row['eov_x'],
|
||||||
|
'eovY': row['eov_y'],
|
||||||
|
'poleHeight': row['pole_height'],
|
||||||
|
'horizontalError': row['horizontal_error'],
|
||||||
|
'verticalError': row['vertical_error'],
|
||||||
|
'description': row['description'],
|
||||||
|
'isDeleted': row['is_deleted'] == 1,
|
||||||
|
'projectId': row['project_id'],
|
||||||
|
};
|
||||||
|
|
||||||
|
await _supabase.from('TerepiSeged_MeasuredPoints').insert(supaPoint);
|
||||||
|
|
||||||
|
await _supabase.from('TerepiSeged_Receiver').update(
|
||||||
|
{'isMeasured': true}).eq('pointNumber', row['point_number']);
|
||||||
|
|
||||||
|
await _db.markPointSynced(localId);
|
||||||
|
} catch (_) {
|
||||||
|
// Következő alkalomra halaszt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await _updatePendingCount();
|
||||||
|
syncStatus.value = SyncStatus.idle;
|
||||||
|
await _db.purgeSyncedPoints();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Segédek ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
Future<void> _updatePendingCount() async {
|
||||||
|
pendingCount.value = await _db.getPendingCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _startConnectivityListener() {
|
||||||
|
_connectivitySub = Connectivity()
|
||||||
|
.onConnectivityChanged
|
||||||
|
.listen((List<ConnectivityResult> results) async {
|
||||||
|
final hasNet = results.any((r) => r != ConnectivityResult.none);
|
||||||
|
if (hasNet && pendingCount.value > 0) {
|
||||||
|
await _syncPendingItems();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> _hasConnectivity() async {
|
||||||
|
final results = await Connectivity().checkConnectivity();
|
||||||
|
return results.any((r) => r != ConnectivityResult.none);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> _isLocalOnlyProject(int? projectId) async {
|
||||||
|
if (projectId == null) return false;
|
||||||
|
final db = await AppDatabase.instance.database;
|
||||||
|
final rows = await db.query(
|
||||||
|
'projects',
|
||||||
|
columns: ['is_local_only'],
|
||||||
|
where: 'id = ?',
|
||||||
|
whereArgs: [projectId],
|
||||||
|
limit: 1,
|
||||||
|
);
|
||||||
|
if (rows.isEmpty) return false;
|
||||||
|
return (rows.first['is_local_only'] as int?) == 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -75,6 +75,7 @@ dependencies:
|
|||||||
|
|
||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
uuid: ^4.5.3
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user