import 'dart:convert'; import 'dart:math'; import 'package:absen/config/config.dart'; import 'package:absen/models/attendance_area.dart'; import 'package:absen/widgets/google_map_web_view.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:geolocator/geolocator.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'package:http/http.dart' as http; import 'checkin_photo_screen.dart'; class CheckinScreen extends StatefulWidget { final String token; const CheckinScreen({Key? key, required this.token}) : super(key: key); @override State createState() => _CheckinScreenState(); } class _CheckinScreenState extends State { Position? _position; bool _isLoading = false; GoogleMapController? _mapController; List _areas = []; LatLng? get _latLng => _position != null ? LatLng(_position!.latitude, _position!.longitude) : null; @override void initState() { super.initState(); _getLocation(); _fetchAttendanceAreas(); } Future _getLocation() async { try { bool serviceEnabled = await Geolocator.isLocationServiceEnabled(); if (!serviceEnabled) { await Geolocator.openLocationSettings(); ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Aktifkan layanan lokasi!')), ); return; } LocationPermission permission = await Geolocator.checkPermission(); if (permission == LocationPermission.denied) { permission = await Geolocator.requestPermission(); if (permission == LocationPermission.denied) { ScaffoldMessenger.of( context, ).showSnackBar(const SnackBar(content: Text('Izin lokasi ditolak!'))); return; } } if (permission == LocationPermission.deniedForever) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Izin lokasi permanen ditolak!')), ); return; } Position? pos; try { pos = await Geolocator.getCurrentPosition( desiredAccuracy: LocationAccuracy.best, timeLimit: const Duration(seconds: 10), ); } catch (_) { pos = await Geolocator.getLastKnownPosition(); } if (pos == null || (pos.latitude == 0.0 && pos.longitude == 0.0)) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Lokasi belum akurat, coba lagi.')), ); return; } setState(() => _position = pos); if (!kIsWeb && _mapController != null && _latLng != null && _areas.isNotEmpty) { _moveCameraToBounds(); } } catch (e) { ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text('Gagal mengambil lokasi: $e'))); } } Future _fetchAttendanceAreas() async { final uri = Uri.parse('${AppConfig.baseUrl}/api/employee/attendance/areas'); final token = widget.token; try { final resp = await http.get( uri, headers: {'Authorization': 'Bearer $token'}, ); if (resp.statusCode == 200) { final List data = jsonDecode(resp.body); final List fetched = data.map((e) { return AttendanceArea( lat: (e['center_lat'] as num).toDouble(), lng: (e['center_lng'] as num).toDouble(), radius: (e['radius'] as num).toDouble(), ); }).toList(); setState(() { _areas = fetched; }); if (_mapController != null && _position != null) { _moveCameraToBounds(); } } } catch (_) {} } void _moveCameraToBounds() { if (_mapController == null || _position == null) return; final List points = [ LatLng(_position!.latitude, _position!.longitude), ..._areas.map((a) => LatLng(a.lat, a.lng)), ]; double south = points.first.latitude, north = points.first.latitude; double west = points.first.longitude, east = points.first.longitude; for (var p in points) { south = min(south, p.latitude); north = max(north, p.latitude); west = min(west, p.longitude); east = max(east, p.longitude); } final bounds = LatLngBounds( southwest: LatLng(south, west), northeast: LatLng(north, east), ); Future.delayed(const Duration(milliseconds: 300), () { _mapController?.animateCamera(CameraUpdate.newLatLngBounds(bounds, 60)); }); } Future _validateLocation() async { if (_position == null) return; setState(() => _isLoading = true); final uri = Uri.parse( '${AppConfig.baseUrl}/api/employee/attendance/check-location', ); try { final response = await http.post( uri, headers: { 'Authorization': 'Bearer ${widget.token}', 'Content-Type': 'application/json', }, body: jsonEncode({ 'latitude': _position!.latitude, 'longitude': _position!.longitude, 'type': 'in', }), ); setState(() => _isLoading = false); final data = jsonDecode(response.body); if (response.statusCode == 200 && data['valid'] == true) { Navigator.push( context, MaterialPageRoute( builder: (_) => CheckinPhotoScreen( token: widget.token, position: _position!, ), ), ); } else { final msg = data['message'] ?? 'Lokasi tidak valid'; ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text(msg))); } } catch (e) { setState(() => _isLoading = false); ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text('Gagal validasi lokasi: $e'))); } } @override Widget build(BuildContext context) { final buttonStyle = ElevatedButton.styleFrom( minimumSize: const Size.fromHeight(48), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), ); return Scaffold( appBar: AppBar( title: const Text('Check-in: Validasi Lokasi'), centerTitle: true, ), body: SafeArea( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0), child: Column( children: [ const SizedBox(height: 16), const Text( 'Pastikan GPS aktif dan Anda berada dalam area yang diperbolehkan. ' 'Tekan tombol di bawah untuk cek lokasi.', textAlign: TextAlign.center, style: TextStyle(fontSize: 16), ), const SizedBox(height: 16), Expanded( child: Card( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), clipBehavior: Clip.hardEdge, elevation: 4, child: _buildMapWidget(), ), ), const SizedBox(height: 16), SizedBox( width: double.infinity, child: ElevatedButton( style: buttonStyle, onPressed: (_position != null && !_isLoading) ? _validateLocation : null, child: _isLoading ? const SizedBox( width: 24, height: 24, child: CircularProgressIndicator( color: Colors.white, strokeWidth: 2, ), ) : const Text('Cek Lokasi & Lanjut'), ), ), const SizedBox(height: 16), ], ), ), ), ); } Widget _buildMapWidget() { if (kIsWeb) { if (_position == null) { return const Center(child: Text('Lokasi belum diambil')); } return GoogleMapWebView( latitude: _position!.latitude, longitude: _position!.longitude, areas: _areas, ); } if (_latLng == null) { return const Center(child: Text('Lokasi belum diambil')); } return SizedBox( height: 300, child: GoogleMap( initialCameraPosition: CameraPosition( target: _areas.isNotEmpty ? LatLng(_areas[0].lat, _areas[0].lng) : _latLng!, zoom: 17, ), markers: { Marker( markerId: const MarkerId('me'), position: _latLng!, infoWindow: const InfoWindow(title: 'Lokasi Anda'), ), ..._areas.asMap().entries.map( (entry) => Marker( markerId: MarkerId('area_${entry.key}'), position: LatLng(entry.value.lat, entry.value.lng), infoWindow: InfoWindow(title: 'Area Absen ${entry.key + 1}'), icon: BitmapDescriptor.defaultMarkerWithHue( BitmapDescriptor.hueAzure, ), ), ), }, circles: _areas .asMap() .entries .map( (entry) => Circle( circleId: CircleId('absen_area_${entry.key}'), center: LatLng(entry.value.lat, entry.value.lng), radius: entry.value.radius, fillColor: Colors.blue.withOpacity(0.2), strokeColor: Colors.blue, strokeWidth: 2, ), ) .toSet(), onMapCreated: (controller) { _mapController = controller; if (_position != null && _areas.isNotEmpty) { _moveCameraToBounds(); } }, myLocationEnabled: true, ), ); } }