import 'dart:convert'; import 'dart:io'; import 'dart:math' show asin, cos, pow, sin, sqrt; import 'package:camera/camera.dart'; import 'package:dotted_border/dotted_border.dart'; import 'package:flutter/material.dart'; import 'package:location/location.dart'; import 'package:qyuota/view/home/camera_pulang_page.dart'; import 'package:qyuota/view/home/home_view.dart'; import 'package:qyuota/models/helperurl.dart'; import 'package:qyuota/models/save_presensi_response.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:syncfusion_flutter_maps/maps.dart'; import 'package:http/http.dart' as http; import 'package:http_parser/http_parser.dart'; import 'package:qyuota/config/api_config.dart'; import 'package:qyuota/models/location_config.dart'; class ClockoutView extends StatefulWidget { final XFile? image; const ClockoutView({super.key, this.image}); @override State createState() => _ClockoutViewState(); } class _ClockoutViewState extends State with TickerProviderStateMixin { String url = MyUrl().getUrlDevice(); String? _selectedStatus; String? _selectedKeterangan; bool _isSaving = false; bool _isInRadius = false; double _distance = 0.0; // Lokasi kantor dan radius double _officeLatitude = ApiConfig.defaultOfficeLatitude; double _officeLongitude = ApiConfig.defaultOfficeLongitude; double _radiusInMeters = ApiConfig.defaultRadiusInMeters; // Lokasi pengguna saat ini LocationData? _currentLocationData; // Location config helper final LocationConfig _locationConfig = LocationConfig(); @override void initState() { super.initState(); _loadLocationConfig(); checkAbsensi(); } // Load konfigurasi lokasi dari SharedPreferences Future _loadLocationConfig() async { _officeLatitude = await _locationConfig.getOfficeLatitude(); _officeLongitude = await _locationConfig.getOfficeLongitude(); _radiusInMeters = await _locationConfig.getRadiusInMeters(); _getCurrentLocationAndCheckRadius(); } // Mendapatkan lokasi saat ini dan memeriksa radius Future _getCurrentLocationAndCheckRadius() async { _currentLocationData = await _currentLocation(); if (_currentLocationData != null) { double distance = _locationConfig.calculateDistance( _currentLocationData!.latitude!, _currentLocationData!.longitude!, _officeLatitude, _officeLongitude ); setState(() { _isInRadius = distance <= _radiusInMeters; _distance = distance; }); if (!_isInRadius) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Jarak dari kantor: ${distance.toStringAsFixed(0)} meter. Anda harus berada dalam radius ${_radiusInMeters.toStringAsFixed(0)} meter.'), backgroundColor: Colors.red, duration: const Duration(seconds: 5), ), ); } } } Future _currentLocation() async { bool serviceEnable; PermissionStatus permissionGranted; Location location = Location(); serviceEnable = await location.serviceEnabled(); if (!serviceEnable) { serviceEnable = await location.requestService(); if (!serviceEnable) { return null; } } permissionGranted = await location.hasPermission(); if (permissionGranted == PermissionStatus.denied) { permissionGranted = await location.requestPermission(); if (permissionGranted != PermissionStatus.granted) { return null; } } return await location.getLocation(); } Future savePresensi(double latitude, double longitude) async { if (_selectedStatus == null || _selectedKeterangan == null) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Harap pilih status dan keterangan'), backgroundColor: Colors.red, ), ); return; } if (widget.image == null) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Foto wajib diisi'), backgroundColor: Colors.red, ), ); return; } // Periksa apakah pengguna berada dalam radius yang ditentukan if (!_isInRadius) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Anda berada di luar radius 200 meter dari kantor. Presensi tidak dapat dilakukan.'), backgroundColor: Colors.red, ), ); return; } setState(() { _isSaving = true; }); try { SharedPreferences prefs = await SharedPreferences.getInstance(); String? token = prefs.getString('auth_token'); String? userId = prefs.getString('user_id'); if (token == null || userId == null) { throw Exception('Token atau User ID tidak ditemukan'); } var uri = Uri.parse(ApiConfig.savePresensi); var request = http.MultipartRequest('POST', uri); request.headers['Authorization'] = 'Bearer $token'; request.headers['Accept'] = 'application/json'; // Pastikan latitude dan longitude tidak null if (latitude == null || longitude == null) { throw Exception('Data lokasi tidak valid'); } // Format data dengan benar final Map fields = { 'user_id': userId, 'status': 'Hadir', // Ubah status menjadi Hadir untuk validasi 'keterangan': _selectedKeterangan!, 'clock_type': 'out', 'latitude': latitude.toString(), 'longitude': longitude.toString(), }; print('Request fields before sending: $fields'); print('Status: ${_selectedStatus}'); print('Latitude: $latitude, Longitude: $longitude'); // Tambahkan fields ke request request.fields.addAll(fields); // Handle foto try { var imageFile = File(widget.image!.path); var stream = imageFile.openRead(); var length = await imageFile.length(); var multipartFile = http.MultipartFile( 'photo', stream, length, filename: 'photo_${DateTime.now().millisecondsSinceEpoch}.jpg', contentType: MediaType('image', 'jpeg'), ); request.files.add(multipartFile); print('Added photo to request'); } catch (e) { print('Error processing photo: $e'); throw Exception('Gagal memproses foto: $e'); } print('Sending request to: ${uri.toString()}'); print('Headers: ${request.headers}'); print('Fields: ${request.fields}'); var streamedResponse = await request.send().timeout( const Duration(seconds: 30), onTimeout: () { throw Exception('Timeout: Koneksi terlalu lama'); }, ); var response = await http.Response.fromStream(streamedResponse); print('Response status code: ${response.statusCode}'); print('Response body: ${response.body}'); if (response.statusCode == 201 || response.statusCode == 200) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Presensi berhasil disimpan'), backgroundColor: Colors.green, ), ); Navigator.pushReplacement( context, MaterialPageRoute(builder: (context) => const HomeView()), ); } else { String errorMessage = 'Gagal menyimpan presensi'; try { var jsonResponse = json.decode(response.body); if (jsonResponse['message'] != null) { errorMessage = jsonResponse['message']; } else if (jsonResponse['errors'] != null && jsonResponse['errors'] is Map && jsonResponse['errors']['photo'] != null) { errorMessage = jsonResponse['errors']['photo'][0]; } } catch (e) { print('Error parsing response: $e'); } print('Error message: $errorMessage'); if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(errorMessage), backgroundColor: Colors.red, ), ); } } catch (e) { print('Exception in savePresensi: $e'); if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Error: ${e.toString()}'), backgroundColor: Colors.red, ), ); } finally { if (mounted) { setState(() { _isSaving = false; }); } } } Future checkAbsensi() async { SharedPreferences prefs = await SharedPreferences.getInstance(); String userId = prefs.getString('user_id') ?? ''; final response = await http.post( Uri.parse(ApiConfig.checkAbsensi), body: { 'user_id': userId, 'clock_type': 'out' }, ); if (response.statusCode == 200) { final responseData = json.decode(response.body); if (responseData['success'] == true) { Navigator.pushReplacement( context, MaterialPageRoute(builder: (context) => const HomeView()), ); ScaffoldMessenger.of(context).showSnackBar(const SnackBar( content: Text("Anda sudah melakukan presensi pulang"))); } } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text( "Presensi Pulang", style: TextStyle(color: Colors.white), ), backgroundColor: const Color(0xFF0077B6), leading: IconButton( icon: const Icon( Icons.arrow_back_ios, color: Colors.white, ), onPressed: () => Navigator.pop(context), ), ), body: Container( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ const Color(0xFF0077B6), const Color(0xFF0077B6).withOpacity(0.7), ], ), ), child: FutureBuilder( future: _currentLocation(), builder: (BuildContext context, AsyncSnapshot snapshot) { if (snapshot.hasData) { final LocationData currentLocation = snapshot.data!; return SafeArea( child: SingleChildScrollView( padding: const EdgeInsets.all(10.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( height: MediaQuery.of(context).size.height * 0.4, child: Stack( children: [ SfMaps( layers: [ MapTileLayer( initialFocalLatLng: MapLatLng( currentLocation.latitude!, currentLocation.longitude!), initialZoomLevel: 15, initialMarkersCount: 2, urlTemplate: "https://tile.openstreetmap.org/{z}/{x}/{y}.png", markerBuilder: (BuildContext context, int index) { if (index == 0) { // Marker untuk lokasi pengguna return MapMarker( latitude: currentLocation.latitude!, longitude: currentLocation.longitude!, child: const Icon( Icons.location_on, color: Colors.red, size: 30, ), ); } else { // Marker untuk lokasi kantor return MapMarker( latitude: _officeLatitude, longitude: _officeLongitude, child: const Icon( Icons.business, color: Colors.blue, size: 30, ), ); } }, sublayers: [ MapCircleLayer( circles: { MapCircle( center: MapLatLng(_officeLatitude, _officeLongitude), radius: _radiusInMeters, color: Colors.blue.withOpacity(0.2), strokeWidth: 2, strokeColor: Colors.blue, ), }, ), ], ) ], ), Positioned( top: 10, left: 10, right: 10, child: Column( children: [ Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: _isInRadius ? Colors.green : Colors.red, borderRadius: BorderRadius.circular(8), ), child: Text( _isInRadius ? 'Dalam Radius (${_distance.toStringAsFixed(0)}m)' : 'Diluar Radius (${_distance.toStringAsFixed(0)}m)', style: const TextStyle( color: Colors.white, fontWeight: FontWeight.bold, ), textAlign: TextAlign.center, ), ), // const SizedBox(height: 5), // ElevatedButton( // onPressed: () => _setCurrentLocationAsOffice(currentLocation), // style: ElevatedButton.styleFrom( // backgroundColor: Colors.blue, // padding: const EdgeInsets.symmetric(horizontal: 10), // minimumSize: const Size(double.infinity, 35), // ), // child: const Text('Gunakan Lokasi Saat Ini', // style: TextStyle(fontSize: 12, color: Colors.white), // ), // ), // const SizedBox(height: 5), // Container( // padding: const EdgeInsets.all(5), // decoration: BoxDecoration( // color: Colors.white.withOpacity(0.8), // borderRadius: BorderRadius.circular(8), // ), // child: Text( // 'Kantor: ${_officeLatitude.toStringAsFixed(6)}, ${_officeLongitude.toStringAsFixed(6)}', // style: const TextStyle(fontSize: 11, color: Colors.black), // textAlign: TextAlign.center, // ), // ), ], ), ), ], ), ), const SizedBox(height: 20), Card( color: Colors.white, elevation: 5, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10), ), child: Column( children: [ Container( height: 50, decoration: const BoxDecoration( borderRadius: BorderRadius.only( topLeft: Radius.circular(10), topRight: Radius.circular(10)), color: Color(0xFF0077B6), ), child: const Row( children: [ SizedBox(width: 12), Icon(Icons.face_retouching_natural_outlined, color: Colors.white), SizedBox(width: 12), Text( "Absen Foto Selfie ya!", style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: Colors.white), ), ], ), ), const Padding( padding: EdgeInsets.fromLTRB(10, 20, 0, 20), child: Text( "Ambil Foto", style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: Colors.black), ), ), GestureDetector( onTap: () { Navigator.push( context, MaterialPageRoute( builder: (context) => const CameraPulangPage())); }, child: Container( margin: const EdgeInsets.fromLTRB(10, 0, 10, 20), width: MediaQuery.of(context).size.width, height: 150, child: DottedBorder( radius: const Radius.circular(10), borderType: BorderType.RRect, color: Color(0xFF023E8A), strokeWidth: 1, dashPattern: const [5, 5], child: SizedBox.expand( child: FittedBox( child: widget.image != null ? Image.file( File(widget.image!.path), fit: BoxFit.cover) : const Icon( Icons.camera_enhance_outlined, color: Color(0xFF0077B6), ), ), ), ), ), ), SizedBox( width: MediaQuery.of(context).size.width * 0.9, child: DropdownButtonFormField( value: _selectedStatus, onChanged: (String? value) { if (value != null) { setState(() { _selectedStatus = value; }); } }, items: ['Pulang'] .map((String status) => DropdownMenuItem( value: status, child: Text(status), )) .toList(), decoration: const InputDecoration( labelText: 'Status', border: OutlineInputBorder(), contentPadding: EdgeInsets.fromLTRB( 20, 8, 20, 8), // Padding kanan dan kiri ), ), ), const SizedBox(height: 20), SizedBox( width: MediaQuery.of(context).size.width * 0.9, child: DropdownButtonFormField( value: _selectedKeterangan, onChanged: (String? value) { if (value != null) { setState(() { _selectedKeterangan = value; }); } }, items: [ 'Bekerja', ] .map((String keterangan) => DropdownMenuItem( value: keterangan, child: Text(keterangan), )) .toList(), decoration: const InputDecoration( labelText: 'Keterangan', border: OutlineInputBorder(), contentPadding: EdgeInsets.symmetric( horizontal: 16, vertical: 8), ), ), ), const SizedBox(height: 20), Padding( padding: const EdgeInsets.only(bottom: 30), child: Center( child: ElevatedButton( onPressed: (_isSaving || !_isInRadius) ? null : () { savePresensi( currentLocation.latitude!, currentLocation.longitude!); }, style: ElevatedButton.styleFrom( backgroundColor: Color(0xFF0077B6), padding: const EdgeInsets.symmetric( horizontal: 30, vertical: 15), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), disabledBackgroundColor: Colors.grey, ), child: _isSaving ? const CircularProgressIndicator( color: Colors.white) : Text( _isInRadius ? "Simpan" : "Diluar Radius", style: const TextStyle( color: Colors.white, fontSize: 16), ), ), ), ), ], ), ), ], ), ), ); } else { return const Center(child: CircularProgressIndicator()); } }, ), ), ); } }