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 {
none,
point,
line,
polygon,
}
enum MapEditTool { none, point, line, polygon, contact }
+38 -7
View File
@@ -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<String, dynamic> 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(),
);
}
@@ -33,6 +33,10 @@ class _ContactEditViewState extends State<ContactEditView> {
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<ContactEditView> {
_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<ContactEditView> {
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<ContactsController>()) {
@@ -182,6 +196,27 @@ class _ContactEditViewState extends State<ContactEditView> {
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,
@@ -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 = <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) {
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;
@@ -64,6 +64,10 @@ class MapSurveyView extends GetView<MapSurveyController> {
}
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<MapSurveyController> {
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) {
+16 -1
View File
@@ -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<void> _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<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 {
final db = await database;
await db.insert('contacts_outbox', row);
+8
View File
@@ -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ő.
@@ -32,6 +32,7 @@ class LabelFieldState extends State<LabelField> {
MapEditTool.point => 'Pont neve...',
MapEditTool.line => 'Vonal neve...',
MapEditTool.polygon => 'Terület neve...',
MapEditTool.contact => 'Felirat ...',
MapEditTool.none => 'Felirat...',
};
return Column(
@@ -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 '';
}
@@ -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: () {},
// ),
],
),
),