import 'dart:io'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:url_launcher/url_launcher.dart'; import '../../../../models/contact.dart'; import '../../../../services/contact_service.dart'; import '../../../../services/permission_service.dart'; import '../controllers/contacts_controller.dart'; import 'contact_edit_view.dart'; /// Kapcsolatok lista — csak bejelentkezett ÉS jogosult felhasználóknak. /// /// A jogosultság-ellenőrzés kétszintű: a UI a PermissionService-szel /// elrejti/tiltja az oldalt (barátságos üzenet), a tényleges védelmet /// pedig a Supabase-oldali RLS adja (az RLS-elutasítás is kezelt). class ContactsView extends StatelessWidget { const ContactsView({super.key}); @override Widget build(BuildContext context) { // Jogosultság-kapu — ha nincs joga, be sem töltjük a listát. if (Get.isRegistered() && !PermissionService.to.canContacts) { return Scaffold( appBar: AppBar(title: const Text('Kapcsolatok')), body: const _NoAccess(), ); } final c = Get.put(ContactsController()); return Scaffold( appBar: AppBar( title: const Text('Kapcsolatok'), actions: [ IconButton( icon: const Icon(Icons.upload_file), tooltip: 'Import CSV', onPressed: () => _importCsv(c), ), IconButton( icon: const Icon(Icons.refresh), tooltip: 'Frissítés', onPressed: c.load, ), Obx(() => c.pendingCount.value == 0 ? const SizedBox.shrink() : IconButton( icon: Badge( label: Text('${c.pendingCount.value}'), child: const Icon(Icons.cloud_upload_outlined), ), tooltip: 'Várólista feltöltése', onPressed: c.flushNow, )) ], ), floatingActionButton: FloatingActionButton.extended( onPressed: () async { final saved = await Get.to(() => const ContactEditView()); if (saved is Contact) c.load(); }, icon: const Icon(Icons.person_add_alt), label: const Text('Új'), ), body: Column( children: [ Padding( padding: const EdgeInsets.fromLTRB(12, 8, 12, 4), child: TextField( decoration: const InputDecoration( hintText: 'Keresés név, telefon, e-mail…', prefixIcon: Icon(Icons.search), isDense: true, border: OutlineInputBorder(), ), onChanged: (v) => c.search.value = v, ), ), Expanded( child: Obx(() { if (c.isLoading.value) { return const Center(child: CircularProgressIndicator()); } if (c.error.value.isNotEmpty) { return _ErrorState(message: c.error.value, onRetry: c.load); } final rows = c.filtered; if (rows.isEmpty) { return const Center( child: Text('Nincs megjeleníthető kapcsolat.')); } return ListView.separated( itemCount: rows.length, separatorBuilder: (_, __) => const Divider(height: 1), itemBuilder: (_, i) => _ContactTile( item: rows[i], onEdit: () async { final saved = await Get.to(() => const ContactEditView(), arguments: rows[i].contact); if (saved != null) c.load(); }, onDelete: () => _confirmDelete(c, rows[i]), ), ); }), ), ], ), ); } Future _confirmDelete( ContactsController c, ContactWithState item) async { final ok = await Get.dialog(AlertDialog( title: const Text('Törlés'), content: Text('Biztosan törlöd: ${item.contact.name}?'), actions: [ TextButton( onPressed: () => Get.back(result: false), child: const Text('Mégse')), FilledButton( onPressed: () => Get.back(result: true), child: const Text('Törlés')), ], )); if (ok == true) await c.delete(item); } Future _importCsv(ContactsController c) async { try { final result = await FilePicker.platform.pickFiles(type: FileType.any); final picked = result?.files.single; final path = picked?.path; if (path == null) return; final name = picked!.name.toLowerCase(); if (!(name.endsWith('.csv') || name.endsWith('.txt'))) { Get.snackbar('Import', 'CSV vagy TXT fájlt válassz.', snackPosition: SnackPosition.BOTTOM); return; } final parsed = await ContactService.parseCsv(File(path)); if (parsed.isEmpty) { Get.snackbar('Import', 'Nem található érvényes sor a fájlban.', snackPosition: SnackPosition.BOTTOM); return; } final ok = await Get.dialog(AlertDialog( title: const Text('Import megerősítése'), content: Text('${parsed.length} kapcsolat importálása?\n\n' 'Első néhány: ' '${parsed.take(3).map((e) => e.name).join(', ')}…'), actions: [ TextButton( onPressed: () => Get.back(result: false), child: const Text('Mégse')), FilledButton( onPressed: () => Get.back(result: true), child: const Text('Import')), ], )); if (ok != true) return; final inserted = await ContactService.to.importMany(parsed); await c.load(); Get.snackbar('Import kész', '$inserted kapcsolat importálva.', snackPosition: SnackPosition.BOTTOM); } catch (e) { Get.snackbar('Import hiba', e.toString(), snackPosition: SnackPosition.BOTTOM); } } } // ── Sor ────────────────────────────────────────────────────────────── class _ContactTile extends StatelessWidget { final ContactWithState item; final VoidCallback onEdit; final VoidCallback onDelete; const _ContactTile( {required this.item, required this.onEdit, required this.onDelete}); Contact get contact => item.contact; @override Widget build(BuildContext context) { final subtitle = [ if (contact.phone.isNotEmpty) contact.phone, if (contact.email.isNotEmpty) contact.email, ].join(' · '); return ListTile( leading: CircleAvatar( child: Text(contact.name.isNotEmpty ? contact.name[0].toUpperCase() : '?'), ), title: Text(contact.name), subtitle: subtitle.isEmpty ? null : Text(subtitle), onTap: () => _showDetail(context), trailing: item.isPending ? const Tooltip( message: 'Feltöltésre vár', child: Icon(Icons.cloud_upload_outlined, size: 20, color: Colors.orange)) : PopupMenuButton( onSelected: (a) { switch (a) { case 'edit': onEdit(); case 'delete': onDelete(); } }, itemBuilder: (_) => const [ PopupMenuItem(value: 'edit', child: Text('Szerkesztés')), PopupMenuItem(value: 'delete', child: Text('Törlés')), ], ), ); } void _showDetail(BuildContext context) { Get.dialog(AlertDialog( title: Text(contact.name), content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ if (contact.address.isNotEmpty) _row(Icons.location_on_outlined, contact.address), if (contact.phone.isNotEmpty) _row(Icons.phone_outlined, contact.phone, onTap: () => _launch('tel:${contact.phone}')), if (contact.email.isNotEmpty) _row(Icons.email_outlined, contact.email, onTap: () => _launch('mailto:${contact.email}')), if (contact.note.isNotEmpty) _row(Icons.notes_outlined, contact.note), ], ), actions: [ TextButton( onPressed: () { Get.back(); onEdit(); }, child: const Text('Szerkesztés')), TextButton(onPressed: Get.back, child: const Text('Bezár')), ], )); } Widget _row(IconData icon, String text, {VoidCallback? onTap}) { return Padding( padding: const EdgeInsets.symmetric(vertical: 4), child: InkWell( onTap: onTap, child: Row( children: [ Icon(icon, size: 18, color: Colors.grey), const SizedBox(width: 10), Expanded( child: Text(text, style: TextStyle( color: onTap != null ? Colors.blue : null, decoration: onTap != null ? TextDecoration.underline : null, )), ), ], ), ), ); } Future _launch(String uri) async { final u = Uri.parse(uri); if (await canLaunchUrl(u)) await launchUrl(u); } } // ── Állapot-widgetek ───────────────────────────────────────────────── class _NoAccess extends StatelessWidget { const _NoAccess(); @override Widget build(BuildContext context) { return Center( child: Padding( padding: const EdgeInsets.all(32), child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.lock_outline, size: 48, color: Colors.grey.shade400), const SizedBox(height: 12), const Text( 'Nincs jogosultságod a kapcsolatok eléréséhez.\n' 'Kérj hozzáférést a rendszergazdától.', textAlign: TextAlign.center, ), ], ), ), ); } } class _ErrorState extends StatelessWidget { final String message; final VoidCallback onRetry; const _ErrorState({required this.message, required this.onRetry}); @override Widget build(BuildContext context) { return Center( child: Padding( padding: const EdgeInsets.all(32), child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.cloud_off, size: 44, color: Colors.grey.shade400), const SizedBox(height: 12), Text(message, textAlign: TextAlign.center), const SizedBox(height: 12), FilledButton(onPressed: onRetry, child: const Text('Újra')), ], ), ), ); } }