import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../../core/theme.dart'; import '../../providers/home_provider.dart'; import '../../providers/attendance_provider.dart'; import '../../providers/face_provider.dart'; import '../../widgets/organisms/presensi_section.dart'; import '../../widgets/molecules/stat_card.dart'; import '../../widgets/molecules/attendance_time_card.dart'; import '../../widgets/organisms/presensi_detail_sheet.dart'; import '../../models/presensi_model.dart'; import '../../widgets/atoms/fade_in_up.dart'; import 'presensi_map_screen.dart'; class PresensiScreen extends StatefulWidget { const PresensiScreen({Key? key}) : super(key: key); @override State createState() => _PresensiScreenState(); } class _PresensiScreenState extends State { @override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { context.read().fetchHistory(); }); } static const List _monthNames = [ 'Januari', 'Februari', 'Maret', 'April', 'Mei', 'Juni', 'Juli', 'Agustus', 'September', 'Oktober', 'November', 'Desember', ]; String _getMonthName(int month) => _monthNames[month - 1]; void _goToPrevMonth(AttendanceProvider p) { if (p.selectedMonth == 1) { p.setYear(p.selectedYear - 1); p.setMonth(12); } else { p.setMonth(p.selectedMonth - 1); } } void _goToNextMonth(AttendanceProvider p) { final now = DateTime.now(); if (p.selectedYear == now.year && p.selectedMonth == now.month) return; if (p.selectedMonth == 12) { p.setYear(p.selectedYear + 1); p.setMonth(1); } else { p.setMonth(p.selectedMonth + 1); } } bool _isCurrentMonth(AttendanceProvider p) { final now = DateTime.now(); return p.selectedYear == now.year && p.selectedMonth == now.month; } @override Widget build(BuildContext context) { final homeProvider = context.watch(); final attendanceProvider = context.watch(); final faceProvider = context.watch(); final faceStatus = faceProvider.faceStatus2['status']; return Scaffold( backgroundColor: AppTheme.bgLight, appBar: AppBar( title: Text("Presensi Karyawan", style: AppTheme.heading3), centerTitle: false, backgroundColor: AppTheme.bgLight, elevation: 0, actions: [ InkWell( onTap: () { _showScheduleModal(context); }, child: Container( margin: const EdgeInsets.only(right: AppTheme.spacingMd), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), decoration: BoxDecoration( color: AppTheme.primaryDark.withOpacity(0.1), borderRadius: BorderRadius.circular(AppTheme.radiusFull), ), child: Row( children: [ Text(homeProvider.presensiToday?.shift ?? "-", style: AppTheme.labelMedium.copyWith(color: AppTheme.primaryDark)), const SizedBox(width: 8), const Icon(Icons.calendar_month, color: AppTheme.primaryDark, size: 20), ], ), ), ), ], ), body: RefreshIndicator( onRefresh: () async { await homeProvider.fetchDashboardData(force: true); await attendanceProvider.fetchHistory(force: true); faceProvider.loadFaceStatus(); }, child: SingleChildScrollView( padding: const EdgeInsets.all(AppTheme.spacingMd), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (faceStatus == 'pending') Container( margin: const EdgeInsets.only(bottom: AppTheme.spacingMd), padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: AppTheme.primaryBlue.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(AppTheme.radiusMd), border: Border.all(color: AppTheme.primaryBlue.withValues(alpha: 0.3)), ), child: Row( children: [ const Icon(Icons.hourglass_top_rounded, color: AppTheme.primaryBlue, size: 20), const SizedBox(width: 12), Expanded( child: Text( "Data wajah Anda sedang dalam proses verifikasi HRD. Presensi belum dapat dilakukan.", style: AppTheme.bodySmall.copyWith(color: AppTheme.primaryBlue, fontWeight: FontWeight.w500), ), ), ], ), ), if (faceStatus == 'rejected') Container( margin: const EdgeInsets.only(bottom: AppTheme.spacingMd), padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: AppTheme.statusRed.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(AppTheme.radiusMd), border: Border.all(color: AppTheme.statusRed.withValues(alpha: 0.3)), ), child: Row( children: [ const Icon(Icons.cancel_rounded, color: AppTheme.statusRed, size: 20), const SizedBox(width: 12), Expanded( child: Text( "Registrasi wajah ditolak oleh HRD. Silakan lakukan registrasi ulang di menu Profil.", style: AppTheme.bodySmall.copyWith(color: AppTheme.statusRed, fontWeight: FontWeight.w500), ), ), ], ), ), PresensiSection( presensiToday: homeProvider.presensiToday, onPresensi: () { _handlePresensi(homeProvider.presensiToday != null && homeProvider.presensiToday!.sudahAbsenMasuk ? 'pulang' : 'masuk'); }, isLoading: attendanceProvider.isLoading, ), const SizedBox(height: AppTheme.spacingXl), Text("History Presensi", style: AppTheme.heading3), const SizedBox(height: 12), // Month/Year Picker Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), decoration: BoxDecoration( color: AppTheme.bgWhite, borderRadius: BorderRadius.circular(AppTheme.radiusMd), boxShadow: AppTheme.shadowSm, ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ InkWell( onTap: () => _goToPrevMonth(attendanceProvider), borderRadius: BorderRadius.circular(AppTheme.radiusFull), child: Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: AppTheme.primaryDark.withValues(alpha: 0.08), shape: BoxShape.circle, ), child: const Icon(Icons.chevron_left, size: 20, color: AppTheme.primaryDark), ), ), InkWell( onTap: () => _showMonthYearPicker(context, attendanceProvider), borderRadius: BorderRadius.circular(AppTheme.radiusSm), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), child: Row( mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.calendar_month, size: 18, color: AppTheme.primaryDark), const SizedBox(width: 8), Text( "${_getMonthName(attendanceProvider.selectedMonth)} ${attendanceProvider.selectedYear}", style: AppTheme.labelLarge.copyWith(fontSize: 16, color: AppTheme.primaryDark), ), ], ), ), ), InkWell( onTap: _isCurrentMonth(attendanceProvider) ? null : () => _goToNextMonth(attendanceProvider), borderRadius: BorderRadius.circular(AppTheme.radiusFull), child: Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: _isCurrentMonth(attendanceProvider) ? AppTheme.textTertiary.withValues(alpha: 0.1) : AppTheme.primaryDark.withValues(alpha: 0.08), shape: BoxShape.circle, ), child: Icon( Icons.chevron_right, size: 20, color: _isCurrentMonth(attendanceProvider) ? AppTheme.textTertiary : AppTheme.primaryDark, ), ), ), ], ), ), const SizedBox(height: AppTheme.spacingMd), // History List if (attendanceProvider.isLoading) const Center( child: Padding( padding: EdgeInsets.all(32), child: CircularProgressIndicator(), ), ) else if (attendanceProvider.historyList.isEmpty) Center( child: Container( padding: const EdgeInsets.all(AppTheme.spacingXl), child: Column( children: [ Icon(Icons.calendar_today, size: 48, color: AppTheme.textTertiary), const SizedBox(height: AppTheme.spacingMd), Text( "Tidak ada data presensi bulan ini", style: AppTheme.bodyMedium.copyWith(color: AppTheme.textSecondary), ), ], ), ), ) else ListView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), itemCount: attendanceProvider.historyList.length, itemBuilder: (context, index) { final history = attendanceProvider.historyList[index]; return FadeInUp( delayMs: index * 50, child: InkWell( onTap: () => _showPresensiDetail(context, history), borderRadius: BorderRadius.circular(AppTheme.radiusLg), child: Container( margin: const EdgeInsets.only(bottom: AppTheme.spacingMd), padding: const EdgeInsets.all(AppTheme.spacingMd), decoration: BoxDecoration( color: AppTheme.bgWhite, borderRadius: BorderRadius.circular(AppTheme.radiusLg), boxShadow: AppTheme.shadowSm, ), child: Column( children: [ Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ const Icon(Icons.calendar_today, size: 16, color: AppTheme.primaryDark), const SizedBox(width: 8), Expanded( child: Text( history.tanggal, style: AppTheme.labelMedium, overflow: TextOverflow.ellipsis, ), ), ], ), const SizedBox(height: 8), Wrap( spacing: 6, runSpacing: 6, children: [ if (history.statusValidasi == 3) _buildBadge("Ditolak", AppTheme.statusRed), if (history.statusValidasi == 2) _buildBadge("Menunggu Validasi", AppTheme.statusOrange, bgColor: AppTheme.statusYellow), if (!history.verifikasiWajah && history.statusMasuk != 'Izin' && history.statusMasuk != 'Cuti' && history.statusMasuk != 'Sakit' && history.statusMasuk != 'Alpha') _buildBadge("Wajah Tidak Terverifikasi", AppTheme.statusOrange), if (history.keteranganLuarRadius != null) _buildBadge("Luar Radius", AppTheme.primaryDark), if (history.statusMasuk != null && history.statusMasuk != '-') _buildBadge(history.statusMasuk!, _getStatusColor(history.statusMasuk!)), if (history.statusPulang != null && history.statusPulang != '-' && history.statusPulang != 'Tepat Waktu' && history.statusPulang != history.statusMasuk) _buildBadge(history.statusPulang!, _getStatusColor(history.statusPulang!)), ], ), ], ), if ((history.jamMasuk != null && history.jamMasuk != '-') || (history.jamPulang != null && history.jamPulang != '-')) ...[ const SizedBox(height: AppTheme.spacingMd), AttendanceTimeCard( jamMasuk: history.jamMasuk ?? '-', jamPulang: history.jamPulang ?? '-', totalJam: history.totalJam, ), const SizedBox(height: 8), ], Row( mainAxisAlignment: MainAxisAlignment.end, children: [ Text( "Lihat Detail", style: AppTheme.bodySmall.copyWith(color: AppTheme.primaryBlue, fontWeight: FontWeight.w500), ), const SizedBox(width: 4), const Icon(Icons.chevron_right, size: 16, color: AppTheme.primaryBlue), ], ), ], ), ), ), ); }, ), const SizedBox(height: 16), ], ), ), ), ); } Widget _buildBadge(String label, Color color, {Color? bgColor}) { return Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), decoration: BoxDecoration( color: (bgColor ?? color).withValues(alpha: 0.1), borderRadius: BorderRadius.circular(AppTheme.radiusFull), ), child: Text( label, style: AppTheme.bodySmall.copyWith( color: color, fontWeight: FontWeight.w600, fontSize: 10, ), ), ); } void _showMonthYearPicker(BuildContext context, AttendanceProvider provider) { final now = DateTime.now(); int tempMonth = provider.selectedMonth; int tempYear = provider.selectedYear; showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: Colors.transparent, builder: (ctx) { return StatefulBuilder( builder: (ctx, setModalState) { return Container( decoration: const BoxDecoration( color: AppTheme.bgWhite, borderRadius: BorderRadius.vertical(top: Radius.circular(AppTheme.radiusXl)), ), padding: const EdgeInsets.fromLTRB(24, 16, 24, 32), child: Column( mainAxisSize: MainAxisSize.min, children: [ Container( width: 40, height: 4, decoration: BoxDecoration( color: AppTheme.textTertiary.withValues(alpha: 0.3), borderRadius: BorderRadius.circular(2), ), ), const SizedBox(height: 20), Text("Pilih Bulan", style: AppTheme.heading3), const SizedBox(height: 16), // Year selector row Row( mainAxisAlignment: MainAxisAlignment.center, children: [ IconButton( onPressed: () => setModalState(() => tempYear--), icon: const Icon(Icons.chevron_left), ), Text("$tempYear", style: AppTheme.labelLarge.copyWith(fontSize: 18)), IconButton( onPressed: tempYear >= now.year ? null : () => setModalState(() => tempYear++), icon: const Icon(Icons.chevron_right), ), ], ), const SizedBox(height: 12), // Month grid GridView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 3, childAspectRatio: 2.5, mainAxisSpacing: 8, crossAxisSpacing: 8, ), itemCount: 12, itemBuilder: (ctx, idx) { final m = idx + 1; final isSelected = m == tempMonth && tempYear == provider.selectedYear; final isFuture = tempYear == now.year && m > now.month; return GestureDetector( onTap: isFuture ? null : () { setModalState(() => tempMonth = m); }, child: AnimatedContainer( duration: const Duration(milliseconds: 200), decoration: BoxDecoration( color: isSelected ? AppTheme.primaryDark : isFuture ? AppTheme.textTertiary.withValues(alpha: 0.05) : m == tempMonth ? AppTheme.primaryDark.withValues(alpha: 0.1) : AppTheme.bgCard, borderRadius: BorderRadius.circular(AppTheme.radiusSm), ), alignment: Alignment.center, child: Text( _monthNames[idx].substring(0, 3), style: AppTheme.labelMedium.copyWith( color: isSelected ? Colors.white : isFuture ? AppTheme.textTertiary : m == tempMonth ? AppTheme.primaryDark : AppTheme.textPrimary, ), ), ), ); }, ), const SizedBox(height: 20), SizedBox( width: double.infinity, child: ElevatedButton( onPressed: () { Navigator.pop(ctx); provider.setYear(tempYear); provider.setMonth(tempMonth); }, style: ElevatedButton.styleFrom( backgroundColor: AppTheme.primaryDark, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 14), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(AppTheme.radiusMd), ), ), child: Text("Terapkan", style: AppTheme.labelLarge.copyWith(color: Colors.white)), ), ), ], ), ); }, ); }, ); } void _showScheduleModal(BuildContext context) { Navigator.pushNamed(context, '/schedule'); } Color _getStatusColor(String status) { switch (status.toLowerCase()) { case 'tepat waktu': case 'datang awal': return AppTheme.statusGreen; case 'terlambat': case 'pulang awal': case 'alpha': return AppTheme.statusRed; case 'izin': case 'sakit': case 'cuti': return AppTheme.statusOrange; default: return AppTheme.primaryDark; } } void _showPresensiDetail(BuildContext context, PresensiHistoryModel history) { showModalBottomSheet( context: context, isScrollControlled: true, barrierColor: Colors.black.withValues(alpha: 0.1), backgroundColor: Colors.transparent, builder: (context) => PresensiDetailSheet(history: history), ); } Future _handlePresensi(String type) async { if (!mounted) return; try { final faceProvider = context.read(); showDialog( context: context, barrierDismissible: false, builder: (context) => const Center(child: CircularProgressIndicator()), ); await faceProvider.loadFaceStatus(); if (!mounted) return; Navigator.pop(context); // Close loading dialog final faceStatus = faceProvider.faceStatus2['status']; if (faceStatus != 'verified') { String msg = 'Wajah Anda belum terverifikasi oleh HRD.'; if (faceStatus == 'not_registered') { msg = 'Anda belum melakukan registrasi wajah.'; } else if (faceStatus == 'pending') { msg = 'Registrasi wajah Anda sedang menunggu verifikasi/training oleh HRD.'; } else if (faceStatus == 'rejected') { msg = 'Registrasi wajah Anda ditolak oleh HRD. Harap lakukan registrasi ulang.'; } showDialog( context: context, builder: (context) => AlertDialog( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), title: const Row( children: [ Icon(Icons.warning_amber_rounded, color: Colors.orange, size: 28), SizedBox(width: 8), Text("Presensi Ditolak", style: TextStyle(fontWeight: FontWeight.bold)), ], ), content: Text(msg), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: const Text("Tutup"), ), ], ), ); return; } final result = await Navigator.push>( context, MaterialPageRoute( builder: (context) => PresensiMapScreen(type: type), ), ); if (!mounted) return; if (result != null && result['success'] == true) { await context.read().fetchDashboardData(force: true); await context.read().fetchHistory(force: true); _showResultDialog(true, "Absensi Berhasil!", result['message'] ?? "Data absensi Anda telah tercatat."); } } catch (e) { if (mounted) Navigator.pop(context); ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text("Gagal memeriksa status verifikasi wajah. Coba lagi.")), ); } } void _showResultDialog(bool success, String title, String message) { showDialog( context: context, builder: (context) => AlertDialog( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), title: Row( children: [ Icon( success ? Icons.check_circle : Icons.error, color: success ? AppTheme.statusGreen : AppTheme.statusRed, ), const SizedBox(width: 8), Text(title, style: AppTheme.heading3), ], ), content: Text(message, style: AppTheme.bodyMedium), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: Text("Tutup", style: AppTheme.labelLarge), ), ], ), ); } }