Ingatlanok tulajdoni lapok és tulajdonosok táblázatos és térképi megjelenítése.
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 6s

This commit is contained in:
2026-08-31 20:38:18 +02:00
parent 443b35fbb9
commit 47f7687e40
13 changed files with 1599 additions and 26 deletions
@@ -1,3 +1,263 @@
import 'package:get/get.dart';
import 'dart:async';
import 'dart:io';
class PropertyListController extends GetxController {}
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:terepi_seged/models/field_party.dart';
import 'package:terepi_seged/models/field_property.dart';
import 'package:terepi_seged/pages/property_list/presentations/views/parcel_map_view.dart';
import 'package:terepi_seged/services/field_property_service.dart';
import 'package:terepi_seged/services/parcel_geometry_service.dart';
import 'package:terepi_seged/services/project_service.dart';
enum FieldViewMode { properties, parties }
class PropertyListController extends GetxController {
static PropertyListController get to => Get.find();
final viewMode = FieldViewMode.properties.obs;
// ── Ingatlanok fül ───────────────────────────────────────────────
final searchText = ''.obs;
final settlementFilter = Rxn<String>();
final mailingStatusFilter = Rxn<String>();
final isLoading = false.obs;
final properties = <FieldProperty>[].obs;
final settlements = <String>[].obs;
/// Ha nem null, a lista egy KIVÁLASZTOTT tulajdonos ingatlanjait
/// mutatja, nem az általános keresést/szűrést.
final selectedParty = Rxn<FieldParty>();
// ── Tulajdonosok fül ─────────────────────────────────────────────
final partySearchText = ''.obs;
final isLoadingParties = false.obs;
final parties = <FieldParty>[].obs;
Timer? _propertyDebounce;
Timer? _partyDebounce;
final partySearchController = TextEditingController();
@override
@override
void onInit() {
super.onInit();
loadProperties();
searchParties(); // a tulajdonos-lista is azonnal töltődjön, ne csak gépelésre
ever(ProjectService.to.activeProject, (_) {
selectedParty.value = null;
loadProperties();
searchParties();
});
}
@override
void onClose() {
_propertyDebounce?.cancel();
_partyDebounce?.cancel();
partySearchController.dispose();
super.onClose();
}
// ── Ingatlan-keresés/szűrés ──────────────────────────────────────
void onSearchChanged(String v) {
searchText.value = v;
_propertyDebounce?.cancel();
_propertyDebounce =
Timer(const Duration(milliseconds: 400), loadProperties);
}
void setSettlementFilter(String? v) {
settlementFilter.value = v;
loadProperties();
}
void setMailingStatusFilter(String? v) {
mailingStatusFilter.value = v;
loadProperties();
}
Future<void> loadProperties() async {
// Ha épp egy tulajdonosra van szűkítve a nézet, a sima keresés ne
// írja felül azt — a felhasználó explicit törli (clearPartySelection).
if (selectedParty.value != null) return;
final projectId = ProjectService.to.activeProject.value?.uuid;
if (projectId == null) {
properties.clear();
settlements.clear();
return;
}
isLoading.value = true;
try {
final list = await FieldPropertyService.to.listProperties(
projectId: projectId,
search: searchText.value,
settlement: settlementFilter.value,
mailingStatus: mailingStatusFilter.value,
);
properties.value = list;
// A település-szűrő lista a TELJES (szűretlen) körből épülne fel
// igazán jól — egyszerűség kedvéért most a jelenlegi találatokból
// számoljuk, ami induláskor (szűrés nélkül) helyes.
if (settlementFilter.value == null) {
settlements.value = list.map((p) => p.settlement).toSet().toList()
..sort();
}
} catch (e) {
Get.snackbar('Hiba', 'Nem sikerült betölteni az ingatlanokat: $e',
snackPosition: SnackPosition.BOTTOM);
} finally {
isLoading.value = false;
}
}
// ── Tulajdonos-keresés ───────────────────────────────────────────
void onPartySearchChanged(String v) {
partySearchText.value = v;
_partyDebounce?.cancel();
_partyDebounce = Timer(const Duration(milliseconds: 400), searchParties);
}
Future<void> searchParties() async {
final projectId = ProjectService.to.activeProject.value?.uuid;
if (projectId == null) return;
isLoadingParties.value = true;
try {
parties.value = await FieldPropertyService.to.searchParties(
projectId: projectId,
search: partySearchText.value,
);
} catch (e) {
Get.snackbar('Hiba', 'Nem sikerült betölteni a tulajdonosokat: $e',
snackPosition: SnackPosition.BOTTOM);
} finally {
isLoadingParties.value = false;
}
}
/// Determinisztikus törlés — nem várja meg a debounce-ot, azonnal
/// visszahozza a teljes (szűretlen) listát.
void clearPartySearch() {
partySearchController.clear();
partySearchText.value = '';
_partyDebounce?.cancel();
searchParties();
}
/// Egy tulajdonos kiválasztása → átvált az Ingatlanok fülre, az ő
/// ingatlanjaival.
Future<void> selectParty(FieldParty party) async {
selectedParty.value = party;
viewMode.value = FieldViewMode.properties;
isLoading.value = true;
try {
properties.value =
await FieldPropertyService.to.listPropertiesForParty(party.id);
} catch (e) {
Get.snackbar('Hiba', 'Nem sikerült betölteni az ingatlanokat: $e',
snackPosition: SnackPosition.BOTTOM);
} finally {
isLoading.value = false;
}
}
void clearPartySelection() {
selectedParty.value = null;
viewMode.value = FieldViewMode.parties;
loadProperties();
// A korábban a tulajdonos MEGTALÁLÁSÁHOZ begépelt keresőszöveg is
// törlődjön — különben a Tulajdonosok fülre visszaváltva a lista
// továbbra is arra az egy szűrt találatra maradna korlátozva.
clearPartySearch();
}
// ── GeoJSON import (geometria-katalógus) ────────────────────────────
Future<void> importGeoJsonFile() async {
// FONTOS: nem FileType.custom + allowedExtensions — a .geojson nincs
// benne Android beépített MIME-táblázatában, emiatt felhős forrásoknál
// (Google Drive stb.) a fájlok szürkén, kiválaszthatatlanul jelennek
// meg. Helyette mindent engedünk, és utólag, kézzel ellenőrizzük a
// kiterjesztést.
final result = await FilePicker.platform.pickFiles(type: FileType.any);
if (result == null || result.files.single.path == null) return;
final pickedName = result.files.single.name.toLowerCase();
if (!pickedName.endsWith('.geojson') && !pickedName.endsWith('.json')) {
Get.snackbar('Nem támogatott fájl',
'Válassz .geojson vagy .json kiterjesztésű fájlt.',
snackPosition: SnackPosition.BOTTOM);
return;
}
final file = File(result.files.single.path!);
final content = await file.readAsString();
Get.dialog(const Center(child: CircularProgressIndicator()),
barrierDismissible: false);
try {
final projectId = ProjectService.to.activeProject.value?.uuid;
if (projectId == null) {
Get.back();
Get.snackbar('Nincs aktív projekt',
'Válassz aktív projektet az importálás előtt.',
snackPosition: SnackPosition.BOTTOM);
return;
}
final res = await ParcelGeometryService.to.importGeoJson(content,
projectId: projectId, sourceFileName: result.files.single.name);
Get.back(); // töltő-dialógus bezárása
if (res.skipped.isNotEmpty) {
Get.dialog(AlertDialog(
title: Text(
'Import: ${res.imported} sikeres, ${res.skipped.length} kihagyva'),
content: SizedBox(
width: double.maxFinite,
child: ListView(
shrinkWrap: true,
children: res.skipped
.take(20)
.map(
(s) => Text('$s', style: const TextStyle(fontSize: 12)))
.toList(),
),
),
actions: [
TextButton(onPressed: Get.back, child: const Text('Rendben')),
],
));
} else {
Get.snackbar('Import kész', '${res.imported} parcella importálva.',
snackPosition: SnackPosition.BOTTOM);
}
} catch (e) {
Get.back();
Get.snackbar('Import hiba', '$e', snackPosition: SnackPosition.BOTTOM);
}
}
// ── Térképi megjelenítés ─────────────────────────────────────────
Future<void> viewOnMap({
required String title,
required List<FieldProperty> targetProperties,
}) async {
if (targetProperties.isEmpty) return;
Get.dialog(const Center(child: CircularProgressIndicator()),
barrierDismissible: false);
try {
final rows =
await ParcelGeometryService.to.fetchGeometriesFor(targetProperties);
Get.back();
Get.to(() => ParcelMapView(title: title, geometryRows: rows));
} catch (e) {
Get.back();
Get.snackbar('Hiba', 'Nem sikerült betölteni a térképi adatot: $e',
snackPosition: SnackPosition.BOTTOM);
}
}
}