diff --git a/lib/pages/contacts/presentation/views/contact_edit_view.dart b/lib/pages/contacts/presentation/views/contact_edit_view.dart index b5b4635..69ed53f 100644 --- a/lib/pages/contacts/presentation/views/contact_edit_view.dart +++ b/lib/pages/contacts/presentation/views/contact_edit_view.dart @@ -24,7 +24,8 @@ class ContactEditView extends StatefulWidget { class _ContactEditViewState extends State { final _formKey = GlobalKey(); - late final Contact? _original; + Contact? _original; + String? _originalLocalUuid; late final TextEditingController _name; late final TextEditingController _address; @@ -41,7 +42,13 @@ class _ContactEditViewState extends State { @override void initState() { super.initState(); - _original = Get.arguments is Contact ? Get.arguments as Contact : null; + final args = Get.arguments; + if (args is ContactWithState) { + _original = args.contact; + _originalLocalUuid = args.isPending ? args.localUuid : null; + } else if (args is Contact) { + _original = args; + } _name = TextEditingController(text: _original?.name ?? ''); _address = TextEditingController(text: _original?.address ?? ''); _phone = TextEditingController(text: _original?.phone ?? ''); @@ -84,7 +91,8 @@ class _ContactEditViewState extends State { clearLocation: _lat == null, ); - final queued = await ContactService.to.save(contact); + final queued = await ContactService.to + .save(contact, existingLocalUuid: _originalLocalUuid); Get.back(result: queued); // a lista frissítéshez visszakapja if (Get.isRegistered()) { Get.find().load(silent: true); diff --git a/lib/pages/contacts/presentation/views/contacts_view.dart b/lib/pages/contacts/presentation/views/contacts_view.dart index b18cc62..87ccc32 100644 --- a/lib/pages/contacts/presentation/views/contacts_view.dart +++ b/lib/pages/contacts/presentation/views/contacts_view.dart @@ -127,7 +127,7 @@ class ContactsView extends StatelessWidget { item: rows[i], onEdit: () async { final saved = await Get.to(() => const ContactEditView(), - arguments: rows[i].contact); + arguments: rows[i]); if (saved != null) c.load(); }, onDelete: () => _confirmDelete(c, rows[i]), diff --git a/lib/pages/map_survey/presentations/controllers/map_survey_controller.dart b/lib/pages/map_survey/presentations/controllers/map_survey_controller.dart index 1d6eab4..cb25f4c 100644 --- a/lib/pages/map_survey/presentations/controllers/map_survey_controller.dart +++ b/lib/pages/map_survey/presentations/controllers/map_survey_controller.dart @@ -1241,6 +1241,11 @@ class MapSurveyController extends GetxController implements StyleEditable { final contactMarkers = [].obs; + /// Nyers lista a feliratozáshoz (a contactMarkers már kész Marker-eket + /// tartalmaz, abból nem érhető el a név) — ugyanaz a szűrt kör, csak + /// Contact-objektumként, hogy a felirat-réteg is el tudja olvasni. + final contactsWithLocation = [].obs; + Future saveContactAtPoint(LatLng point) async { activeEditTool.value = MapEditTool.none; if (!PermissionService.to.canContacts) return; @@ -1272,20 +1277,21 @@ class MapSurveyController extends GetxController implements StyleEditable { Future _loadContactMarkers() async { if (!PermissionService.to.canContacts) { contactMarkers.clear(); + contactsWithLocation.clear(); return; } final projectId = ProjectService.to.activeProject.value?.uuid; if (projectId == null) { contactMarkers.clear(); + contactsWithLocation.clear(); return; } try { final all = await ContactService.to.listMerged(projectId); - contactMarkers.value = all - .where((c) => c.contact.hasLocation) - .map(_markerFromContact) - .toList(); + final located = all.where((c) => c.contact.hasLocation).toList(); + contactsWithLocation.value = located; + contactMarkers.value = located.map(_markerFromContact).toList(); } catch (_) { // Offline/hiba: a meglévő jelölők maradnak, nem törli ki csendben. } diff --git a/lib/services/app_database.dart b/lib/services/app_database.dart index 71cfa26..212806e 100644 --- a/lib/services/app_database.dart +++ b/lib/services/app_database.dart @@ -1351,7 +1351,8 @@ class AppDatabase { Future insertPendingContact(Map row) async { final db = await database; - await db.insert('contacts_outbox', row); + await db.insert('contacts_outbox', row, + conflictAlgorithm: ConflictAlgorithm.replace); } /// [projectId] NÉLKÜL (a flush-hoz) MINDEN várólistás sort ad vissza — diff --git a/lib/services/app_logger.dart b/lib/services/app_logger.dart index 086d8f5..020003a 100644 --- a/lib/services/app_logger.dart +++ b/lib/services/app_logger.dart @@ -280,6 +280,7 @@ class AppLogger extends GetxService { final userId = Supabase.instance.client.auth.currentUser?.id; await Supabase.instance.client.from('terepi_seged_logs').insert({ + 'type': type, 'tag': tag, 'message': message, 'error_text': error?.toString(), diff --git a/lib/services/contact_export_service.dart b/lib/services/contact_export_service.dart index e87c061..5f35f99 100644 --- a/lib/services/contact_export_service.dart +++ b/lib/services/contact_export_service.dart @@ -40,7 +40,17 @@ class ContactExportService { : v; } - final header = ['Nev', 'Cim', 'Telefon', 'Email', 'Megjegyzes'].join(sep); + final header = [ + 'Nev', + 'Cim', + 'Telefon', + 'Email', + 'Megjegyzes', + 'Szélesség', + 'Hosszúság', + 'EOV_Y', + 'EOV_X' + ].join(sep); final sb = StringBuffer(); sb.write('\uFEFF'); // UTF-8 BOM — az Excel enélkül elrontja az ékezeteket @@ -52,6 +62,9 @@ class ContactExportService { txt(c.phone), txt(c.email), txt(c.note), + c.lat?.toString() ?? '', + c.lon?.toString() ?? '', + c.eovY?.toString() ?? '', ].join(sep)); } diff --git a/lib/services/contact_service.dart b/lib/services/contact_service.dart index 6f38c17..12b873b 100644 --- a/lib/services/contact_service.dart +++ b/lib/services/contact_service.dart @@ -4,6 +4,7 @@ import 'dart:io'; import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:get/get.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; +import 'package:terepi_seged/services/app_logger.dart'; import 'package:uuid/uuid.dart'; import '../models/contact.dart'; @@ -81,16 +82,34 @@ class ContactService extends GetxService { /// Új kapcsolat vagy szerver-oldali frissítés. /// Visszaadja, hogy a művelet OFFLINE pufferbe került-e (true = várólistán). - Future save(Contact c) async { + Future save(Contact c, {String? existingLocalUuid}) async { // Meglévő (szerver-oldali) rekord frissítése CSAK online — lásd korlát. final isUpdate = c.id != null; if (await _isOnline) { try { await _client.from('terepi_seged_contacts').upsert(c.toWriteMap()); + // Ha ez korábban egy PENDING (helyi puffer-) sorból indult, és + // most sikerült felmenni, a régi helyi sort törölni kell — + // különben örökre "függőben" maradna, és a flush() is + // duplikálná. + if (existingLocalUuid != null) { + await _db.deletePendingContact(existingLocalUuid); + } return false; // felment } catch (e) { if (isUpdate) rethrow; // frissítést nem pufferelünk + if (e is PostgrestException) { + // Válasz érkezett a szervertől, de hibás (séma, RLS, megkötés) — + // ez NEM hálózati probléma. Ha csendben pufferbe tennénk, ez a + // sor SOSEM jutna fel, és senki nem venné észre — inkább + // azonnal, láthatóan hibázzon. + AppLogger.e('ContactService.save', + 'Supabase hiba új kapcsolat mentésekor: $e'); + rethrow; + } + // Valódi hálózati/kapcsolati hiba (pl. pillanatnyi kiesés) → + // pufferbe, a flush() majd újrapróbálja. // Új rekord + online hiba (pl. pillanatnyi kiesés) → pufferbe. } } else if (isUpdate) { @@ -101,7 +120,7 @@ class ContactService extends GetxService { // Offline (vagy online-hiba) új rekord → outbox. await _db.insertPendingContact({ - 'local_uuid': _uuid.v4(), + 'local_uuid': existingLocalUuid ?? _uuid.v4(), 'project_id': c.projectId, 'name': c.name.trim(), 'address': c.address.trim(), @@ -173,8 +192,12 @@ class ContactService extends GetxService { // Sikeres felküldés → a lokális példány törölhető. await _db.deletePendingContact(m['local_uuid'] as String); uploaded++; - } catch (_) { - // A sor marad a pufferben, a következő flush újrapróbálja. + } catch (e) { + // A sor marad a pufferben (legközelebb újrapróbáljuk) — de + // naplózzuk, hogy ne maradjon örökre észrevétlen, ha a hiba nem + // hálózati, hanem tartós (pl. séma-eltérés, RLS). + AppLogger.e('ContactService.flush', + 'Kapcsolat feltöltési hiba (local_uuid=${m['local_uuid']}): $e'); } } return uploaded; diff --git a/lib/services/project_service.dart b/lib/services/project_service.dart index e5415df..c09d15b 100644 --- a/lib/services/project_service.dart +++ b/lib/services/project_service.dart @@ -6,6 +6,15 @@ import '../models/project.dart'; import 'app_database.dart'; import 'ts_sync_service.dart'; +class ProjectRequiresLoginException implements Exception { + final String message; + ProjectRequiresLoginException( + [this.message = 'Felhős projekt használatához be kell jelentkezni.']); + + @override + String toString() => message; +} + class ProjectService extends GetxService { static ProjectService get to => Get.find(); @@ -19,6 +28,20 @@ class ProjectService extends GetxService { super.onInit(); await _loadProjects(); await _restoreActiveProject(); + + Supabase.instance.client.auth.onAuthStateChange.listen((data) { + final loggedOut = data.session == null; + final active = activeProject.value; + + if (loggedOut && active != null && !active.isLocalOnly) { + final fallback = projects.firstWhereOrNull((p) => p.isLocalOnly); + if (fallback != null) { + setActiveProject(fallback); + } else { + activeProject.value = null; + } + } + }); } Future _loadProjects() async { @@ -44,6 +67,11 @@ class ProjectService extends GetxService { } Future setActiveProject(Project project) async { + if (!project.isLocalOnly && + Supabase.instance.client.auth.currentUser == null) { + throw ProjectRequiresLoginException(); + } + activeProject.value = project; final prefs = await SharedPreferences.getInstance(); await prefs.setInt('active_project_id', project.id!); @@ -164,13 +192,18 @@ class ProjectService extends GetxService { /// 3. azonnali szinkron, hogy a projekt eddigi adatai lejöjjenek. Future joinSharedProject(Map sharedRow) async { final client = Supabase.instance.client; + final user = client.auth.currentUser; + if (user == null) { + throw ProjectRequiresLoginException( + 'Közös projekthez csatlakozáshoz be kell jelentkezni.'); + } final projectUuid = sharedRow['id'] as String; // 1. Tagság (idempotens: ha már tag, nem hiba). await client.from('terepi_seged_project_members').upsert( { 'project_id': projectUuid, - 'user_id': client.auth.currentUser!.id, + 'user_id': user.id, 'role': 'editor', }, ignoreDuplicates: true, @@ -193,11 +226,13 @@ class ProjectService extends GetxService { /// a lokális adat megmarad (archiválható külön). Future leaveSharedProject(Project project) async { final client = Supabase.instance.client; + final user = client.auth.currentUser; + if (user == null) return; // nincs bejelentkezve — nincs mit tenni await client .from('terepi_seged_project_members') .delete() .eq('project_id', project.uuid) - .eq('user_id', client.auth.currentUser!.id); + .eq('user_id', user.id); } // ═════════════════════════════════════════════════════════════════ diff --git a/lib/widgets/map/note_item_label_layer.dart b/lib/widgets/map/note_item_label_layer.dart index 5996673..9448a6c 100644 --- a/lib/widgets/map/note_item_label_layer.dart +++ b/lib/widgets/map/note_item_label_layer.dart @@ -65,6 +65,18 @@ class NoteItemLabelLayer extends StatelessWidget { } } + // ----- Kapcsolatok - névvel a személy ikon felett + for (final c in controller.contactsWithLocation) { + markers.add(Marker( + point: LatLng(c.contact.lat!, c.contact.lon!), + width: 130, + height: 48, + alignment: Alignment.bottomCenter, + child: Column(mainAxisSize: MainAxisSize.min, children: [ + _LabelBubble(label: c.contact.name, color: Colors.indigo) + ]))); + } + if (markers.isEmpty) return const SizedBox.shrink(); return MarkerLayer(markers: markers); }); diff --git a/lib/widgets/project/project_picker_view.dart b/lib/widgets/project/project_picker_view.dart index 6a9070c..ad7c43b 100644 --- a/lib/widgets/project/project_picker_view.dart +++ b/lib/widgets/project/project_picker_view.dart @@ -184,8 +184,15 @@ class _ProjectTileState extends State<_ProjectTile> { ], ), onTap: () async { - await svc.setActiveProject(project); - Get.back(); + try { + await svc.setActiveProject(project); + Get.back(); + } catch (e) { + Get.snackbar('Nem lehet aktiválni', e.toString(), + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.red, + colorText: Colors.white); + } }, ); });