MobilApp/lib/pages/map_survey/presentations/views/measured_points_sheet.dart

363 lines
12 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Bemért pontok listája:
// - Pont neve + EOV Y/X
// - Törlés swipe-pal
// - CSV export (share_plus)
// - SQLite alapú (nem memória)
import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:path_provider/path_provider.dart';
import 'package:share_plus/share_plus.dart';
import '../controllers/map_survey_controller.dart';
import '../../../../../../models/measured_point.dart';
import '../../../../../../services/app_database.dart';
import '../../../../../../services/project_service.dart';
class MeasuredPointsSheet extends StatefulWidget {
const MeasuredPointsSheet({super.key});
@override
State<MeasuredPointsSheet> createState() => _MeasuredPointsSheetState();
}
class _MeasuredPointsSheetState extends State<MeasuredPointsSheet> {
List<MeasuredPoint> _points = [];
bool _loading = true;
@override
void initState() {
super.initState();
_load();
}
Future<void> _load() async {
final projectId = ProjectService.to.activeProjectId;
if (projectId == null) {
setState(() => _loading = false);
return;
}
final pts = await AppDatabase.instance.listMeasuredPoints(projectId);
if (mounted)
setState(() {
_points = pts;
_loading = false;
});
}
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
return Container(
decoration: BoxDecoration(
color: cs.surface,
borderRadius: const BorderRadius.vertical(top: Radius.circular(16)),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.15),
blurRadius: 16,
offset: const Offset(0, -4)),
],
),
child: Column(
children: [
// ── Handle ───────────────────────────────────────────────
Center(
child: Container(
margin: const EdgeInsets.symmetric(vertical: 10),
width: 40,
height: 4,
decoration: BoxDecoration(
color: Colors.grey.shade300,
borderRadius: BorderRadius.circular(2)),
),
),
// ── Fejléc ───────────────────────────────────────────────
Padding(
padding: const EdgeInsets.fromLTRB(20, 0, 12, 8),
child: Row(children: [
const Icon(Icons.location_on, size: 20),
const SizedBox(width: 8),
Text('Bemért pontok',
style: const TextStyle(
fontSize: 18, fontWeight: FontWeight.w600)),
const SizedBox(width: 6),
if (!_loading)
Container(
padding:
const EdgeInsets.symmetric(horizontal: 7, vertical: 2),
decoration: BoxDecoration(
color: cs.primaryContainer,
borderRadius: BorderRadius.circular(10),
),
child: Text('${_points.length}',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: cs.onPrimaryContainer)),
),
const Spacer(),
// CSV export
if (_points.isNotEmpty)
IconButton(
icon: const Icon(Icons.download_outlined),
tooltip: 'CSV export',
onPressed: _exportCsv,
),
// Összes törlése
if (_points.isNotEmpty)
IconButton(
icon: Icon(Icons.delete_sweep_outlined,
color: Colors.red.shade300),
tooltip: 'Összes törlése',
onPressed: _deleteAll,
),
]),
),
const Divider(height: 1),
// ── Lista ─────────────────────────────────────────────────
Expanded(
child: _loading
? const Center(child: CircularProgressIndicator())
: _points.isEmpty
? _emptyState()
: Column(children: [
// Fejléc sor
_TableHeader(),
const Divider(height: 1),
Expanded(
child: ListView.separated(
itemCount: _points.length,
separatorBuilder: (_, __) =>
const Divider(height: 1),
itemBuilder: (_, i) => _PointTile(
point: _points[i],
onDelete: () => _deleteOne(_points[i]),
),
),
),
]),
),
// Alsó padding
SizedBox(height: MediaQuery.of(context).padding.bottom + 8),
],
),
);
}
// ── Törlés ────────────────────────────────────────────────────────
Future<void> _deleteOne(MeasuredPoint pt) async {
final ok = await Get.dialog<bool>(AlertDialog(
title: const Text('Pont törlése'),
content: Text('"${pt.name}" törlése visszavonohatalan.'),
actions: [
TextButton(onPressed: Get.back, child: const Text('Mégse')),
FilledButton(
style: FilledButton.styleFrom(backgroundColor: Colors.red),
onPressed: () => Get.back(result: true),
child: const Text('Törlés'),
),
],
));
if (ok != true) return;
await AppDatabase.instance.deleteMeasuredPoint(pt.id!);
setState(() => _points.remove(pt));
// Controller marker listából is eltávolítjuk
final ctrl = Get.find<MapSurveyController>();
if (pt.latitude != null && pt.longitude != null) {
ctrl.pointNotesMarker.removeWhere((m) =>
(m.point.latitude - pt.latitude!).abs() < 1e-9 &&
(m.point.longitude - pt.longitude!).abs() < 1e-9);
}
ctrl.pointWithDescriptionList
.removeWhere((p) => p.pointId.toString() == pt.name);
}
Future<void> _deleteAll() async {
final ok = await Get.dialog<bool>(AlertDialog(
title: const Text('Összes pont törlése'),
content: Text('${_points.length} pont törlése visszavonohatalan.'),
actions: [
TextButton(onPressed: Get.back, child: const Text('Mégse')),
FilledButton(
style: FilledButton.styleFrom(backgroundColor: Colors.red),
onPressed: () => Get.back(result: true),
child: const Text('Összes törlése'),
),
],
));
if (ok != true) return;
final projectId = ProjectService.to.activeProjectId;
if (projectId != null) {
await AppDatabase.instance.deleteAllMeasuredPoints(projectId);
}
final ctrl = Get.find<MapSurveyController>();
ctrl.pointWithDescriptionList.clear();
ctrl.pointNotesMarker.clear();
setState(() => _points.clear());
}
// ── CSV export ────────────────────────────────────────────────────
Future<void> _exportCsv() async {
final buf = StringBuffer();
buf.writeln(MeasuredPoint.csvHeader());
for (final pt in _points) {
buf.writeln(pt.toCsvRow());
}
final projectName =
ProjectService.to.activeProject.value?.name ?? 'projekt';
final ts =
DateTime.now().toIso8601String().replaceAll(':', '-').substring(0, 16);
final name = '${projectName}_bemert_pontok_$ts.csv';
final dir = await getTemporaryDirectory();
final file = File('${dir.path}/$name');
await file.writeAsString(buf.toString(), encoding: utf8);
await Share.shareXFiles(
[XFile(file.path, mimeType: 'text/csv; charset=utf-8')],
subject: 'Bemért pontok — $projectName',
);
}
Widget _emptyState() => Center(
child: Column(mainAxisSize: MainAxisSize.min, children: [
Icon(Icons.location_off_outlined,
size: 56, color: Colors.grey.shade300),
const SizedBox(height: 12),
Text('Még nincs bemért pont',
style: TextStyle(color: Colors.grey.shade500)),
]),
);
}
// ─── Fejléc sor ──────────────────────────────────────────────────────────────
class _TableHeader extends StatelessWidget {
const _TableHeader();
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
final style = TextStyle(
fontSize: 11, fontWeight: FontWeight.w600, color: cs.onSurfaceVariant);
return Container(
color: cs.surfaceContainerHighest,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
child: Row(children: [
SizedBox(width: 52, child: Text('Pont', style: style)),
Expanded(child: Text('EOV Y', style: style)),
Expanded(child: Text('EOV X', style: style)),
SizedBox(
width: 72,
child: Text('Leírás', style: style, textAlign: TextAlign.center)),
const SizedBox(width: 40), // törlés gomb helye
]),
);
}
}
// ─── Egy pont sor ────────────────────────────────────────────────────────────
class _PointTile extends StatelessWidget {
final MeasuredPoint point;
final VoidCallback onDelete;
const _PointTile({required this.point, required this.onDelete});
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
final hasEov = point.eovY != null && point.eovX != null;
final accColor = _accColor(point.accuracy);
return Dismissible(
key: ValueKey(point.id),
direction: DismissDirection.endToStart,
confirmDismiss: (_) async {
onDelete();
return false; // mi kezeljük a törlést
},
background: Container(
color: Colors.red,
alignment: Alignment.centerRight,
padding: const EdgeInsets.only(right: 20),
child: const Icon(Icons.delete, color: Colors.white),
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
child: Row(children: [
// Pont neve
SizedBox(
width: 52,
child: Text(point.name,
style:
const TextStyle(fontWeight: FontWeight.w700, fontSize: 14),
overflow: TextOverflow.ellipsis),
),
// EOV Y
Expanded(
child: Text(
hasEov ? point.eovY!.toStringAsFixed(1) : '',
style: const TextStyle(
fontSize: 13, fontFeatures: [FontFeature.tabularFigures()]),
),
),
// EOV X
Expanded(
child: Text(
hasEov ? point.eovX!.toStringAsFixed(1) : '',
style: const TextStyle(
fontSize: 13, fontFeatures: [FontFeature.tabularFigures()]),
),
),
// Pontosság
SizedBox(
width: 72,
child: Text(
point.note.isEmpty ? '---' : point.note,
style: TextStyle(
fontSize: 12,
color: point.note.isEmpty ? Colors.grey.shade400 : null,
),
overflow: TextOverflow.ellipsis,
),
),
// Törlés gomb
SizedBox(
width: 40,
child: IconButton(
icon: Icon(Icons.delete_outline,
size: 18, color: Colors.grey.shade400),
onPressed: onDelete,
padding: EdgeInsets.zero,
constraints: const BoxConstraints(minWidth: 36, minHeight: 36),
),
),
]),
),
);
}
Color _accColor(double? acc) {
if (acc == null) return Colors.grey;
if (acc <= 0.05) return Colors.green;
if (acc <= 0.20) return Colors.lightGreen.shade700;
if (acc <= 1.00) return Colors.orange;
return Colors.red;
}
}