import 'dart:async'; import 'package:flutter/material.dart'; import 'models/personel.dart'; import 'serial_service.dart'; import 'services/depolama.dart'; import 'services/okuma_mantigi.dart'; import 'screens/personel_ekle_screen.dart'; void main() { runApp(const KartOkuyucuApp()); } class KartOkuyucuApp extends StatelessWidget { const KartOkuyucuApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( title: 'FikoYT PDKS', debugShowCheckedModeBanner: false, theme: ThemeData.dark().copyWith( scaffoldBackgroundColor: const Color(0xFF0D1117), colorScheme: const ColorScheme.dark( primary: Color(0xFF00E5FF), secondary: Color(0xFF00BFA5), surface: Color(0xFF161B22), onSurface: Colors.white, ), cardColor: const Color(0xFF161B22), dividerColor: const Color(0xFF30363D), ), home: const KartOkuyucuPage(), ); } } // ─── Sabitler ─────────────────────────────────────────────────────────────── const _bgColor = Color(0xFF0D1117); const _cardColor = Color(0xFF161B22); const _borderColor = Color(0xFF30363D); const _accentCyan = Color(0xFF00E5FF); const _accentGreen = Color(0xFF00C853); const _accentRed = Color(0xFFEF5350); const _accentOrange = Color(0xFFFF9800); const _textDim = Color(0xFF8B949E); // ─── Ana Sayfa ─────────────────────────────────────────────────────────────── class KartOkuyucuPage extends StatefulWidget { const KartOkuyucuPage({super.key}); @override State createState() => _KartOkuyucuPageState(); } class _KartOkuyucuPageState extends State with TickerProviderStateMixin { final SerialService _serialService = SerialService(); final OkumaMantigi _mantigi = OkumaMantigi(); // ── Durum ───────────────────────────────────────────────────────────────── String _durum = 'Bağlanıyor...'; String? _port; bool _bagliMi = false; List> _gecmis = []; List _personeller = []; bool _personellerYukleniyor = true; // Okunan kart bilgileri Personel? _personel; String _tip = ''; bool _kartTakili = false; // ── Animasyon ───────────────────────────────────────────────────────────── late final AnimationController _pulseCtrl; late final AnimationController _kartCtrl; late final Animation _pulseAnim; late final Animation _kartAnim; @override void initState() { super.initState(); _pulseCtrl = AnimationController( vsync: this, duration: const Duration(seconds: 2), )..repeat(reverse: true); _kartCtrl = AnimationController( vsync: this, duration: const Duration(milliseconds: 400), ); _pulseAnim = Tween(begin: 0.6, end: 1.0).animate( CurvedAnimation(parent: _pulseCtrl, curve: Curves.easeInOut), ); _kartAnim = Tween(begin: 0.0, end: 1.0).animate( CurvedAnimation(parent: _kartCtrl, curve: Curves.elasticOut), ); _personelleriYukle(); _setupService(); } Future _personelleriYukle() async { setState(() => _personellerYukleniyor = true); try { final liste = await Depolama.tumPersoneller(); if (!mounted) return; _personeller = liste; _mantigi.personelListesi = liste; } catch (_) {} if (!mounted) return; setState(() => _personellerYukleniyor = false); } void _setupService() { _serialService.onConnected = () { if (!mounted) return; setState(() { _bagliMi = true; _port = _serialService.connectedPort; _durum = 'Bağlandı · ${_serialService.connectedPort}'; }); }; _serialService.onDataReceived = (data) { if (!mounted) return; final now = DateTime.now(); final zaman = '${now.hour.toString().padLeft(2, '0')}:${now.minute.toString().padLeft(2, '0')}:${now.second.toString().padLeft(2, '0')}'; if (data.startsWith('KART_TAKILDI:')) { final uid = data.substring(13); // Personel ekleme ekranı kart bekliyorsa UID'yi ona yönlendir, // normal giriş/çıkış akışına sokma. if (_mantigi.onPendingUid != null) { final callback = _mantigi.onPendingUid!; _mantigi.onPendingUid = null; callback(uid); return; } setState(() => _kartOkuBul(uid, zaman)); } else if (data.startsWith('KART_CIKTI:')) { final uid = data.substring(11); setState(() => _kartCiktiBul(uid, zaman)); } }; _serialService.onError = (err) { if (!mounted) return; setState(() { _bagliMi = false; _durum = 'Hata: $err'; }); }; Future.microtask(() async { if (!_serialService.isConnected) { await _serialService.connectAuto(); } }); } /// Kart takıldığında sadece kimin okutulduğunu gösterir. /// Giriş/çıkış kararı kart ÇEKİLDİĞİNDE (_kartCiktiBul) verilir. void _kartOkuBul(String uid, String zaman) { final personel = _mantigi.personelBul(uid); _kartTakili = true; _kartCtrl.forward(from: 0); if (personel == null) { _personel = null; _tip = ''; _durum = 'Tanınmayan Kart: $uid'; return; } _personel = personel; _tip = ''; _durum = '${personel.isim} okundu'; } /// Kart ÇEKİLDİĞİNDE asıl giriş/çıkış kararı burada verilir: /// Bugün bu kişi için daha önce giriş yapılmışsa → çıkış, /// yapılmamışsa → giriş olarak tek bir kayıt yazılır. void _kartCiktiBul(String uid, String zaman) { _kartTakili = false; final personel = _mantigi.personelBul(uid); if (personel == null) { // Tanınmayan kart çekildi, kayıt tutulmaz. return; } Future.microtask(() async { try { final durum = await Depolama.bugunDurumu(uid); final tip = durum.cikisYapilabilirMi() ? 'cikis' : 'giris'; final simdi = DateTime.now(); final yeniDurum = GunDurumu( personelId: uid, uid: uid, girisYapti: true, sonTip: tip, ); await Depolama.bugunDurumuKaydet(yeniDurum); await Depolama.kayitEk(OkumaKaydi( personelId: uid, uid: uid, tip: tip, zaman: simdi, )); if (!mounted) return; if (tip == 'giris') { // Yeni bir giriş: geçmişe yeni (henüz çıkışsız) satır ekle. setState(() { _tip = tip; _durum = '${personel.isim} — Giriş'; _gecmis.insert(0, { 'uid': uid, 'isim': personel.isim, 'departman': personel.departman, 'girisZamanDT': simdi, 'girisZaman': zaman, 'cikisZamanDT': null, 'cikisZaman': null, 'sure': null, 'tip': 'giris', }); }); } else { // Çıkış: aynı kişinin çıkışsız giriş satırını bul ve GÜNCELLE // (yeni satır ekleme). Map? girisKaydi; for (final k in _gecmis) { if (k['uid'] == uid && k['tip'] == 'giris' && k['cikisZamanDT'] == null) { girisKaydi = k; break; } } DateTime girisZamanDT; if (girisKaydi != null) { girisZamanDT = girisKaydi['girisZamanDT'] as DateTime; } else { // Bu oturumda eşleşen giriş yok (uygulama yeniden başlamış // olabilir); kalıcı kayıtlardan son girişi bul. final kayitlar = await Depolama.bugunKayitlari(uid); OkumaKaydi? sonGiris; for (final k in kayitlar) { if (k.tip == 'giris') { sonGiris = k; break; } } girisZamanDT = sonGiris?.zaman ?? simdi; } final fark = simdi.difference(girisZamanDT); final sureStr = _sureFormatla(fark); setState(() { _tip = tip; _durum = '${personel.isim} — Çıkış ($sureStr)'; if (girisKaydi != null) { girisKaydi['cikisZamanDT'] = simdi; girisKaydi['cikisZaman'] = zaman; girisKaydi['sure'] = sureStr; girisKaydi['tip'] = 'tamamlandi'; } else { _gecmis.insert(0, { 'uid': uid, 'isim': personel.isim, 'departman': personel.departman, 'girisZamanDT': girisZamanDT, 'girisZaman': _saatStr(girisZamanDT), 'cikisZamanDT': simdi, 'cikisZaman': zaman, 'sure': sureStr, 'tip': 'tamamlandi', }); } }); } _personelleriYukle(); } catch (_) {} }); } /// HH:mm:ss biçiminde saat metni üretir. String _saatStr(DateTime t) => '${t.hour.toString().padLeft(2, '0')}:${t.minute.toString().padLeft(2, '0')}:${t.second.toString().padLeft(2, '0')}'; /// Süreyi saat:dakika:saniye biçiminde döner (örn. 01:24:07). String _sureFormatla(Duration d) { final saat = d.inHours; final dakika = d.inMinutes % 60; final saniye = d.inSeconds % 60; return '${saat.toString().padLeft(2, '0')}:${dakika.toString().padLeft(2, '0')}:${saniye.toString().padLeft(2, '0')}'; } Future _yenidenBaglan() async { setState(() { _bagliMi = false; _durum = 'Yeniden bağlanıyor...'; }); _serialService.disconnect(); await Future.delayed(const Duration(milliseconds: 800)); if (mounted) _setupService(); } void _personelEkle() { showModalBottomSheet( context: context, backgroundColor: const Color(0xFF161B22), isScrollControlled: true, builder: (ctx) => Padding( padding: EdgeInsets.only( bottom: MediaQuery.of(ctx).padding.bottom, ), child: PersonelEkleScreen( mantigi: _mantigi, onClose: () => Navigator.pop(ctx), ), ), ); } @override void dispose() { _pulseCtrl.dispose(); _kartCtrl.dispose(); _serialService.disconnect(); super.dispose(); } // ─── BUILD ──────────────────────────────────────────────────────────────── @override Widget build(BuildContext context) { return Scaffold( backgroundColor: _bgColor, body: Column( children: [ _buildHeader(), Expanded( child: Row( children: [ // Sol panel – kart okuyucu Expanded(flex: 3, child: _buildKartPanel()), // Sağ panel – geçmiş Expanded(flex: 2, child: _buildGecmisPanel()), ], ), ), ], ), ); } // ── Header ──────────────────────────────────────────────────────────────── Widget _buildHeader() { return Container( height: 56, padding: const EdgeInsets.symmetric(horizontal: 24), decoration: const BoxDecoration( color: _cardColor, border: Border(bottom: BorderSide(color: _borderColor)), ), child: Row( children: [ const Icon(Icons.contactless, color: _accentCyan, size: 22), const SizedBox(width: 10), const Text( 'FikoYT', style: TextStyle( color: _accentCyan, fontSize: 16, fontWeight: FontWeight.w700, letterSpacing: 1.5, ), ), const Text( ' PDKS', style: TextStyle(color: Colors.white70, fontSize: 16, letterSpacing: 1), ), const Spacer(), _buildBaglantiChip(), const SizedBox(width: 12), IconButton( icon: const Icon(Icons.add_circle_outline_rounded, size: 22), color: _textDim, tooltip: 'Personel Ekle', onPressed: _personelEkle, ), IconButton( icon: const Icon(Icons.refresh_rounded, size: 20), color: _textDim, tooltip: 'Yeniden Bağlan', onPressed: _yenidenBaglan, ), ], ), ); } Widget _buildBaglantiChip() { final renk = _bagliMi ? _accentGreen : _accentRed; final label = _bagliMi ? (_port ?? 'Bağlı') : 'Bağlı Değil'; return AnimatedBuilder( animation: _pulseAnim, builder: (_, __) => Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), decoration: BoxDecoration( color: renk.withOpacity(0.12), borderRadius: BorderRadius.circular(20), border: Border.all(color: renk.withOpacity(0.4)), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Container( width: 6, height: 6, decoration: BoxDecoration( shape: BoxShape.circle, color: renk.withOpacity(_bagliMi ? _pulseAnim.value : 1.0), ), ), const SizedBox(width: 6), Text(label, style: TextStyle(color: renk, fontSize: 12, fontWeight: FontWeight.w600)), ], ), ), ); } // ── Kart Paneli ─────────────────────────────────────────────────────────── Widget _buildKartPanel() { return Padding( padding: const EdgeInsets.all(24), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Expanded(child: _buildKartOkuyucu()), const SizedBox(height: 16), _buildInfoRow(), ], ), ); } Widget _buildKartOkuyucu() { final Color renk = _kartTakili ? _accentGreen : _accentCyan; return Container( decoration: BoxDecoration( color: _cardColor, borderRadius: BorderRadius.circular(20), border: Border.all(color: _borderColor), ), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ // Kart ikonu – animasyonlu AnimatedBuilder( animation: Listenable.merge([_pulseAnim, _kartAnim]), builder: (_, __) { final glow = _kartTakili ? _kartAnim.value : _pulseAnim.value * 0.5; return Stack( alignment: Alignment.center, children: [ if (_kartTakili) ...List.generate(3, (i) { return Container( width: 120.0 + i * 40, height: 120.0 + i * 40, decoration: BoxDecoration( shape: BoxShape.circle, border: Border.all( color: renk.withOpacity( (1 - i * 0.3) * _kartAnim.value * 0.3), width: 1.5, ), ), ); }), // İç daire Container( width: 110, height: 110, decoration: BoxDecoration( shape: BoxShape.circle, color: renk.withOpacity(0.08 + glow * 0.1), border: Border.all( color: renk.withOpacity(0.4 + glow * 0.4), width: 2), ), child: Icon( _kartTakili ? Icons.credit_card : Icons.contactless_outlined, size: 48, color: renk.withOpacity(0.7 + glow * 0.3), ), ), ], ); }, ), const SizedBox(height: 28), // Durum yazısı Text( _takiliMi() ? 'KART OKUNDU' : 'KART BEKLENİYOR', style: TextStyle( color: _takiliMi() ? _accentGreen : _textDim, fontSize: 13, fontWeight: FontWeight.w700, letterSpacing: 3, ), ), const SizedBox(height: 12), // UID gösterimi AnimatedSwitcher( duration: const Duration(milliseconds: 300), child: Text( _takiliMi() ? _kartUidGoster() : '• • • • • • • •', key: ValueKey(_kartUidGoster() + _takiliMi().toString()), style: TextStyle( color: _takiliMi() ? Colors.white : _textDim, fontSize: _takiliMi() ? 20 : 18, fontWeight: FontWeight.w300, fontFamily: 'monospace', letterSpacing: 3, ), ), ), ], ), ); } bool _takiliMi() => _kartTakili; String _kartUidGoster() => _personel?.id ?? '-'; Widget _buildInfoRow() { return Row( children: [ Expanded(child: _buildInfoKart( Icons.people_rounded, 'Personel', '${_personeller.length}', _accentCyan, )), const SizedBox(width: 12), Expanded(child: _buildInfoKart( Icons.login_rounded, 'Giriş', '${_gecmis.length}', _accentGreen, )), const SizedBox(width: 12), Expanded(child: _buildInfoKart( Icons.logout_rounded, 'Çıkış', '${_gecmis.where((e) => e['tip'] == 'tamamlandi').length}', _accentOrange, )), ], ); } Widget _buildInfoKart(IconData icon, String baslik, String deger, Color renk) { return Container( padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 12), decoration: BoxDecoration( color: _cardColor, borderRadius: BorderRadius.circular(12), border: Border.all(color: _borderColor), ), child: Column( children: [ Icon(icon, color: renk, size: 20), const SizedBox(height: 6), Text(deger, style: TextStyle(color: renk, fontSize: 22, fontWeight: FontWeight.w700)), const SizedBox(height: 2), Text(baslik, style: const TextStyle(color: _textDim, fontSize: 11)), ], ), ); } // ── Geçmiş Paneli ───────────────────────────────────────────────────────── Widget _buildGecmisPanel() { return Container( margin: const EdgeInsets.fromLTRB(0, 24, 24, 24), decoration: BoxDecoration( color: _cardColor, borderRadius: BorderRadius.circular(20), border: Border.all(color: _borderColor), ), child: Column( children: [ // Panel başlığı Padding( padding: const EdgeInsets.fromLTRB(16, 14, 8, 14), child: Row( children: [ const Icon(Icons.format_list_bulleted_rounded, color: _accentCyan, size: 18), const SizedBox(width: 8), const Text('Kart Geçmişi', style: TextStyle(color: Colors.white, fontWeight: FontWeight.w600, fontSize: 14)), const Spacer(), if (_gecmis.isNotEmpty) TextButton.icon( onPressed: () => setState(() => _gecmis.clear()), icon: const Icon(Icons.delete_outline_rounded, size: 15), label: const Text('Temizle', style: TextStyle(fontSize: 12)), style: TextButton.styleFrom( foregroundColor: _textDim, padding: const EdgeInsets.symmetric(horizontal: 8), ), ), ], ), ), const Divider(color: _borderColor, height: 1), // Liste Expanded( child: _gecmis.isEmpty ? Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.contactless_outlined, color: _textDim.withOpacity(0.3), size: 48), const SizedBox(height: 12), const Text('Henüz kart okutulmadı', style: TextStyle(color: _textDim, fontSize: 13)), ], ), ) : ListView.separated( padding: const EdgeInsets.symmetric(vertical: 8), itemCount: _gecmis.length, separatorBuilder: (_, __) => const Divider(color: _borderColor, height: 1, indent: 16, endIndent: 16), itemBuilder: (_, i) => _buildGecmisItem(_gecmis[i], i), ), ), ], ), ); } Widget _buildGecmisItem(Map kayit, int index) { final tamamlandi = kayit['tip'] == 'tamamlandi'; final renk = tamamlandi ? _accentOrange : _accentGreen; final icon = tamamlandi ? Icons.logout_rounded : Icons.login_rounded; final girisZaman = kayit['girisZaman'] as String? ?? ''; final cikisZaman = kayit['cikisZaman'] as String?; final sure = kayit['sure'] as String?; final altYazi = (tamamlandi && cikisZaman != null) ? 'Giriş $girisZaman · Çıkış $cikisZaman' : 'Giriş $girisZaman · İçeride'; return Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), color: index == 0 ? renk.withOpacity(0.04) : Colors.transparent, child: Row( children: [ Container( width: 32, height: 32, decoration: BoxDecoration( color: renk.withOpacity(0.12), borderRadius: BorderRadius.circular(8), ), child: Icon(icon, color: renk, size: 16), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( kayit['isim'] as String? ?? 'Bilinmiyor', style: const TextStyle( color: Colors.white, fontSize: 13, fontWeight: FontWeight.w600, ), ), const SizedBox(height: 2), Text( altYazi, style: const TextStyle(color: _textDim, fontSize: 11), ), ], ), ), if (tamamlandi && sure != null) Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), decoration: BoxDecoration( color: renk.withOpacity(0.15), borderRadius: BorderRadius.circular(4), ), child: Text(sure, style: TextStyle( color: renk, fontSize: 11, fontWeight: FontWeight.w700, fontFamily: 'monospace', )), ) else if (index == 0) Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( color: renk.withOpacity(0.15), borderRadius: BorderRadius.circular(4), ), child: Text('YENİ', style: TextStyle(color: renk, fontSize: 9, fontWeight: FontWeight.w700)), ), ], ), ); } }