Files

207 lines
7.9 KiB
Dart
Raw Permalink Normal View History

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';
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;
}
}
// ── 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'],
'app_instance_id': r['app_instance_id'],
'deleted_at': _toUtc(r['deleted_at'] as String?),
})
.toList();
await _client.from('terepi_seged_stakeout_points').upsert(payload);
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
.from('terepi_seged_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'],
'app_instance_id': r['app_instance_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();
}
}