import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_foreground_task/flutter_foreground_task.dart'; import '../../Utils/image_constant.dart'; import '../../Utils/size_utils.dart'; import '../../theme/app_style.dart'; import '../../widgets/app_bar/appbar_image.dart'; import '../../widgets/app_bar/appbar_title.dart'; import '../../widgets/app_bar/custom_app_bar.dart'; import '../../widgets/custom_button.dart'; import 'attendance_math.dart'; import 'attendance_service.dart'; import 'attendance_storage.dart'; // --------------------------------------------------------------------------- // Duty punch page: Punch In -> background records one GPS dot every 5 min -> // Punch Out -> summary (points + km). Sessions stay on the phone (sqflite). // --------------------------------------------------------------------------- class PunchScreen extends StatefulWidget { const PunchScreen({super.key}); @override State createState() => _PunchScreenState(); } class _PunchScreenState extends State { bool _punchedIn = false; bool _busy = false; String? _sessionId; String? _punchInTime; int _points = 0; double _km = 0; List> _history = []; Timer? _clock; void _onTaskData(Object data) { if (data is Map && mounted) { setState(() { _points = (data['points'] as num?)?.toInt() ?? _points; _km = (data['km'] as num?)?.toDouble() ?? _km; }); } } @override void initState() { super.initState(); FlutterForegroundTask.addTaskDataCallback(_onTaskData); _refresh(); _clock = Timer.periodic(const Duration(seconds: 30), (_) { if (mounted && _punchedIn) setState(() {}); }); } @override void dispose() { FlutterForegroundTask.removeTaskDataCallback(_onTaskData); _clock?.cancel(); super.dispose(); } Future _refresh() async { final bool open = await AttendanceService.isPunchedIn; final String? sid = await AttendanceStorage.getActiveSession(); List> history = await AttendanceStorage.getRecentSessions(limit: 10); int points = 0; double km = 0; String? punchIn; if (open && sid != null) { final session = await AttendanceStorage.getSession(sid); punchIn = session?['punch_in'] as String?; final pts = await AttendanceStorage.getPoints(sid); points = pts.length; km = _sumKm(pts); } if (!mounted) return; setState(() { _punchedIn = open; _sessionId = sid; _punchInTime = punchIn; _points = points; _km = km; _history = history; }); } double _sumKm(List> pts) { double meters = 0; for (int i = 1; i < pts.length; i++) { meters += haversineMeters( (pts[i - 1]['lat'] as num).toDouble(), (pts[i - 1]['lng'] as num).toDouble(), (pts[i]['lat'] as num).toDouble(), (pts[i]['lng'] as num).toDouble(), ); } return meters / 1000.0; } Future _onPunchIn() async { setState(() => _busy = true); try { final PermissionResult perm = await AttendanceService.ensurePermissions(); if (!perm.ok) { _msg(perm.message, true); return; } if (perm.message != ' granted') _msg(perm.message, false); await AttendanceService.punchIn(); if (mounted) { await AttendanceService.maybeAskBatteryExemption(context); } await _refresh(); _msg('Punched in - tracking every 5 min', false); } catch (e) { _msg('Punch in failed: $e', true); } finally { if (mounted) setState(() => _busy = false); } } Future _onPunchOut() async { setState(() => _busy = true); try { final Map summary = await AttendanceService.punchOut(); await _refresh(); if (!mounted) return; final double km = (summary['km'] as num).toDouble(); final int pts = summary['points'] as int; showDialog( context: context, builder: (c) => AlertDialog( title: const Text('Duty summary'), content: Text( 'Points recorded: $pts\n' 'Distance (straight line): ${km.toStringAsFixed(2)} km\n' 'Road distance: coming in step 4 (free API).', ), actions: [ TextButton( onPressed: () => Navigator.of(c).pop(), child: const Text('OK'), ), ], ), ); } catch (e) { _msg('Punch out failed: $e', true); } finally { if (mounted) setState(() => _busy = false); } } void _msg(String text, bool isError) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(text), backgroundColor: isError ? Colors.red : Colors.green, ), ); } @override Widget build(BuildContext context) { return Scaffold( appBar: CustomAppBar( height: getVerticalSize(49), leadingWidth: 40, leading: AppbarImage( height: getSize(24), width: getSize(24), svgPath: ImageConstant.imgArrowleftBlueGray900, margin: getMargin(left: 16, top: 12, bottom: 13), onTap: () => Navigator.pop(context), ), centerTitle: true, title: AppbarTitle(text: 'Duty Punch'), ), body: SingleChildScrollView( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ _statusCard(), const SizedBox(height: 16), CustomButton( height: getVerticalSize(50), text: _busy ? 'Please wait...' : (_punchedIn ? 'Punch Out' : 'Punch In'), margin: getMargin(top: 8, bottom: 5), onTap: _busy ? () {} : (_punchedIn ? _onPunchOut : _onPunchIn), ), const SizedBox(height: 8), Text( 'Keeps recording one location every 5 min, even with the app ' 'minimized. For Xiaomi/Oppo/Vivo/Samsung allow Unrestricted ' 'battery use or tracking may pause.', style: AppStyle.txtGilroyMedium16Bluegray900.copyWith( fontSize: 12, ), ), const SizedBox(height: 20), Text('Recent duties', style: AppStyle.txtGilroyMedium16Bluegray900.copyWith( fontWeight: FontWeight.bold, )), const SizedBox(height: 8), ..._history.map(_historyTile), ], ), ), ); } Widget _statusCard() { final Color dot = _punchedIn ? Colors.green : Colors.grey; return Card( elevation: 2, child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Container( width: 12, height: 12, decoration: BoxDecoration(color: dot, shape: BoxShape.circle), ), const SizedBox(width: 8), Text( _punchedIn ? 'Punched IN' : 'Punched OUT', style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold), ), ], ), const SizedBox(height: 12), Text('Session: ${_sessionId ?? '-'}'), if (_punchInTime != null) Text('Since: $_punchInTime'), const SizedBox(height: 8), Text('Points: $_points', style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600)), Text('Distance: ${_km.toStringAsFixed(2)} km', style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600)), ], ), ), ); } Widget _historyTile(Map s) { final String status = (s['status'] ?? '') as String; final double km = ((s['total_km_haversine'] ?? 0) as num).toDouble(); return Card( child: ListTile( title: Text('${s['punch_in']}', style: const TextStyle(fontSize: 12)), subtitle: Text( 'Out: ${s['punch_out'] ?? '-'} • ${km.toStringAsFixed(2)} km', style: const TextStyle(fontSize: 11)), trailing: Text(status, style: TextStyle( color: status == 'OPEN' ? Colors.green : Colors.grey, fontWeight: FontWeight.bold, )), ), ); } }