import 'dart:convert'; import 'dart:math'; 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 'package:absen/models/attendance_area.dart'; import 'checkout_photo_screen.dart'; class CheckoutScreen extends StatefulWidget { final String token; const CheckoutScreen({Key? key, required this.token}) : super(key: key); @override State createState() => _CheckoutScreenState(); } class _CheckoutScreenState extends State { Position? _position; GoogleMapController? _mapController; bool _isLoading = false; bool _canCheckout = false; bool _checkedStatus = false; List _areas = []; LatLng? get _latLng => _position != null ? LatLng(_position!.latitude, _position!.longitude) : null; @override void initState() { super.initState(); _checkTodayStatus(); _fetchAttendanceAreas(); _getLocation(); } Future _checkTodayStatus() async { setState(() => _checkedStatus = false); final url = Uri.parse( 'http://localhost:8000/api/employee/attendance/today-status', ); try { final resp = await http.get( url, headers: { 'Authorization': 'Bearer ${widget.token}', 'Accept': 'application/json', }, ); if (resp.statusCode == 200) { final data = jsonDecode(resp.body)['data']; setState(() { _canCheckout = data['clock_in'] != null && data['clock_out'] == null; }); } else { setState(() => _canCheckout = false); } } catch (_) { setState(() => _canCheckout = false); } finally { setState(() => _checkedStatus = true); } } Future _fetchAttendanceAreas() async { final uri = Uri.parse( 'http://localhost:8000/api/employee/attendance/areas', ); // Asumsikan endpoint mengembalikan JSON array: [ { center_lat, center_lng, radius }, ... ] try { final resp = await http.get( uri, headers: {'Authorization': 'Bearer ${widget.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 (_) { // Jika API lama hanya mengirim satu area, bisa fallback: // final single = jsonDecode(resp.body); // setState(() => _areas = [AttendanceArea(...)] ); } } 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; } final pos = await Geolocator.getCurrentPosition( desiredAccuracy: LocationAccuracy.high, ); setState(() => _position = pos); if (_mapController != null && _areas.isNotEmpty) { _moveCameraToBounds(); } } catch (e) { ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text('Gagal mengambil lokasi: $e'))); } } void _moveCameraToBounds() { if (_mapController == null || _position == null) return; // kumpulkan semua LatLng: posisi user + semua pusat area 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 _validateLocationAndNavigate() async { if (!_canCheckout) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Anda belum check-in hari ini!')), ); return; } if (_position == null) { ScaffoldMessenger.of( context, ).showSnackBar(const SnackBar(content: Text('Lokasi belum diambil!'))); return; } // Opsional: cek client-side apakah posisi di dalam salah satu area bool insideAny = false; for (var area in _areas) { final dist = Geolocator.distanceBetween( _position!.latitude, _position!.longitude, area.lat, area.lng, ); // meter if (dist <= area.radius) { insideAny = true; break; } } if (!_areas.isEmpty && !insideAny) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Anda berada di luar semua area yang diizinkan.'), ), ); return; } setState(() => _isLoading = true); final uri = Uri.parse( 'http://localhost:8000/api/employee/attendance/check-location', ); try { final resp = await http.post( uri, headers: { 'Authorization': 'Bearer ${widget.token}', 'Content-Type': 'application/json', }, body: jsonEncode({ 'latitude': _position!.latitude, 'longitude': _position!.longitude, 'type': 'out', }), ); setState(() => _isLoading = false); final data = jsonDecode(resp.body); if (resp.statusCode == 200 && data['valid'] == true) { Navigator.push( context, MaterialPageRoute( builder: (_) => CheckoutPhotoScreen( 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)), ); if (!_checkedStatus) { return const Scaffold(body: Center(child: CircularProgressIndicator())); } return Scaffold( appBar: AppBar(title: const Text('Check-out'), centerTitle: true), body: SafeArea( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0), child: Column( children: [ const SizedBox(height: 16), Text( _canCheckout ? 'Tekan tombol di bawah untuk cek lokasi dan lanjut ambil foto.' : 'Anda belum melakukan check-in hari ini atau sudah check-out.', textAlign: TextAlign.center, style: const TextStyle(fontSize: 16), ), const SizedBox(height: 16), Expanded( child: Card( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), elevation: 4, clipBehavior: Clip.hardEdge, child: _buildMapArea(), ), ), const SizedBox(height: 16), SizedBox( width: double.infinity, child: ElevatedButton( style: buttonStyle, onPressed: (_position != null && !_isLoading && _canCheckout) ? _validateLocationAndNavigate : 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 _buildMapArea() { 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')); } // Tampilkan semua area sebagai circle & marker final markers = { Marker( markerId: const MarkerId('me'), position: _latLng!, infoWindow: const InfoWindow(title: 'Lokasi Anda'), ), }; for (int i = 0; i < _areas.length; i++) { final a = _areas[i]; markers.add( Marker( markerId: MarkerId('area_$i'), position: LatLng(a.lat, a.lng), infoWindow: InfoWindow(title: 'Area Absen ${i + 1}'), icon: BitmapDescriptor.defaultMarkerWithHue( BitmapDescriptor.hueAzure, ), ), ); } final circles = {}; for (int i = 0; i < _areas.length; i++) { final a = _areas[i]; circles.add( Circle( circleId: CircleId('absen_area_$i'), center: LatLng(a.lat, a.lng), radius: a.radius, fillColor: Colors.blue.withOpacity(0.2), strokeColor: Colors.blue, strokeWidth: 2, ), ); } return GoogleMap( initialCameraPosition: CameraPosition( target: _areas.isNotEmpty ? LatLng(_areas[0].lat, _areas[0].lng) : _latLng!, zoom: 16, ), markers: markers, circles: circles, onMapCreated: (ctrl) { _mapController = ctrl; if (_position != null && _areas.isNotEmpty) { _moveCameraToBounds(); } }, myLocationEnabled: true, myLocationButtonEnabled: false, ); } }