Kontaktok listázási hibájának javítása, csv export
This commit is contained in:
@@ -65,6 +65,7 @@ Future<void> main() async {
|
|||||||
Get.put(AuthService());
|
Get.put(AuthService());
|
||||||
Get.put(DeviceIdentityService(), permanent: true);
|
Get.put(DeviceIdentityService(), permanent: true);
|
||||||
await AppDatabase.instance.database;
|
await AppDatabase.instance.database;
|
||||||
|
//await AppDatabase.instance.testOnly();
|
||||||
Get.put(ProjectService(), permanent: true);
|
Get.put(ProjectService(), permanent: true);
|
||||||
|
|
||||||
Get.put(AppLogger(), permanent: true);
|
Get.put(AppLogger(), permanent: true);
|
||||||
|
|||||||
@@ -6,6 +6,8 @@
|
|||||||
/// tábla dönti el. Az uuid szerveroldali (gen_random_uuid()).
|
/// tábla dönti el. Az uuid szerveroldali (gen_random_uuid()).
|
||||||
class Contact {
|
class Contact {
|
||||||
final String? id; // uuid — új rekordnál null, a szerver adja
|
final String? id; // uuid — új rekordnál null, a szerver adja
|
||||||
|
final String
|
||||||
|
projectId; // az AKTUÁLIS projekt uuid-ja (terepi_seged_projects.id)
|
||||||
final String name;
|
final String name;
|
||||||
final String address;
|
final String address;
|
||||||
final String phone;
|
final String phone;
|
||||||
@@ -16,6 +18,7 @@ class Contact {
|
|||||||
|
|
||||||
const Contact({
|
const Contact({
|
||||||
this.id,
|
this.id,
|
||||||
|
required this.projectId,
|
||||||
required this.name,
|
required this.name,
|
||||||
this.address = '',
|
this.address = '',
|
||||||
this.phone = '',
|
this.phone = '',
|
||||||
@@ -34,6 +37,7 @@ class Contact {
|
|||||||
}) =>
|
}) =>
|
||||||
Contact(
|
Contact(
|
||||||
id: id,
|
id: id,
|
||||||
|
projectId: projectId,
|
||||||
name: name ?? this.name,
|
name: name ?? this.name,
|
||||||
address: address ?? this.address,
|
address: address ?? this.address,
|
||||||
phone: phone ?? this.phone,
|
phone: phone ?? this.phone,
|
||||||
@@ -48,6 +52,7 @@ class Contact {
|
|||||||
/// sosem a kliens állítja.
|
/// sosem a kliens állítja.
|
||||||
Map<String, dynamic> toWriteMap() => {
|
Map<String, dynamic> toWriteMap() => {
|
||||||
if (id != null) 'id': id,
|
if (id != null) 'id': id,
|
||||||
|
'project_id': projectId,
|
||||||
'name': name.trim(),
|
'name': name.trim(),
|
||||||
'address': address.trim(),
|
'address': address.trim(),
|
||||||
'phone': phone.trim(),
|
'phone': phone.trim(),
|
||||||
@@ -57,6 +62,7 @@ class Contact {
|
|||||||
|
|
||||||
factory Contact.fromMap(Map<String, dynamic> m) => Contact(
|
factory Contact.fromMap(Map<String, dynamic> m) => Contact(
|
||||||
id: m['id'] as String?,
|
id: m['id'] as String?,
|
||||||
|
projectId: m['project_id'] as String,
|
||||||
name: (m['name'] as String?) ?? '',
|
name: (m['name'] as String?) ?? '',
|
||||||
address: (m['address'] as String?) ?? '',
|
address: (m['address'] as String?) ?? '',
|
||||||
phone: (m['phone'] as String?) ?? '',
|
phone: (m['phone'] as String?) ?? '',
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
import 'package:terepi_seged/services/project_service.dart';
|
||||||
|
|
||||||
import '../../../../services/contact_service.dart';
|
import '../../../../services/contact_service.dart';
|
||||||
|
|
||||||
@@ -27,16 +28,22 @@ class ContactsController extends GetxController {
|
|||||||
}).toList();
|
}).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> load() async {
|
Future<void> load({bool silent = false}) async {
|
||||||
isLoading.value = true;
|
final projectId = ProjectService.to.activeProject.value?.uuid;
|
||||||
|
if (projectId == null) {
|
||||||
|
error.value = 'Nincs aktív projekt.';
|
||||||
|
isLoading.value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!silent) isLoading.value = true;
|
||||||
error.value = '';
|
error.value = '';
|
||||||
try {
|
try {
|
||||||
items.value = await ContactService.to.listMerged();
|
items.value = await ContactService.to.listMerged(projectId);
|
||||||
pendingCount.value = await ContactService.to.pendingCount();
|
pendingCount.value = await ContactService.to.pendingCount(projectId);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error.value = _friendly(e);
|
error.value = _friendly(e);
|
||||||
} finally {
|
} finally {
|
||||||
isLoading.value = false;
|
if (!silent) isLoading.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,7 +67,10 @@ class ContactsController extends GetxController {
|
|||||||
await ContactService.to
|
await ContactService.to
|
||||||
.delete(id: it.contact.id, localUuid: it.localUuid);
|
.delete(id: it.contact.id, localUuid: it.localUuid);
|
||||||
items.remove(it);
|
items.remove(it);
|
||||||
pendingCount.value = await ContactService.to.pendingCount();
|
final projectId = ProjectService.to.activeProject.value?.uuid;
|
||||||
|
if (projectId != null) {
|
||||||
|
pendingCount.value = await ContactService.to.pendingCount(projectId);
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Get.snackbar('Hiba', _friendly(e), snackPosition: SnackPosition.BOTTOM);
|
Get.snackbar('Hiba', _friendly(e), snackPosition: SnackPosition.BOTTOM);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
import 'package:terepi_seged/pages/contacts/presentation/controllers/contacts_controller.dart';
|
||||||
|
import 'package:terepi_seged/services/project_service.dart';
|
||||||
|
|
||||||
import '../../../../models/contact.dart';
|
import '../../../../models/contact.dart';
|
||||||
import '../../../../services/contact_service.dart';
|
import '../../../../services/contact_service.dart';
|
||||||
@@ -57,7 +59,11 @@ class _ContactEditViewState extends State<ContactEditView> {
|
|||||||
if (!(_formKey.currentState?.validate() ?? false)) return;
|
if (!(_formKey.currentState?.validate() ?? false)) return;
|
||||||
setState(() => _saving = true);
|
setState(() => _saving = true);
|
||||||
try {
|
try {
|
||||||
final contact = (_original ?? const Contact(name: '')).copyWith(
|
final contact = (_original ??
|
||||||
|
Contact(
|
||||||
|
name: '',
|
||||||
|
projectId: ProjectService.to.activeProject.value!.uuid))
|
||||||
|
.copyWith(
|
||||||
name: _name.text,
|
name: _name.text,
|
||||||
address: _address.text,
|
address: _address.text,
|
||||||
phone: _phone.text,
|
phone: _phone.text,
|
||||||
@@ -66,6 +72,9 @@ class _ContactEditViewState extends State<ContactEditView> {
|
|||||||
);
|
);
|
||||||
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>()) {
|
||||||
|
Get.find<ContactsController>().load(silent: true);
|
||||||
|
}
|
||||||
Get.snackbar(queued ? 'Elmentve (offline)' : 'Mentve',
|
Get.snackbar(queued ? 'Elmentve (offline)' : 'Mentve',
|
||||||
queued ? '${contact.name} - feltöltés, amint van net' : contact.name,
|
queued ? '${contact.name} - feltöltés, amint van net' : contact.name,
|
||||||
snackPosition: SnackPosition.BOTTOM);
|
snackPosition: SnackPosition.BOTTOM);
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ import 'dart:io';
|
|||||||
import 'package:file_picker/file_picker.dart';
|
import 'package:file_picker/file_picker.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
import 'package:terepi_seged/models/project.dart';
|
||||||
|
import 'package:terepi_seged/services/contact_export_service.dart';
|
||||||
|
import 'package:terepi_seged/services/project_service.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
import '../../../../models/contact.dart';
|
import '../../../../models/contact.dart';
|
||||||
@@ -29,18 +32,42 @@ class ContactsView extends StatelessWidget {
|
|||||||
body: const _NoAccess(),
|
body: const _NoAccess(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
// Projekt-kapu — a kapcsolat mindig egy projekthez tartozik, és csak
|
||||||
|
// MEGOSZTOTT (online) projektnél van értelme (a funkció a Supabase-en
|
||||||
|
// él). Csak-lokális vagy hiányzó projektnél blokkolunk.
|
||||||
|
final activeProject = ProjectService.to.activeProject.value;
|
||||||
|
if (activeProject == null || activeProject.isLocalOnly) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(title: const Text('Kapcsolatok')),
|
||||||
|
body: _ProjectRequired(project: activeProject),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
final c = Get.put(ContactsController());
|
final c = Get.put(ContactsController());
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text('Kapcsolatok'),
|
title: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const Text('Kapcsolatok'),
|
||||||
|
Text(activeProject.name,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 12, fontWeight: FontWeight.normal)),
|
||||||
|
],
|
||||||
|
),
|
||||||
actions: [
|
actions: [
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.upload_file),
|
icon: const Icon(Icons.upload_file),
|
||||||
tooltip: 'Import CSV',
|
tooltip: 'Import CSV',
|
||||||
onPressed: () => _importCsv(c),
|
onPressed: () => _importCsv(c),
|
||||||
),
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.ios_share),
|
||||||
|
tooltip: 'Export CSV',
|
||||||
|
onPressed: () => _exportDialog(c, activeProject.name),
|
||||||
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.refresh),
|
icon: const Icon(Icons.refresh),
|
||||||
tooltip: 'Frissítés',
|
tooltip: 'Frissítés',
|
||||||
@@ -143,8 +170,9 @@ class ContactsView extends StatelessWidget {
|
|||||||
snackPosition: SnackPosition.BOTTOM);
|
snackPosition: SnackPosition.BOTTOM);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
final activeProject = ProjectService.to.activeProject.value;
|
||||||
final parsed = await ContactService.parseCsv(File(path));
|
final parsed = await ContactService.parseCsv(File(path),
|
||||||
|
projectId: activeProject!.uuid);
|
||||||
if (parsed.isEmpty) {
|
if (parsed.isEmpty) {
|
||||||
Get.snackbar('Import', 'Nem található érvényes sor a fájlban.',
|
Get.snackbar('Import', 'Nem található érvényes sor a fájlban.',
|
||||||
snackPosition: SnackPosition.BOTTOM);
|
snackPosition: SnackPosition.BOTTOM);
|
||||||
@@ -167,15 +195,59 @@ class ContactsView extends StatelessWidget {
|
|||||||
));
|
));
|
||||||
if (ok != true) return;
|
if (ok != true) return;
|
||||||
|
|
||||||
final inserted = await ContactService.to.importMany(parsed);
|
final imported = await ContactService.to.importMany(parsed);
|
||||||
await c.load();
|
await c.load();
|
||||||
Get.snackbar('Import kész', '$inserted kapcsolat importálva.',
|
Get.snackbar(
|
||||||
snackPosition: SnackPosition.BOTTOM);
|
'Import kész',
|
||||||
|
imported.queued > 0
|
||||||
|
? '${imported.uploaded} feltöltve, ${imported.queued} offline '
|
||||||
|
'várólistán (net jöttével automatikusan felmegy).'
|
||||||
|
: '${imported.uploaded} kapcsolat importálva.',
|
||||||
|
snackPosition: SnackPosition.BOTTOM,
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Get.snackbar('Import hiba', e.toString(),
|
Get.snackbar('Import hiba', e.toString(),
|
||||||
snackPosition: SnackPosition.BOTTOM);
|
snackPosition: SnackPosition.BOTTOM);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _exportDialog(ContactsController c, String projectName) async {
|
||||||
|
final hungarian = true.obs;
|
||||||
|
|
||||||
|
await Get.dialog(AlertDialog(
|
||||||
|
title: const Text('Export CSV'),
|
||||||
|
content: Obx(() => SwitchListTile(
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
dense: true,
|
||||||
|
title: const Text('Magyar Excel-formátum'),
|
||||||
|
subtitle: const Text('pontosvessző elválasztó',
|
||||||
|
style: TextStyle(fontSize: 11)),
|
||||||
|
value: hungarian.value,
|
||||||
|
onChanged: (v) => hungarian.value = v,
|
||||||
|
)),
|
||||||
|
actions: [
|
||||||
|
TextButton(onPressed: Get.back, child: const Text('Mégse')),
|
||||||
|
FilledButton.icon(
|
||||||
|
icon: const Icon(Icons.ios_share, size: 18),
|
||||||
|
label: const Text('Export'),
|
||||||
|
onPressed: () async {
|
||||||
|
Get.back();
|
||||||
|
try {
|
||||||
|
await ContactExportService.exportAndShare(
|
||||||
|
contacts: c.filtered.map((it) => it.contact).toList(),
|
||||||
|
// contacts: c.items.map((it) => it.contact).toList(),
|
||||||
|
hungarian: hungarian.value,
|
||||||
|
projectName: projectName,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
Get.snackbar('Export', e.toString(),
|
||||||
|
snackPosition: SnackPosition.BOTTOM);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Sor ──────────────────────────────────────────────────────────────
|
// ── Sor ──────────────────────────────────────────────────────────────
|
||||||
@@ -333,3 +405,35 @@ class _ErrorState extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _ProjectRequired extends StatelessWidget {
|
||||||
|
final Project? project;
|
||||||
|
const _ProjectRequired({required this.project});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final isLocalOnly = project?.isLocalOnly ?? false;
|
||||||
|
return Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(32),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.folder_off_outlined,
|
||||||
|
size: 48, color: Colors.grey.shade400),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Text(
|
||||||
|
project == null
|
||||||
|
? 'Nincs aktív projekt.\nVálassz egyet a menüből.'
|
||||||
|
: 'A(z) "${project!.name}" projekt csak lokális.\n'
|
||||||
|
'A kapcsolatok megosztott (online) projekthez '
|
||||||
|
'tartoznak — válts megosztott projektre, vagy tedd '
|
||||||
|
'megosztottá ezt.',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'dart:math' as math;
|
|||||||
import 'dart:ui' show FontFeature;
|
import 'dart:ui' show FontFeature;
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
//import 'package:flutter/widget_previews.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:terepi_seged/pages/map_survey/presentations/widgets/stakeout_list_sheet.dart';
|
import 'package:terepi_seged/pages/map_survey/presentations/widgets/stakeout_list_sheet.dart';
|
||||||
import 'package:terepi_seged/pages/map_survey/presentations/widgets/stakeout_settings_dialog.dart';
|
import 'package:terepi_seged/pages/map_survey/presentations/widgets/stakeout_settings_dialog.dart';
|
||||||
@@ -619,6 +620,7 @@ class _DevRow extends StatelessWidget {
|
|||||||
required this.posText,
|
required this.posText,
|
||||||
required this.negText});
|
required this.negText});
|
||||||
|
|
||||||
|
//@Preview(name: 'Example')
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final cm = value.abs() * 100;
|
final cm = value.abs() * 100;
|
||||||
|
|||||||
@@ -1294,6 +1294,7 @@ class AppDatabase {
|
|||||||
await db.execute('''
|
await db.execute('''
|
||||||
CREATE TABLE IF NOT EXISTS contacts_outbox (
|
CREATE TABLE IF NOT EXISTS contacts_outbox (
|
||||||
local_uuid TEXT PRIMARY KEY,
|
local_uuid TEXT PRIMARY KEY,
|
||||||
|
project_id TEXT NOT NULL,
|
||||||
name TEXT NOT NULL,
|
name TEXT NOT NULL,
|
||||||
address TEXT NOT NULL DEFAULT '',
|
address TEXT NOT NULL DEFAULT '',
|
||||||
phone TEXT NOT NULL DEFAULT '',
|
phone TEXT NOT NULL DEFAULT '',
|
||||||
@@ -1302,6 +1303,8 @@ class AppDatabase {
|
|||||||
created_at TEXT NOT NULL
|
created_at TEXT NOT NULL
|
||||||
)
|
)
|
||||||
''');
|
''');
|
||||||
|
await db.execute('CREATE INDEX IF NOT EXISTS idx_contacts_outbox_project '
|
||||||
|
'ON contacts_outbox(project_id)');
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> insertPendingContact(Map<String, dynamic> row) async {
|
Future<void> insertPendingContact(Map<String, dynamic> row) async {
|
||||||
@@ -1309,8 +1312,18 @@ class AppDatabase {
|
|||||||
await db.insert('contacts_outbox', row);
|
await db.insert('contacts_outbox', row);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<Map<String, dynamic>>> listPendingContacts() async {
|
/// [projectId] NÉLKÜL (a flush-hoz) MINDEN várólistás sort ad vissza —
|
||||||
|
/// a feltöltés projekttől függetlenül fusson. [projectId]-vel (a UI
|
||||||
|
/// listához) csak az adott projekt pending sorait.
|
||||||
|
Future<List<Map<String, dynamic>>> listPendingContacts(
|
||||||
|
{String? projectId}) async {
|
||||||
final db = await database;
|
final db = await database;
|
||||||
|
if (projectId != null) {
|
||||||
|
return db.query('contacts_outbox',
|
||||||
|
where: 'project_id = ?',
|
||||||
|
whereArgs: [projectId],
|
||||||
|
orderBy: 'created_at ASC');
|
||||||
|
}
|
||||||
return db.query('contacts_outbox', orderBy: 'created_at ASC');
|
return db.query('contacts_outbox', orderBy: 'created_at ASC');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1327,10 +1340,24 @@ class AppDatabase {
|
|||||||
where: 'local_uuid = ?', whereArgs: [localUuid]);
|
where: 'local_uuid = ?', whereArgs: [localUuid]);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<int> countPendingContacts() async {
|
Future<int> countPendingContacts({String? projectId}) async {
|
||||||
final db = await database;
|
final db = await database;
|
||||||
|
if (projectId != null) {
|
||||||
|
return Sqflite.firstIntValue(await db.rawQuery(
|
||||||
|
'SELECT COUNT(*) FROM contacts_outbox WHERE project_id = ?',
|
||||||
|
[projectId])) ??
|
||||||
|
0;
|
||||||
|
}
|
||||||
return Sqflite.firstIntValue(
|
return Sqflite.firstIntValue(
|
||||||
await db.rawQuery('SELECT COUNT(*) FROM contacts_outbox')) ??
|
await db.rawQuery('SELECT COUNT(*) FROM contacts_outbox')) ??
|
||||||
0;
|
0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Future<void> testOnly() async {
|
||||||
|
// final db = await database;
|
||||||
|
// await db.execute(
|
||||||
|
// "ALTER TABLE contacts_outbox ADD COLUMN project_id TEXT NOT NULL DEFAULT ''");
|
||||||
|
// await db.execute('CREATE INDEX IF NOT EXISTS idx_contacts_outbox_project '
|
||||||
|
// 'ON contacts_outbox(project_id)');
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:path/path.dart' as p;
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
import 'package:share_plus/share_plus.dart';
|
||||||
|
|
||||||
|
import '../models/contact.dart';
|
||||||
|
|
||||||
|
/// Kapcsolatok CSV-export — a kitűzési jegyzőkönyv-exporttal azonos elvek.
|
||||||
|
///
|
||||||
|
/// Két dialektus:
|
||||||
|
/// * MAGYAR (Excel-barát): pontosvessző elválasztó, UTF-8 BOM-mal
|
||||||
|
/// (enélkül az Excel elrontja az ékezeteket)
|
||||||
|
/// * NEMZETKÖZI: vessző elválasztó
|
||||||
|
///
|
||||||
|
/// Mivel a lista már projekthez szűrt (ContactsController.filtered), az
|
||||||
|
/// export mindig a JELENLEG LÁTOTT (kereséssel/projekttel szűrt) sorokat
|
||||||
|
/// menti — amit a képernyőn látsz, azt kapod a fájlban is.
|
||||||
|
class ContactExportService {
|
||||||
|
ContactExportService._();
|
||||||
|
|
||||||
|
static Future<void> exportAndShare({
|
||||||
|
required List<Contact> contacts,
|
||||||
|
required bool hungarian,
|
||||||
|
String projectName = '',
|
||||||
|
}) async {
|
||||||
|
if (contacts.isEmpty) {
|
||||||
|
throw StateError('Nincs exportálható kapcsolat.');
|
||||||
|
}
|
||||||
|
|
||||||
|
final rows = List<Contact>.from(contacts)
|
||||||
|
..sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()));
|
||||||
|
|
||||||
|
final sep = hungarian ? ';' : ',';
|
||||||
|
|
||||||
|
String txt(String s) {
|
||||||
|
final v = s.trim();
|
||||||
|
return v.contains(sep) || v.contains('"') || v.contains('\n')
|
||||||
|
? '"${v.replaceAll('"', '""')}"'
|
||||||
|
: v;
|
||||||
|
}
|
||||||
|
|
||||||
|
final header = ['Nev', 'Cim', 'Telefon', 'Email', 'Megjegyzes'].join(sep);
|
||||||
|
|
||||||
|
final sb = StringBuffer();
|
||||||
|
sb.write('\uFEFF'); // UTF-8 BOM — az Excel enélkül elrontja az ékezeteket
|
||||||
|
sb.writeln(header);
|
||||||
|
for (final c in rows) {
|
||||||
|
sb.writeln([
|
||||||
|
txt(c.name),
|
||||||
|
txt(c.address),
|
||||||
|
txt(c.phone),
|
||||||
|
txt(c.email),
|
||||||
|
txt(c.note),
|
||||||
|
].join(sep));
|
||||||
|
}
|
||||||
|
|
||||||
|
final dir = await getTemporaryDirectory();
|
||||||
|
final stamp = DateTime.now()
|
||||||
|
.toIso8601String()
|
||||||
|
.substring(0, 16)
|
||||||
|
.replaceAll(':', '')
|
||||||
|
.replaceAll('-', '')
|
||||||
|
.replaceAll('T', '_');
|
||||||
|
final safeName = projectName.isEmpty
|
||||||
|
? 'kapcsolatok'
|
||||||
|
: 'kapcsolatok_${projectName.replaceAll(RegExp(r'[^\w\-]'), '_')}';
|
||||||
|
final path = p.join(dir.path, '${safeName}_$stamp.csv');
|
||||||
|
await File(path).writeAsString(sb.toString(), flush: true);
|
||||||
|
|
||||||
|
await SharePlus.instance.share(ShareParams(
|
||||||
|
files: [XFile(path, mimeType: 'text/csv')],
|
||||||
|
subject: 'Kapcsolatok — $projectName',
|
||||||
|
text: '${rows.length} kapcsolat',
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -46,8 +46,8 @@ class ContactService extends GetxService {
|
|||||||
/// A megjelenítendő lista: a Supabase-ből lehúzott kapcsolatok ELÉ
|
/// A megjelenítendő lista: a Supabase-ből lehúzott kapcsolatok ELÉ
|
||||||
/// fűzve a még fel nem töltött (pending) helyi sorok. A pending sorokat
|
/// fűzve a még fel nem töltött (pending) helyi sorok. A pending sorokat
|
||||||
/// az `isPending` jelöli — a UI kis felhő-ikonnal mutatja őket.
|
/// az `isPending` jelöli — a UI kis felhő-ikonnal mutatja őket.
|
||||||
Future<List<ContactWithState>> listMerged() async {
|
Future<List<ContactWithState>> listMerged(String projectId) async {
|
||||||
final pending = (await _db.listPendingContacts())
|
final pending = (await _db.listPendingContacts(projectId: projectId))
|
||||||
.map((m) => ContactWithState(
|
.map((m) => ContactWithState(
|
||||||
contact: Contact.fromMap(m),
|
contact: Contact.fromMap(m),
|
||||||
isPending: true,
|
isPending: true,
|
||||||
@@ -60,6 +60,7 @@ class ContactService extends GetxService {
|
|||||||
final rows = await _client
|
final rows = await _client
|
||||||
.from('terepi_seged_contacts')
|
.from('terepi_seged_contacts')
|
||||||
.select()
|
.select()
|
||||||
|
.eq('project_id', projectId)
|
||||||
.order('name', ascending: true);
|
.order('name', ascending: true);
|
||||||
remote = rows
|
remote = rows
|
||||||
.map((r) =>
|
.map((r) =>
|
||||||
@@ -101,6 +102,7 @@ class ContactService extends GetxService {
|
|||||||
// Offline (vagy online-hiba) új rekord → outbox.
|
// Offline (vagy online-hiba) új rekord → outbox.
|
||||||
await _db.insertPendingContact({
|
await _db.insertPendingContact({
|
||||||
'local_uuid': _uuid.v4(),
|
'local_uuid': _uuid.v4(),
|
||||||
|
'project_id': c.projectId,
|
||||||
'name': c.name.trim(),
|
'name': c.name.trim(),
|
||||||
'address': c.address.trim(),
|
'address': c.address.trim(),
|
||||||
'phone': c.phone.trim(),
|
'phone': c.phone.trim(),
|
||||||
@@ -152,6 +154,7 @@ class ContactService extends GetxService {
|
|||||||
// local_uuid mint kliens-kulcs → az ismételt próba idempotens.
|
// local_uuid mint kliens-kulcs → az ismételt próba idempotens.
|
||||||
await _client.from('terepi_seged_contacts').upsert({
|
await _client.from('terepi_seged_contacts').upsert({
|
||||||
'client_uuid': m['local_uuid'],
|
'client_uuid': m['local_uuid'],
|
||||||
|
'project_id': m['project_id'],
|
||||||
'name': m['name'],
|
'name': m['name'],
|
||||||
'address': m['address'],
|
'address': m['address'],
|
||||||
'phone': m['phone'],
|
'phone': m['phone'],
|
||||||
@@ -169,13 +172,15 @@ class ContactService extends GetxService {
|
|||||||
return uploaded;
|
return uploaded;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<int> pendingCount() => _db.countPendingContacts();
|
Future<int> pendingCount(String projectId) =>
|
||||||
|
_db.countPendingContacts(projectId: projectId);
|
||||||
|
|
||||||
// ═════════════════════════════════════════════════════════════════
|
// ═════════════════════════════════════════════════════════════════
|
||||||
// CSV import (változatlan a korábbihoz képest)
|
// CSV import (változatlan a korábbihoz képest)
|
||||||
// ═════════════════════════════════════════════════════════════════
|
// ═════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
static Future<List<Contact>> parseCsv(File file) async {
|
static Future<List<Contact>> parseCsv(File file,
|
||||||
|
{required String projectId}) async {
|
||||||
final bytes = await file.readAsBytes();
|
final bytes = await file.readAsBytes();
|
||||||
String text;
|
String text;
|
||||||
try {
|
try {
|
||||||
@@ -256,6 +261,7 @@ class ContactService extends GetxService {
|
|||||||
final name = cell(r, iName);
|
final name = cell(r, iName);
|
||||||
if (name.isEmpty) continue;
|
if (name.isEmpty) continue;
|
||||||
contacts.add(Contact(
|
contacts.add(Contact(
|
||||||
|
projectId: projectId,
|
||||||
name: name,
|
name: name,
|
||||||
address: cell(r, iAddr),
|
address: cell(r, iAddr),
|
||||||
phone: cell(r, iPhone),
|
phone: cell(r, iPhone),
|
||||||
@@ -285,6 +291,7 @@ class ContactService extends GetxService {
|
|||||||
for (final c in contacts) {
|
for (final c in contacts) {
|
||||||
await _db.insertPendingContact({
|
await _db.insertPendingContact({
|
||||||
'local_uuid': _uuid.v4(),
|
'local_uuid': _uuid.v4(),
|
||||||
|
'project_id': c.projectId,
|
||||||
'name': c.name.trim(),
|
'name': c.name.trim(),
|
||||||
'address': c.address.trim(),
|
'address': c.address.trim(),
|
||||||
'phone': c.phone.trim(),
|
'phone': c.phone.trim(),
|
||||||
|
|||||||
Reference in New Issue
Block a user