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
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 6s
This commit is contained in:
@@ -190,25 +190,22 @@ class DeviceIdentityService extends GetxService {
|
||||
final user = Supabase.instance.client.auth.currentUser;
|
||||
if (user == null || deviceId.isEmpty) return;
|
||||
|
||||
await Supabase.instance.client.from('terepi_seged_devices').upsert({
|
||||
'id': appInstanceId,
|
||||
'user_id': user.id,
|
||||
'name': info.systemDeviceName,
|
||||
'device_id': deviceId,
|
||||
'platform': info.platform,
|
||||
'model': model,
|
||||
'os_version': info.osVersion,
|
||||
'app_version': appInfo,
|
||||
'last_seen_at': DateTime.now().toUtc().toIso8601String()
|
||||
});
|
||||
|
||||
//
|
||||
// // await Supabase.instance.client
|
||||
// .from('devices')
|
||||
// .upsert(
|
||||
// info.toRegistrationMap(label: deviceLabel.value),
|
||||
// onConflict: 'device_id',
|
||||
// );
|
||||
try {
|
||||
await Supabase.instance.client.from('terepi_seged_devices').upsert({
|
||||
'id': appInstanceId,
|
||||
'user_id': user.id,
|
||||
'name': info.systemDeviceName,
|
||||
'device_id': deviceId,
|
||||
'platform': info.platform,
|
||||
'model': model,
|
||||
'os_version': info.osVersion,
|
||||
'app_version': appInfo,
|
||||
'last_seen_at': DateTime.now().toUtc().toIso8601String()
|
||||
});
|
||||
} catch (e) {
|
||||
AppLogger.e('DeviceIdentityService.registerDevice',
|
||||
'Eszköz-regisztráció hiba (user=${user.id} - device=$deviceId): $e');
|
||||
}
|
||||
}
|
||||
|
||||
// ── Gyors elérők (kényelemért) ────────────────────────────────────
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
|
||||
import '../models/field_party.dart';
|
||||
import '../models/field_property.dart';
|
||||
|
||||
/// Ingatlan-nyilvántartás lekérdezései — SZÁNDÉKOSAN tisztán online,
|
||||
/// nincs helyi outbox/tükör-tábla (a kapcsolatokkal/mérésekkel
|
||||
/// ellentétben). Ez az adat nem kerülhet a készülék adatbázisába.
|
||||
class FieldPropertyService extends GetxService {
|
||||
static FieldPropertyService get to => Get.find();
|
||||
|
||||
SupabaseClient get _client => Supabase.instance.client;
|
||||
|
||||
/// Ingatlanok listája, kereséssel/szűréssel.
|
||||
Future<List<FieldProperty>> listProperties({
|
||||
required String projectId,
|
||||
String? search,
|
||||
String? settlement,
|
||||
String? mailingStatus,
|
||||
}) async {
|
||||
var query = _client
|
||||
.from('terepi_seged_field_properties_view')
|
||||
.select()
|
||||
.eq('project_id', projectId);
|
||||
|
||||
if (settlement != null && settlement.isNotEmpty) {
|
||||
query = query.eq('settlement', settlement);
|
||||
}
|
||||
if (mailingStatus != null && mailingStatus.isNotEmpty) {
|
||||
query = query.eq('mailing_status', mailingStatus);
|
||||
}
|
||||
if (search != null && search.trim().isNotEmpty) {
|
||||
final s = search.trim().replaceAll(',', ' ');
|
||||
query = query.or(
|
||||
'parcel_number.ilike.%$s%,owner_names.ilike.%$s%,settlement.ilike.%$s%',
|
||||
);
|
||||
}
|
||||
|
||||
final rows = await query
|
||||
.order('settlement', ascending: true)
|
||||
.order('parcel_number', ascending: true)
|
||||
.limit(1000);
|
||||
return rows.map((r) => FieldProperty.fromMap(r)).toList();
|
||||
}
|
||||
|
||||
/// Tulajdonosok keresése név szerint.
|
||||
Future<List<FieldParty>> searchParties({
|
||||
required String projectId,
|
||||
String? search,
|
||||
}) async {
|
||||
var query = _client
|
||||
.from('terepi_seged_field_parties_view')
|
||||
.select()
|
||||
.eq('project_id', projectId);
|
||||
|
||||
if (search != null && search.trim().isNotEmpty) {
|
||||
query = query.ilike('name', '%${search.trim()}%');
|
||||
}
|
||||
|
||||
final rows = await query.order('name', ascending: true).limit(1000);
|
||||
return rows.map((r) => FieldParty.fromMap(r)).toList();
|
||||
}
|
||||
|
||||
/// Egy adott tulajdonos ÖSSZES ingatlana.
|
||||
Future<List<FieldProperty>> listPropertiesForParty(String partyId) async {
|
||||
final links = await _client
|
||||
.from('terepi_seged_field_property_parties')
|
||||
.select('property_id')
|
||||
.eq('party_id', partyId)
|
||||
.eq('is_current', true);
|
||||
|
||||
final propertyIds =
|
||||
links.map((r) => r['property_id'] as String).toSet().toList();
|
||||
if (propertyIds.isEmpty) return [];
|
||||
|
||||
final rows = await _client
|
||||
.from('terepi_seged_field_properties_view')
|
||||
.select()
|
||||
.inFilter('id', propertyIds)
|
||||
.order('settlement')
|
||||
.order('parcel_number');
|
||||
|
||||
return rows.map((r) => FieldProperty.fromMap(r)).toList();
|
||||
}
|
||||
|
||||
/// Egy ingatlan tulajdonosi/kezelői bontása, tulajdoni hányaddal — a
|
||||
/// részletnézethez.
|
||||
Future<List<Map<String, dynamic>>> propertyOwnershipDetail(
|
||||
String propertyId) async {
|
||||
final rows = await _client
|
||||
.from('terepi_seged_field_property_parties')
|
||||
.select('*, terepi_seged_field_parties(name, party_type)')
|
||||
.eq('property_id', propertyId)
|
||||
.eq('is_current', true);
|
||||
return List<Map<String, dynamic>>.from(rows);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
import 'package:flutter_map/flutter_map.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
|
||||
import '../models/field_property.dart';
|
||||
|
||||
class GeoJsonImportResult {
|
||||
final int imported;
|
||||
final List<String> skipped;
|
||||
const GeoJsonImportResult({required this.imported, required this.skipped});
|
||||
}
|
||||
|
||||
/// Ingatlan-parcellák geometriája (GeoJSON-import + térképi lekérdezés).
|
||||
///
|
||||
/// SZÁNDÉKOSAN tisztán online, nincs helyi tükör-tábla. A tábla
|
||||
/// PROJEKT-FÜGGETLEN, közös katalógus — egy fizikai telek geometriája
|
||||
/// nem egyetlen projekthez tartozik.
|
||||
class ParcelGeometryService extends GetxService {
|
||||
static ParcelGeometryService get to => Get.find();
|
||||
|
||||
SupabaseClient get _client => Supabase.instance.client;
|
||||
|
||||
// ═════════════════════════════════════════════════════════════════
|
||||
// Normalizálás — PONTOSAN a levél-pipeline szabálya (ellenőrzött
|
||||
// minta alapján): minden "/"-lel elválasztott számjegy-szegmensről
|
||||
// levágja a vezető nullákat.
|
||||
// "0173/4" → "173/4", "0620" → "620", "011/9" → "11/9"
|
||||
// ═════════════════════════════════════════════════════════════════
|
||||
|
||||
static String normalizeParcelNumber(String raw) {
|
||||
final trimmed = raw.trim();
|
||||
final parts = trimmed.split('/');
|
||||
final normalized = parts.map((p) {
|
||||
final stripped = p.replaceFirst(RegExp(r'^0+(?=\d)'), '');
|
||||
return stripped.isEmpty ? '0' : stripped;
|
||||
}).join('/');
|
||||
return normalized;
|
||||
}
|
||||
|
||||
// ═════════════════════════════════════════════════════════════════
|
||||
// Import
|
||||
// ═════════════════════════════════════════════════════════════════
|
||||
|
||||
static const _settlementKeys = ['telepules', 'település', 'settlement'];
|
||||
static const _parcelKeys = [
|
||||
'hrsz',
|
||||
'helyrajziszam',
|
||||
'helyrajzi_szam',
|
||||
'parcel_number',
|
||||
];
|
||||
|
||||
static String? _extractByKeys(Map<String, dynamic> props, List<String> keys) {
|
||||
final lower = {for (final e in props.entries) e.key.toLowerCase(): e.value};
|
||||
for (final k in keys) {
|
||||
final v = lower[k];
|
||||
if (v != null && v.toString().trim().isNotEmpty)
|
||||
return v.toString().trim();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// A GeoJSON tartalmának importja — Feature-önként upsert, egy hibás
|
||||
/// elem nem akasztja meg a többit.
|
||||
Future<GeoJsonImportResult> importGeoJson(String content,
|
||||
{required String projectId, String? sourceFileName}) async {
|
||||
final dynamic json = jsonDecode(content);
|
||||
final features = (json is Map && json['features'] is List)
|
||||
? json['features'] as List
|
||||
: <dynamic>[];
|
||||
|
||||
var imported = 0;
|
||||
final skipped = <String>[];
|
||||
|
||||
for (final f in features) {
|
||||
try {
|
||||
if (f is! Map) {
|
||||
skipped.add('érvénytelen elem');
|
||||
continue;
|
||||
}
|
||||
final geometry = f['geometry'];
|
||||
if (geometry is! Map) {
|
||||
skipped.add('geometria nélküli elem');
|
||||
continue;
|
||||
}
|
||||
|
||||
final geomType = geometry['type'] as String?;
|
||||
List coords;
|
||||
if (geomType == 'MultiPolygon') {
|
||||
coords = geometry['coordinates'] as List;
|
||||
} else if (geomType == 'Polygon') {
|
||||
// Egységesítjük MultiPolygon-alakúra tároláshoz, hogy a
|
||||
// megjelenítés mindig ugyanazt a szerkezetet olvassa.
|
||||
coords = [geometry['coordinates']];
|
||||
} else {
|
||||
skipped.add('nem támogatott geometria-típus: $geomType');
|
||||
continue;
|
||||
}
|
||||
|
||||
final props = (f['properties'] as Map?)?.cast<String, dynamic>() ?? {};
|
||||
final settlement = _extractByKeys(props, _settlementKeys);
|
||||
final rawParcel = _extractByKeys(props, _parcelKeys);
|
||||
|
||||
if (settlement == null || rawParcel == null) {
|
||||
skipped.add(
|
||||
'hiányzó település/helyrajzi szám (talált kulcsok: ${props.keys.join(", ")})');
|
||||
continue;
|
||||
}
|
||||
|
||||
final normalized = normalizeParcelNumber(rawParcel);
|
||||
final bbox = _computeBbox(coords);
|
||||
|
||||
await _client.from('terepi_seged_field_parcel_geometries').upsert({
|
||||
'project_id': projectId,
|
||||
'settlement': settlement,
|
||||
'normalized_parcel_number': normalized,
|
||||
'parcel_number_raw': rawParcel,
|
||||
'geometry': {'type': 'MultiPolygon', 'coordinates': coords},
|
||||
'bbox_min_lat': bbox.minLat,
|
||||
'bbox_max_lat': bbox.maxLat,
|
||||
'bbox_min_lon': bbox.minLon,
|
||||
'bbox_max_lon': bbox.maxLon,
|
||||
'source_file': sourceFileName,
|
||||
}, onConflict: 'project_id,settlement,normalized_parcel_number');
|
||||
|
||||
imported++;
|
||||
} catch (e) {
|
||||
skipped.add('hiba: $e');
|
||||
}
|
||||
}
|
||||
|
||||
return GeoJsonImportResult(imported: imported, skipped: skipped);
|
||||
}
|
||||
|
||||
_Bbox _computeBbox(List multiPolygonCoords) {
|
||||
double minLat = double.infinity, maxLat = -double.infinity;
|
||||
double minLon = double.infinity, maxLon = -double.infinity;
|
||||
for (final polygon in multiPolygonCoords) {
|
||||
for (final ring in polygon as List) {
|
||||
for (final point in ring as List) {
|
||||
final lon = (point[0] as num).toDouble();
|
||||
final lat = (point[1] as num).toDouble();
|
||||
if (lat < minLat) minLat = lat;
|
||||
if (lat > maxLat) maxLat = lat;
|
||||
if (lon < minLon) minLon = lon;
|
||||
if (lon > maxLon) maxLon = lon;
|
||||
}
|
||||
}
|
||||
}
|
||||
return _Bbox(
|
||||
minLat: minLat, maxLat: maxLat, minLon: minLon, maxLon: maxLon);
|
||||
}
|
||||
|
||||
// ═════════════════════════════════════════════════════════════════
|
||||
// Lekérdezés a térképi megjelenítéshez
|
||||
// ═════════════════════════════════════════════════════════════════
|
||||
|
||||
/// Egy vagy több ingatlan geometriája (settlement + normalized_parcel_number
|
||||
/// párok alapján) — a property-lista bármely részhalmazára hívható.
|
||||
Future<List<Map<String, dynamic>>> fetchGeometriesFor(
|
||||
List<FieldProperty> properties) async {
|
||||
if (properties.isEmpty) return [];
|
||||
|
||||
// Minden átadott property ugyanahhoz az (aktív) projekthez tartozik —
|
||||
// a geometria is erre a projektre szűkül.
|
||||
final projectId = properties.first.projectId;
|
||||
|
||||
final bySettlement = <String, Set<String>>{};
|
||||
for (final p in properties) {
|
||||
bySettlement
|
||||
.putIfAbsent(p.settlement, () => {})
|
||||
.add(p.normalizedParcelNumber);
|
||||
}
|
||||
|
||||
final result = <Map<String, dynamic>>[];
|
||||
for (final entry in bySettlement.entries) {
|
||||
final rows = await _client
|
||||
.from('terepi_seged_field_parcel_geometries')
|
||||
.select()
|
||||
.eq('project_id', projectId)
|
||||
.eq('settlement', entry.key)
|
||||
.inFilter('normalized_parcel_number', entry.value.toList());
|
||||
result.addAll(List<Map<String, dynamic>>.from(rows));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// ═════════════════════════════════════════════════════════════════
|
||||
// flutter_map segédek
|
||||
// ═════════════════════════════════════════════════════════════════
|
||||
|
||||
List<Polygon> parsePolygons(List<Map<String, dynamic>> rows,
|
||||
{Color color = Colors.deepOrange}) {
|
||||
final polygons = <Polygon>[];
|
||||
for (final row in rows) {
|
||||
final geom = row['geometry'] as Map;
|
||||
final coords = geom['coordinates'] as List; // MultiPolygon
|
||||
for (final polygon in coords) {
|
||||
final outerRing = (polygon as List).first as List;
|
||||
final points = outerRing
|
||||
.map((p) =>
|
||||
LatLng((p[1] as num).toDouble(), (p[0] as num).toDouble()))
|
||||
.toList();
|
||||
polygons.add(Polygon(
|
||||
points: points,
|
||||
color: color.withOpacity(0.35),
|
||||
borderColor: color,
|
||||
borderStrokeWidth: 2,
|
||||
));
|
||||
}
|
||||
}
|
||||
return polygons;
|
||||
}
|
||||
|
||||
LatLngBounds? computeBounds(List<Map<String, dynamic>> rows) {
|
||||
if (rows.isEmpty) return null;
|
||||
double minLat = double.infinity, maxLat = -double.infinity;
|
||||
double minLon = double.infinity, maxLon = -double.infinity;
|
||||
for (final row in rows) {
|
||||
minLat = math.min(minLat, (row['bbox_min_lat'] as num).toDouble());
|
||||
maxLat = math.max(maxLat, (row['bbox_max_lat'] as num).toDouble());
|
||||
minLon = math.min(minLon, (row['bbox_min_lon'] as num).toDouble());
|
||||
maxLon = math.max(maxLon, (row['bbox_max_lon'] as num).toDouble());
|
||||
}
|
||||
return LatLngBounds(LatLng(minLat, minLon), LatLng(maxLat, maxLon));
|
||||
}
|
||||
}
|
||||
|
||||
class _Bbox {
|
||||
final double minLat, maxLat, minLon, maxLon;
|
||||
const _Bbox({
|
||||
required this.minLat,
|
||||
required this.maxLat,
|
||||
required this.minLon,
|
||||
required this.maxLon,
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user