Kontaktok listázási hibájának javítása, csv export

This commit is contained in:
2026-07-08 22:13:30 +02:00
parent 7cd682fe59
commit 1ab656585b
9 changed files with 262 additions and 19 deletions
+29 -2
View File
@@ -1294,6 +1294,7 @@ class AppDatabase {
await db.execute('''
CREATE TABLE IF NOT EXISTS contacts_outbox (
local_uuid TEXT PRIMARY KEY,
project_id TEXT NOT NULL,
name TEXT NOT NULL,
address TEXT NOT NULL DEFAULT '',
phone TEXT NOT NULL DEFAULT '',
@@ -1302,6 +1303,8 @@ class AppDatabase {
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 {
@@ -1309,8 +1312,18 @@ class AppDatabase {
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;
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');
}
@@ -1327,10 +1340,24 @@ class AppDatabase {
where: 'local_uuid = ?', whereArgs: [localUuid]);
}
Future<int> countPendingContacts() async {
Future<int> countPendingContacts({String? projectId}) async {
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(
await db.rawQuery('SELECT COUNT(*) FROM contacts_outbox')) ??
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)');
// }
}
+77
View File
@@ -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',
));
}
}
+11 -4
View File
@@ -46,8 +46,8 @@ class ContactService extends GetxService {
/// 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
/// az `isPending` jelöli — a UI kis felhő-ikonnal mutatja őket.
Future<List<ContactWithState>> listMerged() async {
final pending = (await _db.listPendingContacts())
Future<List<ContactWithState>> listMerged(String projectId) async {
final pending = (await _db.listPendingContacts(projectId: projectId))
.map((m) => ContactWithState(
contact: Contact.fromMap(m),
isPending: true,
@@ -60,6 +60,7 @@ class ContactService extends GetxService {
final rows = await _client
.from('terepi_seged_contacts')
.select()
.eq('project_id', projectId)
.order('name', ascending: true);
remote = rows
.map((r) =>
@@ -101,6 +102,7 @@ class ContactService extends GetxService {
// Offline (vagy online-hiba) új rekord → outbox.
await _db.insertPendingContact({
'local_uuid': _uuid.v4(),
'project_id': c.projectId,
'name': c.name.trim(),
'address': c.address.trim(),
'phone': c.phone.trim(),
@@ -152,6 +154,7 @@ class ContactService extends GetxService {
// local_uuid mint kliens-kulcs → az ismételt próba idempotens.
await _client.from('terepi_seged_contacts').upsert({
'client_uuid': m['local_uuid'],
'project_id': m['project_id'],
'name': m['name'],
'address': m['address'],
'phone': m['phone'],
@@ -169,13 +172,15 @@ class ContactService extends GetxService {
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)
// ═════════════════════════════════════════════════════════════════
static Future<List<Contact>> parseCsv(File file) async {
static Future<List<Contact>> parseCsv(File file,
{required String projectId}) async {
final bytes = await file.readAsBytes();
String text;
try {
@@ -256,6 +261,7 @@ class ContactService extends GetxService {
final name = cell(r, iName);
if (name.isEmpty) continue;
contacts.add(Contact(
projectId: projectId,
name: name,
address: cell(r, iAddr),
phone: cell(r, iPhone),
@@ -285,6 +291,7 @@ class ContactService extends GetxService {
for (final c in contacts) {
await _db.insertPendingContact({
'local_uuid': _uuid.v4(),
'project_id': c.projectId,
'name': c.name.trim(),
'address': c.address.trim(),
'phone': c.phone.trim(),