Kapcsolat hozzáadása koordinátával a terepbejárás térképen.
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 6s

This commit is contained in:
2026-08-06 16:14:30 +02:00
parent 7f514da38f
commit 950c2f48f1
10 changed files with 239 additions and 21 deletions
+1 -6
View File
@@ -1,6 +1 @@
enum MapEditTool { enum MapEditTool { none, point, line, polygon, contact }
none,
point,
line,
polygon,
}
+38 -7
View File
@@ -14,8 +14,17 @@ class Contact {
final String email; final String email;
final String note; final String note;
final String? createdBy; final String? createdBy;
final DateTime? updatedAt; 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({ const Contact({
this.id, this.id,
required this.projectId, required this.projectId,
@@ -26,15 +35,25 @@ class Contact {
this.note = '', this.note = '',
this.createdBy, this.createdBy,
this.updatedAt, this.updatedAt,
this.lat,
this.lon,
this.eovY,
this.eovX,
}); });
Contact copyWith({ bool get hasLocation => lat != null && lon != null;
String? name,
String? address, Contact copyWith(
String? phone, {String? name,
String? email, String? address,
String? note, String? phone,
}) => String? email,
String? note,
double? lat,
double? lon,
double? eovY,
double? eovX,
bool clearLocation = false}) =>
Contact( Contact(
id: id, id: id,
projectId: projectId, projectId: projectId,
@@ -45,6 +64,10 @@ class Contact {
note: note ?? this.note, note: note ?? this.note,
createdBy: createdBy, createdBy: createdBy,
updatedAt: updatedAt, 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 /// 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(), 'phone': phone.trim(),
'email': email.trim(), 'email': email.trim(),
'note': note.trim(), 'note': note.trim(),
'lat': lat,
'lon': lon,
'eov_y': eovY,
'eov_x': eovX,
}; };
factory Contact.fromMap(Map<String, dynamic> m) => Contact( factory Contact.fromMap(Map<String, dynamic> m) => Contact(
@@ -72,5 +99,9 @@ class Contact {
updatedAt: m['updated_at'] != null updatedAt: m['updated_at'] != null
? DateTime.tryParse(m['updated_at'] as String) ? DateTime.tryParse(m['updated_at'] as String)
: null, : 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(),
); );
} }
@@ -33,6 +33,10 @@ class _ContactEditViewState extends State<ContactEditView> {
late final TextEditingController _note; late final TextEditingController _note;
bool _saving = false; bool _saving = false;
late double? _lat;
late double? _lon;
late double? _eovY;
late double? _eovX;
@override @override
void initState() { void initState() {
@@ -43,6 +47,10 @@ class _ContactEditViewState extends State<ContactEditView> {
_phone = TextEditingController(text: _original?.phone ?? ''); _phone = TextEditingController(text: _original?.phone ?? '');
_email = TextEditingController(text: _original?.email ?? ''); _email = TextEditingController(text: _original?.email ?? '');
_note = TextEditingController(text: _original?.note ?? ''); _note = TextEditingController(text: _original?.note ?? '');
_lat = _original?.lat;
_lon = _original?.lon;
_eovY = _original?.eovY;
_eovX = _original?.eovX;
} }
@override @override
@@ -69,7 +77,13 @@ class _ContactEditViewState extends State<ContactEditView> {
phone: _phone.text, phone: _phone.text,
email: _email.text, email: _email.text,
note: _note.text, note: _note.text,
lat: _lat,
lon: _lon,
eovY: _eovY,
eovX: _eovX,
clearLocation: _lat == null,
); );
final queued = await ContactService.to.save(contact); final queued = await ContactService.to.save(contact);
Get.back(result: queued); // a lista frissítéshez visszakapja Get.back(result: queued); // a lista frissítéshez visszakapja
if (Get.isRegistered<ContactsController>()) { if (Get.isRegistered<ContactsController>()) {
@@ -182,6 +196,27 @@ class _ContactEditViewState extends State<ContactEditView> {
alignLabelWithHint: true, 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), const SizedBox(height: 24),
FilledButton.icon( FilledButton.icon(
onPressed: _saving ? null : _save, onPressed: _saving ? null : _save,
@@ -5,6 +5,7 @@ import 'dart:math';
//import 'dart:math' as math; //import 'dart:math' as math;
import 'package:file_picker/file_picker.dart'; import 'package:file_picker/file_picker.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_map/flutter_map.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/enums/note_type.dart';
import 'package:terepi_seged/eov/convert_coordinate.dart'; import 'package:terepi_seged/eov/convert_coordinate.dart';
import 'package:terepi_seged/eov/eov.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/measured_point.dart';
import 'package:terepi_seged/models/note_item.dart'; import 'package:terepi_seged/models/note_item.dart';
import 'package:terepi_seged/models/point_to_measure.dart'; import 'package:terepi_seged/models/point_to_measure.dart';
import 'package:terepi_seged/models/point_with_description_model.dart'; import 'package:terepi_seged/models/point_with_description_model.dart';
import 'package:shared_preferences/shared_preferences.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/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/controllers/ntrip_settings_controller.dart';
import 'package:terepi_seged/pages/ntrip_settings/presentation/views/ntrip_settings_sheet.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/pages/tracking/presentation/controllers/tracking_controller.dart';
import 'package:terepi_seged/services/app_database.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/coord_converter_service.dart';
import 'package:terepi_seged/services/device_identity_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_connection.dart';
import 'package:terepi_seged/services/gnss/gnss_device_service.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/gnss/gnss_service.dart';
import 'package:terepi_seged/services/ntrip_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/project_service.dart';
import 'package:terepi_seged/services/stakeout_service.dart'; import 'package:terepi_seged/services/stakeout_service.dart';
import 'package:terepi_seged/widgets/map/all_layer_overlay.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.point:
case MapEditTool.contact:
case MapEditTool.none: case MapEditTool.none:
return ''; return '';
} }
@@ -300,10 +306,14 @@ class MapSurveyController extends GetxController implements StyleEditable {
gpsHeightController.text = '1.8'; gpsHeightController.text = '1.8';
ever(ProjectService.to.activeProject, (_) => _loadNoteItems()); ever(ProjectService.to.activeProject, (_) {
_loadNoteItems();
_loadContactMarkers();
});
await _loadNoteItems(); await _loadNoteItems();
await _loadMeasurePoints(); await _loadMeasurePoints();
await _loadContactMarkers();
_subscribeToTeamPosition(); _subscribeToTeamPosition();
} }
@@ -1088,6 +1098,8 @@ class MapSurveyController extends GetxController implements StyleEditable {
return Icons.polyline_outlined; return Icons.polyline_outlined;
case MapEditTool.polygon: case MapEditTool.polygon:
return Icons.border_outer_outlined; return Icons.border_outer_outlined;
case MapEditTool.contact:
return Icons.person_pin_circle_outlined;
case MapEditTool.none: case MapEditTool.none:
return Icons.edit_location_alt_outlined; return Icons.edit_location_alt_outlined;
} }
@@ -1101,6 +1113,8 @@ class MapSurveyController extends GetxController implements StyleEditable {
return 'Vonal rögzítése'; return 'Vonal rögzítése';
case MapEditTool.polygon: case MapEditTool.polygon:
return 'Terület rögzítése'; return 'Terület rögzítése';
case MapEditTool.contact:
return 'Kapcsolat hozzáadása';
case MapEditTool.none: case MapEditTool.none:
return ''; return '';
} }
@@ -1114,6 +1128,8 @@ class MapSurveyController extends GetxController implements StyleEditable {
return 'Hosszan nyomj a térképre a töréspontokhoz'; return 'Hosszan nyomj a térképre a töréspontokhoz';
case MapEditTool.polygon: case MapEditTool.polygon:
return 'Hosszan nyomj a térképre a sarokpontokhoz.'; 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: case MapEditTool.none:
return ''; return '';
} }
@@ -1127,6 +1143,8 @@ class MapSurveyController extends GetxController implements StyleEditable {
return editorPointCount >= 2; return editorPointCount >= 2;
case MapEditTool.polygon: case MapEditTool.polygon:
return editorPointCount >= 3; return editorPointCount >= 3;
case MapEditTool.contact:
return false;
case MapEditTool.none: case MapEditTool.none:
return false; return false;
} }
@@ -1140,6 +1158,8 @@ class MapSurveyController extends GetxController implements StyleEditable {
return 'Kész'; return 'Kész';
case MapEditTool.polygon: case MapEditTool.polygon:
return 'Lezárás'; return 'Lezárás';
case MapEditTool.contact:
return 'Kész';
case MapEditTool.none: case MapEditTool.none:
return 'Kész'; return 'Kész';
} }
@@ -1150,6 +1170,16 @@ class MapSurveyController extends GetxController implements StyleEditable {
activeEditLabel.value = ''; 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() { void startLineTool() {
polygonEditorController.clear(); polygonEditorController.clear();
polygonEditorController.setMode(PolygonEditorMode.line); polygonEditorController.setMode(PolygonEditorMode.line);
@@ -1207,6 +1237,87 @@ class MapSurveyController extends GetxController implements StyleEditable {
//draftPoints.clear(); //draftPoints.clear();
} }
// ── Kapcsolatok a térképen ──────────────────────────────────────────
final contactMarkers = <Marker>[].obs;
Future<void> 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<void> _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) { Marker _markerFromNoteItem(NoteItem item) {
return Marker( return Marker(
key: ValueKey('note_point_${item.id}'), key: ValueKey('note_point_${item.id}'),
@@ -1730,6 +1841,7 @@ class MapSurveyController extends GetxController implements StyleEditable {
break; break;
case MapEditTool.point: case MapEditTool.point:
case MapEditTool.contact:
case MapEditTool.none: case MapEditTool.none:
draftLengthMeters.value = 0.0; draftLengthMeters.value = 0.0;
draftAreaSquareMeters.value = 0.0; draftAreaSquareMeters.value = 0.0;
@@ -64,6 +64,10 @@ class MapSurveyView extends GetView<MapSurveyController> {
} }
return; return;
} }
if (controller.activeEditTool.value == MapEditTool.contact) {
controller.saveContactAtPoint(point);
return;
}
if (controller.activeEditTool.value == MapEditTool.line || if (controller.activeEditTool.value == MapEditTool.line ||
controller.activeEditTool.value == MapEditTool.polygon) { controller.activeEditTool.value == MapEditTool.polygon) {
controller.polygonEditorController.addPoint(point); controller.polygonEditorController.addPoint(point);
@@ -142,6 +146,13 @@ class MapSurveyView extends GetView<MapSurveyController> {
return MarkerLayer(markers: [...controller.pointNotes]); 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(() { Obx(() {
// Vonalak - terepbejárás // Vonalak - terepbejárás
if (controller.mode.value != MapSurveyMode.fieldWalk) { if (controller.mode.value != MapSurveyMode.fieldWalk) {
+16 -1
View File
@@ -46,7 +46,7 @@ class AppDatabase {
final path = p.join(dbDir.path, 'terepi_seged.db'); final path = p.join(dbDir.path, 'terepi_seged.db');
return openDatabase(path, return openDatabase(path,
version: 6, version: 7,
onConfigure: (db) => db.execute('PRAGMA foreign_keys = ON'), onConfigure: (db) => db.execute('PRAGMA foreign_keys = ON'),
onCreate: _onCreate, onCreate: _onCreate,
onUpgrade: _onUpgrade); onUpgrade: _onUpgrade);
@@ -271,7 +271,9 @@ class AppDatabase {
await _createStakeoutTable(db); await _createStakeoutTable(db);
await _createContactsOutbox(db); await _createContactsOutbox(db);
await _addAppInstanceIdColumns(db); await _addAppInstanceIdColumns(db);
await _addContactLocationColumns(db);
await _createVibratorNavTables(db); await _createVibratorNavTables(db);
@@ -312,6 +314,7 @@ class AppDatabase {
await _addAppInstanceIdColumns(db); await _addAppInstanceIdColumns(db);
await _createVibratorNavTables(db); await _createVibratorNavTables(db);
await _addContactLocationColumns(db);
} }
Future<void> _migrateToV4(Database db) async { Future<void> _migrateToV4(Database db) async {
@@ -1327,6 +1330,10 @@ class AppDatabase {
phone TEXT NOT NULL DEFAULT '', phone TEXT NOT NULL DEFAULT '',
email TEXT NOT NULL DEFAULT '', email TEXT NOT NULL DEFAULT '',
note TEXT NOT NULL DEFAULT '', note TEXT NOT NULL DEFAULT '',
lat REAL,
lon REAL,
eov_y REAL,
eov_x REAL,
created_at TEXT NOT NULL created_at TEXT NOT NULL
) )
'''); ''');
@@ -1334,6 +1341,14 @@ class AppDatabase {
'ON contacts_outbox(project_id)'); '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<void> _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<void> insertPendingContact(Map<String, dynamic> row) async { Future<void> insertPendingContact(Map<String, dynamic> row) async {
final db = await database; final db = await database;
await db.insert('contacts_outbox', row); await db.insert('contacts_outbox', row);
+8
View File
@@ -108,6 +108,10 @@ class ContactService extends GetxService {
'phone': c.phone.trim(), 'phone': c.phone.trim(),
'email': c.email.trim(), 'email': c.email.trim(),
'note': c.note.trim(), 'note': c.note.trim(),
'lat': c.lat,
'lon': c.lon,
'eov_y': c.eovY,
'eov_x': c.eovX,
'created_at': DateTime.now().toIso8601String(), 'created_at': DateTime.now().toIso8601String(),
}); });
return true; // várólistán return true; // várólistán
@@ -160,6 +164,10 @@ class ContactService extends GetxService {
'phone': m['phone'], 'phone': m['phone'],
'email': m['email'], 'email': m['email'],
'note': m['note'], 'note': m['note'],
'lat': m['lat'],
'lon': m['lon'],
'eov_y': m['eov_y'],
'eov_x': m['eov_x'],
}, onConflict: 'client_uuid', ignoreDuplicates: true); }, onConflict: 'client_uuid', ignoreDuplicates: true);
// Sikeres felküldés → a lokális példány törölhető. // Sikeres felküldés → a lokális példány törölhető.
@@ -32,6 +32,7 @@ class LabelFieldState extends State<LabelField> {
MapEditTool.point => 'Pont neve...', MapEditTool.point => 'Pont neve...',
MapEditTool.line => 'Vonal neve...', MapEditTool.line => 'Vonal neve...',
MapEditTool.polygon => 'Terület neve...', MapEditTool.polygon => 'Terület neve...',
MapEditTool.contact => 'Felirat ...',
MapEditTool.none => 'Felirat...', MapEditTool.none => 'Felirat...',
}; };
return Column( return Column(
@@ -116,6 +116,8 @@ class LineOrPolygonDrawingContent extends StatelessWidget {
return 'min. 3'; return 'min. 3';
case MapEditTool.point: case MapEditTool.point:
return '1 pont'; return '1 pont';
case MapEditTool.contact:
return '';
case MapEditTool.none: case MapEditTool.none:
return ''; return '';
} }
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:terepi_seged/enums/map_edit_tool.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/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_action.dart';
import 'map_toolbar_divider.dart'; import 'map_toolbar_divider.dart';
@@ -65,6 +66,13 @@ class MapEditCompactToolbar extends StatelessWidget {
selected: activeTool == MapEditTool.polygon, selected: activeTool == MapEditTool.polygon,
onTap: controller.startPolygonTool, 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(), const ToolbarDivider(),
ToolbarAction( ToolbarAction(
icon: Icons.list_alt_outlined, icon: Icons.list_alt_outlined,
@@ -72,12 +80,12 @@ class MapEditCompactToolbar extends StatelessWidget {
selected: false, selected: false,
onTap: () {}, onTap: () {},
), ),
ToolbarAction( // ToolbarAction(
icon: Icons.layers_outlined, // icon: Icons.layers_outlined,
label: 'Rétegek', // label: 'Rétegek',
selected: false, // selected: false,
onTap: () {}, // onTap: () {},
), // ),
], ],
), ),
), ),