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'; 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 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.to.active.value) { await StakeoutService.to.load(); } } catch (e) { lastError.value = e.toString(); } finally { isSyncing.value = false; } } Future _pushThenPull() async { await _push(); return _pull(); } // ── PUSH ───────────────────────────────────────────────────────── Future _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'], 'deleted_at': _toUtc(r['deleted_at'] as String?), }) .toList(); await _client.from('ts_stakeout_points').upsert(payload); await _db.markSyncedByUuid( 'stakeout_points', rows.map((r) => r['uuid'] as String).toList()); } // ── PULL ───────────────────────────────────────────────────────── Future _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 .from('ts_stakeout_points') .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'], '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(); } }