diff --git a/lib/enums/map_edit_tool.dart b/lib/enums/map_edit_tool.dart index 06617f7..fa35b73 100644 --- a/lib/enums/map_edit_tool.dart +++ b/lib/enums/map_edit_tool.dart @@ -1,6 +1 @@ -enum MapEditTool { - none, - point, - line, - polygon, -} +enum MapEditTool { none, point, line, polygon, contact } diff --git a/lib/models/contact.dart b/lib/models/contact.dart index 7ffa41e..87daacf 100644 --- a/lib/models/contact.dart +++ b/lib/models/contact.dart @@ -14,8 +14,17 @@ class Contact { final String email; final String note; final String? createdBy; + final DateTime? updatedAt; + /// Opcionális helyszín — WGS84 (lat/lon) és EOV (eovY/eovX) is tárolva, + /// akárcsak a többi geometria-típusnál. Csak akkor van kitöltve, ha a + /// térképi "Kapcsolat" eszközzel vették fel, vagy utólag hozzárendelték. + final double? lat; + final double? lon; + final double? eovY; + final double? eovX; + const Contact({ this.id, required this.projectId, @@ -26,15 +35,25 @@ class Contact { this.note = '', this.createdBy, this.updatedAt, + this.lat, + this.lon, + this.eovY, + this.eovX, }); - Contact copyWith({ - String? name, - String? address, - String? phone, - String? email, - String? note, - }) => + bool get hasLocation => lat != null && lon != null; + + Contact copyWith( + {String? name, + String? address, + String? phone, + String? email, + String? note, + double? lat, + double? lon, + double? eovY, + double? eovX, + bool clearLocation = false}) => Contact( id: id, projectId: projectId, @@ -45,6 +64,10 @@ class Contact { note: note ?? this.note, createdBy: createdBy, updatedAt: updatedAt, + lat: clearLocation ? null : (lat ?? this.lat), + lon: clearLocation ? null : (lon ?? this.lon), + eovX: clearLocation ? null : (eovX ?? this.eovX), + eovY: clearLocation ? null : (eovY ?? this.eovY), ); /// Beszúráshoz/frissítéshez — az id-t csak akkor küldjük, ha van @@ -58,6 +81,10 @@ class Contact { 'phone': phone.trim(), 'email': email.trim(), 'note': note.trim(), + 'lat': lat, + 'lon': lon, + 'eov_y': eovY, + 'eov_x': eovX, }; factory Contact.fromMap(Map m) => Contact( @@ -72,5 +99,9 @@ class Contact { updatedAt: m['updated_at'] != null ? DateTime.tryParse(m['updated_at'] as String) : null, + lat: (m['lat'] as num?)?.toDouble(), + lon: (m['lon'] as num?)?.toDouble(), + eovY: (m['eov_y'] as num?)?.toDouble(), + eovX: (m['eov_x'] as num?)?.toDouble(), ); } diff --git a/lib/pages/contacts/presentation/views/contact_edit_view.dart b/lib/pages/contacts/presentation/views/contact_edit_view.dart index 2dabc9b..b5b4635 100644 --- a/lib/pages/contacts/presentation/views/contact_edit_view.dart +++ b/lib/pages/contacts/presentation/views/contact_edit_view.dart @@ -33,6 +33,10 @@ class _ContactEditViewState extends State { late final TextEditingController _note; bool _saving = false; + late double? _lat; + late double? _lon; + late double? _eovY; + late double? _eovX; @override void initState() { @@ -43,6 +47,10 @@ class _ContactEditViewState extends State { _phone = TextEditingController(text: _original?.phone ?? ''); _email = TextEditingController(text: _original?.email ?? ''); _note = TextEditingController(text: _original?.note ?? ''); + _lat = _original?.lat; + _lon = _original?.lon; + _eovY = _original?.eovY; + _eovX = _original?.eovX; } @override @@ -69,7 +77,13 @@ class _ContactEditViewState extends State { phone: _phone.text, email: _email.text, note: _note.text, + lat: _lat, + lon: _lon, + eovY: _eovY, + eovX: _eovX, + clearLocation: _lat == null, ); + final queued = await ContactService.to.save(contact); Get.back(result: queued); // a lista frissítéshez visszakapja if (Get.isRegistered()) { @@ -182,6 +196,27 @@ class _ContactEditViewState extends State { alignLabelWithHint: true, ), ), + const SizedBox(height: 12), + if (_lat != null && _lon != null) + Card( + color: Colors.indigo.withOpacity(0.06), + child: ListTile( + leading: const Icon(Icons.location_on, color: Colors.indigo), + title: Text( + '${_lat!.toStringAsFixed(6)}, ${_lon!.toStringAsFixed(6)}'), + subtitle: const Text('Helyszín rögzítve'), + trailing: IconButton( + icon: const Icon(Icons.close), + tooltip: 'Helyszín törlése', + onPressed: () => setState(() { + _lat = null; + _lon = null; + _eovY = null; + _eovX = null; + }), + ), + ), + ), const SizedBox(height: 24), FilledButton.icon( onPressed: _saving ? null : _save, 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 909b4e4..1d6eab4 100644 --- a/lib/pages/map_survey/presentations/controllers/map_survey_controller.dart +++ b/lib/pages/map_survey/presentations/controllers/map_survey_controller.dart @@ -5,6 +5,7 @@ import 'dart:math'; //import 'dart:math' as math; import 'package:file_picker/file_picker.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_map/flutter_map.dart'; @@ -31,22 +32,26 @@ import 'package:terepi_seged/enums/map_survey_mode.dart'; import 'package:terepi_seged/enums/note_type.dart'; import 'package:terepi_seged/eov/convert_coordinate.dart'; import 'package:terepi_seged/eov/eov.dart'; +import 'package:terepi_seged/models/contact.dart'; import 'package:terepi_seged/models/measured_point.dart'; import 'package:terepi_seged/models/note_item.dart'; import 'package:terepi_seged/models/point_to_measure.dart'; import 'package:terepi_seged/models/point_with_description_model.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import 'package:terepi_seged/pages/contacts/presentation/views/contact_edit_view.dart'; import 'package:terepi_seged/pages/map_survey/presentations/views/measured_points_table_dialog.dart'; import 'package:terepi_seged/pages/ntrip_settings/presentation/controllers/ntrip_settings_controller.dart'; import 'package:terepi_seged/pages/ntrip_settings/presentation/views/ntrip_settings_sheet.dart'; import 'package:terepi_seged/pages/tracking/presentation/controllers/tracking_controller.dart'; import 'package:terepi_seged/services/app_database.dart'; +import 'package:terepi_seged/services/contact_service.dart'; import 'package:terepi_seged/services/coord_converter_service.dart'; import 'package:terepi_seged/services/device_identity_service.dart'; import 'package:terepi_seged/services/gnss/gnss_connection.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/ntrip_service.dart'; +import 'package:terepi_seged/services/permission_service.dart'; import 'package:terepi_seged/services/project_service.dart'; import 'package:terepi_seged/services/stakeout_service.dart'; import 'package:terepi_seged/widgets/map/all_layer_overlay.dart'; @@ -237,6 +242,7 @@ class MapSurveyController extends GetxController implements StyleEditable { ); case MapEditTool.point: + case MapEditTool.contact: case MapEditTool.none: return ''; } @@ -300,10 +306,14 @@ class MapSurveyController extends GetxController implements StyleEditable { gpsHeightController.text = '1.8'; - ever(ProjectService.to.activeProject, (_) => _loadNoteItems()); + ever(ProjectService.to.activeProject, (_) { + _loadNoteItems(); + _loadContactMarkers(); + }); await _loadNoteItems(); await _loadMeasurePoints(); + await _loadContactMarkers(); _subscribeToTeamPosition(); } @@ -1088,6 +1098,8 @@ class MapSurveyController extends GetxController implements StyleEditable { return Icons.polyline_outlined; case MapEditTool.polygon: return Icons.border_outer_outlined; + case MapEditTool.contact: + return Icons.person_pin_circle_outlined; case MapEditTool.none: return Icons.edit_location_alt_outlined; } @@ -1101,6 +1113,8 @@ class MapSurveyController extends GetxController implements StyleEditable { return 'Vonal rögzítése'; case MapEditTool.polygon: return 'Terület rögzítése'; + case MapEditTool.contact: + return 'Kapcsolat hozzáadása'; case MapEditTool.none: return ''; } @@ -1114,6 +1128,8 @@ class MapSurveyController extends GetxController implements StyleEditable { return 'Hosszan nyomj a térképre a töréspontokhoz'; case MapEditTool.polygon: return 'Hosszan nyomj a térképre a sarokpontokhoz.'; + case MapEditTool.contact: + return 'Hosszan nyomj a térképre a kapcsolat helyéhez.'; case MapEditTool.none: return ''; } @@ -1127,6 +1143,8 @@ class MapSurveyController extends GetxController implements StyleEditable { return editorPointCount >= 2; case MapEditTool.polygon: return editorPointCount >= 3; + case MapEditTool.contact: + return false; case MapEditTool.none: return false; } @@ -1140,6 +1158,8 @@ class MapSurveyController extends GetxController implements StyleEditable { return 'Kész'; case MapEditTool.polygon: return 'Lezárás'; + case MapEditTool.contact: + return 'Kész'; case MapEditTool.none: return 'Kész'; } @@ -1150,6 +1170,16 @@ class MapSurveyController extends GetxController implements StyleEditable { activeEditLabel.value = ''; } + void startContactTool() { + if (!PermissionService.to.canContacts) { + Get.snackbar( + 'Nincs jogosultság', 'Kapcsolatok kezeléséhez jogosultság szükséges.', + snackPosition: SnackPosition.BOTTOM); + return; + } + activeEditTool.value = MapEditTool.contact; + } + void startLineTool() { polygonEditorController.clear(); polygonEditorController.setMode(PolygonEditorMode.line); @@ -1207,6 +1237,87 @@ class MapSurveyController extends GetxController implements StyleEditable { //draftPoints.clear(); } + // ── Kapcsolatok a térképen ────────────────────────────────────────── + + final contactMarkers = [].obs; + + Future saveContactAtPoint(LatLng point) async { + activeEditTool.value = MapEditTool.none; + if (!PermissionService.to.canContacts) return; + + final projectId = ProjectService.to.activeProject.value?.uuid; + if (projectId == null) return; + + final eov = CoordConverterService.to.wgsToEovPoint( + point.longitude, + point.latitude, + ); + + final draft = Contact( + projectId: projectId, + name: '', + lat: point.latitude, + lon: point.longitude, + eovY: eov.x, + eovX: eov.y, + ); + + final result = + await Get.to(() => const ContactEditView(), arguments: draft); + if (result != null) { + await _loadContactMarkers(); + } + } + + Future _loadContactMarkers() async { + if (!PermissionService.to.canContacts) { + contactMarkers.clear(); + return; + } + + final projectId = ProjectService.to.activeProject.value?.uuid; + if (projectId == null) { + contactMarkers.clear(); + return; + } + try { + final all = await ContactService.to.listMerged(projectId); + contactMarkers.value = all + .where((c) => c.contact.hasLocation) + .map(_markerFromContact) + .toList(); + } catch (_) { + // Offline/hiba: a meglévő jelölők maradnak, nem törli ki csendben. + } + } + + Marker _markerFromContact(ContactWithState c) { + return Marker( + key: ValueKey('contact_${c.contact.id ?? c.localUuid}'), + point: LatLng(c.contact.lat!, c.contact.lon!), + width: 34, + height: 34, + child: GestureDetector( + onTap: () async { + final result = + await Get.to(() => const ContactEditView(), arguments: c.contact); + if (result != null) await _loadContactMarkers(); + }, + child: Container( + decoration: BoxDecoration( + color: c.isPending ? Colors.orange : Colors.indigo, + shape: BoxShape.circle, + border: Border.all(color: Colors.white, width: 2), + boxShadow: const [ + BoxShadow(color: Colors.black26, blurRadius: 4), + ], + ), + child: const Icon(Icons.person, color: Colors.white, size: 20), + ), + ), + ); + } + Marker _markerFromNoteItem(NoteItem item) { return Marker( key: ValueKey('note_point_${item.id}'), @@ -1730,6 +1841,7 @@ class MapSurveyController extends GetxController implements StyleEditable { break; case MapEditTool.point: + case MapEditTool.contact: case MapEditTool.none: draftLengthMeters.value = 0.0; draftAreaSquareMeters.value = 0.0; diff --git a/lib/pages/map_survey/presentations/views/map_survey_view.dart b/lib/pages/map_survey/presentations/views/map_survey_view.dart index e9680f7..a064d86 100644 --- a/lib/pages/map_survey/presentations/views/map_survey_view.dart +++ b/lib/pages/map_survey/presentations/views/map_survey_view.dart @@ -64,6 +64,10 @@ class MapSurveyView extends GetView { } return; } + if (controller.activeEditTool.value == MapEditTool.contact) { + controller.saveContactAtPoint(point); + return; + } if (controller.activeEditTool.value == MapEditTool.line || controller.activeEditTool.value == MapEditTool.polygon) { controller.polygonEditorController.addPoint(point); @@ -142,6 +146,13 @@ class MapSurveyView extends GetView { return MarkerLayer(markers: [...controller.pointNotes]); }), + Obx(() { + // Kapcsolatok - terepi bejárás + if (controller.mode.value != MapSurveyMode.fieldWalk) { + return const SizedBox.shrink(); + } + return MarkerLayer(markers: [...controller.contactMarkers]); + }), Obx(() { // Vonalak - terepbejárás if (controller.mode.value != MapSurveyMode.fieldWalk) { diff --git a/lib/services/app_database.dart b/lib/services/app_database.dart index e5582a5..71cfa26 100644 --- a/lib/services/app_database.dart +++ b/lib/services/app_database.dart @@ -46,7 +46,7 @@ class AppDatabase { final path = p.join(dbDir.path, 'terepi_seged.db'); return openDatabase(path, - version: 6, + version: 7, onConfigure: (db) => db.execute('PRAGMA foreign_keys = ON'), onCreate: _onCreate, onUpgrade: _onUpgrade); @@ -271,7 +271,9 @@ class AppDatabase { await _createStakeoutTable(db); await _createContactsOutbox(db); + await _addAppInstanceIdColumns(db); + await _addContactLocationColumns(db); await _createVibratorNavTables(db); @@ -312,6 +314,7 @@ class AppDatabase { await _addAppInstanceIdColumns(db); await _createVibratorNavTables(db); + await _addContactLocationColumns(db); } Future _migrateToV4(Database db) async { @@ -1327,6 +1330,10 @@ class AppDatabase { phone TEXT NOT NULL DEFAULT '', email TEXT NOT NULL DEFAULT '', note TEXT NOT NULL DEFAULT '', + lat REAL, + lon REAL, + eov_y REAL, + eov_x REAL, created_at TEXT NOT NULL ) '''); @@ -1334,6 +1341,14 @@ class AppDatabase { 'ON contacts_outbox(project_id)'); } + /// Idempotens oszlop-pótlás, ha a contacts_outbox már létezik (korábbi + /// telepítéseknél) — ugyanaz a minta, mint az app_instance_id-nél. + Future _addContactLocationColumns(Database db) async { + for (final col in ['lat', 'lon', 'eov_y', 'eov_x']) { + await _tryExec(db, 'ALTER TABLE contacts_outbox ADD COLUMN $col REAL'); + } + } + Future insertPendingContact(Map row) async { final db = await database; await db.insert('contacts_outbox', row); diff --git a/lib/services/contact_service.dart b/lib/services/contact_service.dart index c4f9d84..6f38c17 100644 --- a/lib/services/contact_service.dart +++ b/lib/services/contact_service.dart @@ -108,6 +108,10 @@ class ContactService extends GetxService { 'phone': c.phone.trim(), 'email': c.email.trim(), 'note': c.note.trim(), + 'lat': c.lat, + 'lon': c.lon, + 'eov_y': c.eovY, + 'eov_x': c.eovX, 'created_at': DateTime.now().toIso8601String(), }); return true; // várólistán @@ -160,6 +164,10 @@ class ContactService extends GetxService { 'phone': m['phone'], 'email': m['email'], 'note': m['note'], + 'lat': m['lat'], + 'lon': m['lon'], + 'eov_y': m['eov_y'], + 'eov_x': m['eov_x'], }, onConflict: 'client_uuid', ignoreDuplicates: true); // Sikeres felküldés → a lokális példány törölhető. diff --git a/lib/widgets/map_edit_tools/label_field.dart b/lib/widgets/map_edit_tools/label_field.dart index b83b82d..efa1b48 100644 --- a/lib/widgets/map_edit_tools/label_field.dart +++ b/lib/widgets/map_edit_tools/label_field.dart @@ -32,6 +32,7 @@ class LabelFieldState extends State { MapEditTool.point => 'Pont neve...', MapEditTool.line => 'Vonal neve...', MapEditTool.polygon => 'Terület neve...', + MapEditTool.contact => 'Felirat ...', MapEditTool.none => 'Felirat...', }; return Column( diff --git a/lib/widgets/map_edit_tools/map_edit_line_or_polygon_drawing_content.dart b/lib/widgets/map_edit_tools/map_edit_line_or_polygon_drawing_content.dart index 7c57fe3..5591445 100644 --- a/lib/widgets/map_edit_tools/map_edit_line_or_polygon_drawing_content.dart +++ b/lib/widgets/map_edit_tools/map_edit_line_or_polygon_drawing_content.dart @@ -116,6 +116,8 @@ class LineOrPolygonDrawingContent extends StatelessWidget { return 'min. 3'; case MapEditTool.point: return '1 pont'; + case MapEditTool.contact: + return ''; case MapEditTool.none: return ''; } diff --git a/lib/widgets/map_edit_tools/map_edit_toolbar.dart b/lib/widgets/map_edit_tools/map_edit_toolbar.dart index 5fa5ff4..13055ae 100644 --- a/lib/widgets/map_edit_tools/map_edit_toolbar.dart +++ b/lib/widgets/map_edit_tools/map_edit_toolbar.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:terepi_seged/enums/map_edit_tool.dart'; import 'package:terepi_seged/pages/map_survey/presentations/controllers/map_survey_controller.dart'; +import 'package:terepi_seged/services/permission_service.dart'; import 'map_toolbar_action.dart'; import 'map_toolbar_divider.dart'; @@ -65,6 +66,13 @@ class MapEditCompactToolbar extends StatelessWidget { selected: activeTool == MapEditTool.polygon, onTap: controller.startPolygonTool, ), + if (PermissionService.to.canContacts) + ToolbarAction( + icon: Icons.person_pin_circle_outlined, + label: 'Kapcsolat', + selected: activeTool == MapEditTool.contact, + onTap: controller.startContactTool, + ), const ToolbarDivider(), ToolbarAction( icon: Icons.list_alt_outlined, @@ -72,12 +80,12 @@ class MapEditCompactToolbar extends StatelessWidget { selected: false, onTap: () {}, ), - ToolbarAction( - icon: Icons.layers_outlined, - label: 'Rétegek', - selected: false, - onTap: () {}, - ), + // ToolbarAction( + // icon: Icons.layers_outlined, + // label: 'Rétegek', + // selected: false, + // onTap: () {}, + // ), ], ), ),