2026-07-06 14:26:16 +02:00
|
|
|
import 'package:get/get.dart';
|
|
|
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
|
|
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
|
|
|
|
import 'package:terepi_seged/services/app_database.dart';
|
2026-07-12 02:14:49 +02:00
|
|
|
import 'package:terepi_seged/services/app_logger.dart';
|
2026-07-06 14:26:16 +02:00
|
|
|
|
|
|
|
|
import 'stakeout_service.dart';
|
|
|
|
|
|
|
|
|
|
/// Kitűzési pontok Supabase-szinkronja — a meglévő outbox-mintára.
|
|
|
|
|
///
|
|
|
|
|
/// A TsSyncService hívja minden ciklus végén (mint a LayerSyncService-t),
|
|
|
|
|
/// de kézzel (syncNow) is futtatható. PUSH: a 'pending' sorok uuid-ra
|
|
|
|
|
/// upserttel (idempotens); PULL: projektenként inkrementálisan
|
|
|
|
|
/// (updated_at > kurzor). A csak-lokális projektek kizárását az
|
|
|
|
|
/// AppDatabase.pendingStakeoutPoints() végzi SQL-szinten, a pull pedig
|
|
|
|
|
/// csak a nem-lokális projekteken iterál.
|
|
|
|
|
///
|
|
|
|
|
/// SAJÁT kurzort használ (SharedPreferences, projektenként), mert a
|
|
|
|
|
/// TsSyncService a projects.last_pulled_at kurzort a saját pullja végén
|
|
|
|
|
/// már előretolta — közös kurzorral a kitűzés-sorok kimaradhatnának.
|
|
|
|
|
///
|
|
|
|
|
/// Csapat-láthatóság: a letöltött sorok a lokális DB-be kerülnek, a
|
|
|
|
|
/// StakeoutService.load() frissíti a listát, így a MÁS ESZKÖZÖN kitűzött
|
|
|
|
|
/// pont a térképen magától zöldre vált.
|
|
|
|
|
class StakeoutSyncService extends GetxService {
|
|
|
|
|
static StakeoutSyncService get to => Get.find();
|
|
|
|
|
|
|
|
|
|
final isSyncing = false.obs;
|
|
|
|
|
final lastError = ''.obs;
|
|
|
|
|
|
|
|
|
|
SupabaseClient get _client => Supabase.instance.client;
|
|
|
|
|
AppDatabase get _db => AppDatabase.instance;
|
|
|
|
|
|
|
|
|
|
Future<void> sync() async {
|
|
|
|
|
if (_client.auth.currentUser == null) return;
|
|
|
|
|
if (isSyncing.value) return;
|
|
|
|
|
isSyncing.value = true;
|
|
|
|
|
lastError.value = '';
|
|
|
|
|
try {
|
|
|
|
|
final pulledAny = await _pushThenPull();
|
|
|
|
|
// Ha aktív a kitűzés mód és jött új adat, frissítjük a nézetet —
|
|
|
|
|
// itt válik láthatóvá a többiek munkája (zöld markerek).
|
|
|
|
|
if (pulledAny &&
|
|
|
|
|
Get.isRegistered<StakeoutService>() &&
|
|
|
|
|
StakeoutService.to.active.value) {
|
|
|
|
|
await StakeoutService.to.load();
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
lastError.value = e.toString();
|
|
|
|
|
} finally {
|
|
|
|
|
isSyncing.value = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Future<bool> _pushThenPull() async {
|
2026-07-12 02:14:49 +02:00
|
|
|
try {
|
|
|
|
|
await _push().timeout(const Duration(seconds: 25));
|
|
|
|
|
} catch (e, s) {
|
|
|
|
|
lastError.value = 'kitűzés push: $e';
|
|
|
|
|
AppLogger.e('StackeoutSyncService - _pushThenPull', lastError.value,
|
|
|
|
|
error: e, stack: s);
|
|
|
|
|
}
|
|
|
|
|
try {
|
|
|
|
|
return _pull().timeout(const Duration(seconds: 25));
|
|
|
|
|
} catch (e, s) {
|
|
|
|
|
lastError.value = lastError.value.isEmpty
|
|
|
|
|
? 'kitűzés pull: $e'
|
|
|
|
|
: '${lastError.value} · kitűzés pull: $e';
|
|
|
|
|
AppLogger.e('StackeoutSyncService - _pushThenPull', lastError.value,
|
|
|
|
|
error: e, stack: s);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2026-07-06 14:26:16 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── PUSH ─────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
Future<void> _push() async {
|
|
|
|
|
final rows = await _db.pendingStakeoutPoints();
|
|
|
|
|
if (rows.isEmpty) return;
|
|
|
|
|
|
|
|
|
|
final payload = rows
|
|
|
|
|
.map((r) => {
|
|
|
|
|
'id': r['uuid'],
|
|
|
|
|
'project_id': r['project_uuid'],
|
|
|
|
|
'line_id': r['line_id'],
|
|
|
|
|
'station': r['station'],
|
|
|
|
|
'name': r['name'],
|
|
|
|
|
'point_type': r['point_type'],
|
|
|
|
|
'source': r['source'],
|
|
|
|
|
'plan_eov_y': r['plan_eov_y'],
|
|
|
|
|
'plan_eov_x': r['plan_eov_x'],
|
|
|
|
|
'plan_eov_z': r['plan_eov_z'],
|
|
|
|
|
'plan_lat': r['plan_lat'],
|
|
|
|
|
'plan_lon': r['plan_lon'],
|
|
|
|
|
'status': r['status'],
|
|
|
|
|
'measured_eov_y': r['measured_eov_y'],
|
|
|
|
|
'measured_eov_x': r['measured_eov_x'],
|
|
|
|
|
'measured_eov_z': r['measured_eov_z'],
|
|
|
|
|
'measured_lat': r['measured_lat'],
|
|
|
|
|
'measured_lon': r['measured_lon'],
|
|
|
|
|
'dev_inline': r['dev_inline'],
|
|
|
|
|
'dev_crossline': r['dev_crossline'],
|
|
|
|
|
'dev_dz': r['dev_dz'],
|
|
|
|
|
'fix_quality': r['fix_quality'],
|
|
|
|
|
'accuracy': r['accuracy'],
|
|
|
|
|
'tilt_deg': r['tilt_deg'],
|
|
|
|
|
'staked_at': _toUtc(r['staked_at'] as String?),
|
|
|
|
|
'is_offset': (r['is_offset'] as int? ?? 0) == 1,
|
|
|
|
|
'parent_uuid': r['parent_uuid'],
|
|
|
|
|
'offset_dist': r['offset_dist'],
|
|
|
|
|
'offset_bearing': r['offset_bearing'],
|
|
|
|
|
'device_id': r['device_id'],
|
2026-07-08 23:29:22 +02:00
|
|
|
'app_instance_id': r['app_instance_id'],
|
2026-07-06 14:26:16 +02:00
|
|
|
'deleted_at': _toUtc(r['deleted_at'] as String?),
|
|
|
|
|
})
|
|
|
|
|
.toList();
|
|
|
|
|
|
2026-07-07 03:25:51 +02:00
|
|
|
await _client.from('terepi_seged_stakeout_points').upsert(payload);
|
2026-07-06 14:26:16 +02:00
|
|
|
await _db.markSyncedByUuid(
|
|
|
|
|
'stakeout_points', rows.map((r) => r['uuid'] as String).toList());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── PULL ─────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
Future<bool> _pull() async {
|
|
|
|
|
final prefs = await SharedPreferences.getInstance();
|
|
|
|
|
var pulledAny = false;
|
|
|
|
|
|
|
|
|
|
final projects = await _db.listProjects();
|
|
|
|
|
for (final project in projects.where((p) => !p.isLocalOnly)) {
|
|
|
|
|
final cursorKey = 'stakeout_cursor_${project.uuid}';
|
|
|
|
|
final cursor = prefs.getString(cursorKey) ?? '1970-01-01T00:00:00Z';
|
|
|
|
|
var maxSeen = cursor;
|
|
|
|
|
|
|
|
|
|
final rows = await _client
|
2026-07-07 03:25:51 +02:00
|
|
|
.from('terepi_seged_stakeout_points')
|
2026-07-06 14:26:16 +02:00
|
|
|
.select()
|
|
|
|
|
.eq('project_id', project.uuid)
|
|
|
|
|
.gt('updated_at', cursor)
|
|
|
|
|
.order('updated_at', ascending: true)
|
|
|
|
|
.limit(1000);
|
|
|
|
|
|
|
|
|
|
for (final r in rows) {
|
|
|
|
|
final ts = r['updated_at'] as String?;
|
|
|
|
|
if (ts != null && DateTime.parse(ts).isAfter(DateTime.parse(maxSeen))) {
|
|
|
|
|
maxSeen = ts;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Az AppDatabase generikus applyRemoteRow-ja: uuid alapján
|
|
|
|
|
// beszúr/frissít, a lokális 'pending' sort nem írja felül
|
|
|
|
|
// (a push előbb futott).
|
|
|
|
|
await _db.applyRemoteRow('stakeout_points', {
|
|
|
|
|
'uuid': r['id'],
|
|
|
|
|
'project_id': project.id,
|
|
|
|
|
'line_id': r['line_id'] ?? '',
|
|
|
|
|
'station': r['station'],
|
|
|
|
|
'name': r['name'] ?? '${r['station']}',
|
|
|
|
|
'point_type': r['point_type'] ?? 'geofon',
|
|
|
|
|
'source': r['source'] ?? 'supabase',
|
|
|
|
|
'plan_eov_y': r['plan_eov_y'],
|
|
|
|
|
'plan_eov_x': r['plan_eov_x'],
|
|
|
|
|
'plan_eov_z': r['plan_eov_z'],
|
|
|
|
|
'plan_lat': r['plan_lat'],
|
|
|
|
|
'plan_lon': r['plan_lon'],
|
|
|
|
|
'status': r['status'] ?? 'pending',
|
|
|
|
|
'measured_eov_y': r['measured_eov_y'],
|
|
|
|
|
'measured_eov_x': r['measured_eov_x'],
|
|
|
|
|
'measured_eov_z': r['measured_eov_z'],
|
|
|
|
|
'measured_lat': r['measured_lat'],
|
|
|
|
|
'measured_lon': r['measured_lon'],
|
|
|
|
|
'dev_inline': r['dev_inline'],
|
|
|
|
|
'dev_crossline': r['dev_crossline'],
|
|
|
|
|
'dev_dz': r['dev_dz'],
|
|
|
|
|
'fix_quality': r['fix_quality'],
|
|
|
|
|
'accuracy': r['accuracy'],
|
|
|
|
|
'tilt_deg': r['tilt_deg'],
|
|
|
|
|
'staked_at': r['staked_at'],
|
|
|
|
|
'is_offset': (r['is_offset'] == true) ? 1 : 0,
|
|
|
|
|
'parent_uuid': r['parent_uuid'],
|
|
|
|
|
'offset_dist': r['offset_dist'],
|
|
|
|
|
'offset_bearing': r['offset_bearing'],
|
|
|
|
|
'created_by': r['created_by'],
|
|
|
|
|
'device_id': r['device_id'],
|
2026-07-08 23:29:22 +02:00
|
|
|
'app_instance_id': r['app_instance_id'],
|
2026-07-06 14:26:16 +02:00
|
|
|
'created_at': r['created_at'] ?? DateTime.now().toIso8601String(),
|
|
|
|
|
'updated_at': r['updated_at'],
|
|
|
|
|
'deleted_at': r['deleted_at'],
|
|
|
|
|
'sync_status': 'synced',
|
|
|
|
|
});
|
|
|
|
|
pulledAny = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (maxSeen != cursor) {
|
|
|
|
|
await prefs.setString(cursorKey, maxSeen);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return pulledAny;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// A lokális, zóna nélküli időbélyeg a Postgres-ben UTC-nek számítana
|
|
|
|
|
/// → küldés előtt explicit UTC-re konvertálunk.
|
|
|
|
|
String? _toUtc(String? localIso) {
|
|
|
|
|
if (localIso == null || localIso.isEmpty) return null;
|
|
|
|
|
return DateTime.parse(localIso).toUtc().toIso8601String();
|
|
|
|
|
}
|
|
|
|
|
}
|