import 'dart:io'; import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter/cupertino.dart'; import 'package:dropdown_button2/dropdown_button2.dart'; import 'package:flutter_svg/svg.dart'; import 'package:image_picker/image_picker.dart'; import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; import 'package:geolocator/geolocator.dart'; import 'package:geocoding/geocoding.dart'; import '../../Models/ordersModels/commonResponse.dart'; import '../../Notifiers/hrmProvider/attendanceListProvider.dart'; import '../../Utils/app_colors.dart'; import '../../Utils/commonServices.dart'; import '../../Utils/commonWidgets.dart'; import '../../Utils/dropdownTheme.dart'; class AddManualAttendanceScreen extends StatefulWidget { const AddManualAttendanceScreen({super.key}); @override State createState() => _AddManualAttendanceScreenState(); } class _AddManualAttendanceScreenState extends State { Dropdowntheme ddtheme = Dropdowntheme(); final ImagePicker picker = ImagePicker(); // Connectivity Map _source = {ConnectivityResult.mobile: true}; final MyConnectivity _connectivity = MyConnectivity.instance; String connection = "Online"; // Controllers final checkInTime = TextEditingController(); final checkInLocation = TextEditingController(); final checkInDescription = TextEditingController(); XFile? checkInProof; final checkOutTime = TextEditingController(); final checkOutLocation = TextEditingController(); final checkOutDescription = TextEditingController(); XFile? checkOutProof; String? selectedType; final List types = ["Check In", "Check Out", "Check In/Out"]; // Errors String? dateError, typeError; String? checkInTimeError, checkInLocError, checkInDescError, checkInProofError; String? checkOutTimeError, checkOutLocError, checkOutDescError, checkOutProofError; // In your Attendancelistprovider class CommonResponse? get addResponse => addResponse; String? get errorMessage => errorMessage; @override void initState() { super.initState(); _connectivity.initialise(); _connectivity.myStream.listen((src) { setState(() => _source = src); }); WidgetsBinding.instance.addPostFrameCallback((_) { _fetchInitialLocation(); }); } @override void dispose() { _connectivity.disposeStream(); super.dispose(); } Future _fetchInitialLocation() async { String loc = await getCurrentLocation(); setState(() { checkInLocation.text = loc; checkOutLocation.text = loc; }); } Future getCurrentLocation() async { try { LocationPermission permission = await Geolocator.checkPermission(); if (permission == LocationPermission.denied) { permission = await Geolocator.requestPermission(); if (permission == LocationPermission.denied) return "Permission denied"; } if (permission == LocationPermission.deniedForever) { return "Permission permanently denied"; } Position pos = await Geolocator.getCurrentPosition( desiredAccuracy: LocationAccuracy.high); List placemarks = await placemarkFromCoordinates(pos.latitude, pos.longitude); if (placemarks.isNotEmpty) { Placemark p = placemarks.first; return "${p.locality}, ${p.administrativeArea}, ${p.country}"; } return "${pos.latitude}, ${pos.longitude}"; } catch (e) { return "Error: $e"; } } Future _pickTime(TextEditingController controller) async { final TimeOfDay? picked = await showTimePicker(context: context, initialTime: TimeOfDay.now()); if (picked != null) controller.text = picked.format(context); } Future _pickFile(bool isCheckIn) async { showModalBottomSheet( useSafeArea: true, isDismissible: true, showDragHandle: true, backgroundColor: Colors.white, context: context, builder: (_) { return SafeArea( child: Column( mainAxisSize: MainAxisSize.min, children: [ ListTile( leading: const Icon(Icons.camera_alt), title: const Text("Capture photo from camera"), onTap: () async { final XFile? file = await picker.pickImage(source: ImageSource.camera); if (file != null) { setState(() { if (isCheckIn) checkInProof = file; else checkOutProof = file; }); } Navigator.pop(context); }, ), ListTile( leading: const Icon(Icons.photo_library), title: const Text("Select photo from gallery"), onTap: () async { final XFile? file = await picker.pickImage(source: ImageSource.gallery); if (file != null) { setState(() { if (isCheckIn) checkInProof = file; else checkOutProof = file; }); } Navigator.pop(context); }, ), ], ), ); }, ); } void _submitForm(BuildContext context) async { // Reset errors first dateError = null; typeError = null; checkInTimeError = checkInLocError = checkInDescError = checkInProofError = null; checkOutTimeError = checkOutLocError = checkOutDescError = checkOutProofError = null; final provider = Provider.of(context, listen: false); // --- Date Validation (allow today, yesterday, day before yesterday) --- if (provider.dateController.text.isEmpty) { dateError = "Please select a date"; } else { try { final enteredDate = DateFormat("dd MMM yyyy").parse(provider.dateController.text); provider.setSelectedDate(enteredDate); final today = DateTime.now(); final yesterday = today.subtract(const Duration(days: 1)); final dayBeforeYesterday = today.subtract(const Duration(days: 2)); // Normalize dates (ignore time part) bool isValid = enteredDate.year == today.year && enteredDate.month == today.month && (enteredDate.day == today.day || enteredDate.day == yesterday.day || enteredDate.day == dayBeforeYesterday.day); if (!isValid) { dateError = "Date must be today, yesterday, or the day before yesterday"; } } catch (e) { dateError = "Invalid date format (use dd MMM yyyy)"; } } // --- Type Validation --- if (selectedType == null) { typeError = "Please select type"; } // --- Check In Validations --- if (selectedType == "Check In" || selectedType == "Check In/Out") { if (checkInTime.text.isEmpty) checkInTimeError = "Please select check-in time"; if (checkInLocation.text.isEmpty) checkInLocError = "Please enter check-in location"; if (checkInDescription.text.isEmpty) checkInDescError = "Please enter description"; if (checkInProof == null) checkInProofError = "Please attach check-in proof"; } // --- Check Out Validations --- if (selectedType == "Check Out" || selectedType == "Check In/Out") { if (checkOutTime.text.isEmpty) checkOutTimeError = "Please select check-out time"; if (checkOutLocation.text.isEmpty) checkOutLocError = "Please enter check-out location"; if (checkOutDescription.text.isEmpty) checkOutDescError = "Please enter description"; if (checkOutProof == null) checkOutProofError = "Please attach check-out proof"; } // --- Stop if any error --- if ([ dateError, typeError, checkInTimeError, checkInLocError, checkInDescError, checkInProofError, checkOutTimeError, checkOutLocError, checkOutDescError, checkOutProofError ].any((e) => e != null)) { setState(() {}); // refresh UI to show error messages return; } // --- Format date for server --- String formattedDate = ""; try { final parsedDate = DateFormat("dd MMM yyyy").parse(provider.dateController.text); formattedDate = DateFormat("yyyy-MM-dd").format(parsedDate); } catch (e) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text("Error formatting date: $e")), ); return; } // --- Build data according to type --- String? finalCheckInTime; String? finalCheckInLoc; File? finalCheckInProof; String? finalCheckOutTime; String? finalCheckOutLoc; File? finalCheckOutProof; String? finalNote; if (selectedType == "Check In") { finalCheckInTime = checkInTime.text; finalCheckInLoc = checkInLocation.text; finalCheckInProof = File(checkInProof!.path); finalNote = checkInDescription.text; } else if (selectedType == "Check Out") { finalCheckOutTime = checkOutTime.text; finalCheckOutLoc = checkOutLocation.text; finalCheckOutProof = File(checkOutProof!.path); finalNote = checkOutDescription.text; } else if (selectedType == "Check In/Out") { finalCheckInTime = checkInTime.text; finalCheckInLoc = checkInLocation.text; finalCheckInProof = File(checkInProof!.path); finalCheckOutTime = checkOutTime.text; finalCheckOutLoc = checkOutLocation.text; finalCheckOutProof = File(checkOutProof!.path); finalNote = "${checkInDescription.text} / ${checkOutDescription.text}"; } // --- Submit to provider --- await provider.addAttendanceRequest( context, process: "Manual", type: selectedType!, loc: selectedType == "Check In" ? checkInLocation.text : selectedType == "Check Out" ? checkOutLocation.text : "${checkInLocation.text}, ${checkOutLocation.text}", checkDate: formattedDate, checkInTime: finalCheckInTime, checkInLoc: finalCheckInLoc, checkInProof: finalCheckInProof, checkOutTime: finalCheckOutTime, checkOutLoc: finalCheckOutLoc, checkOutProof: finalCheckOutProof, note: finalNote, ); // --- Response handling --- if (provider.addResponse != null && provider.addResponse!.error == "0") { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(provider.addResponse!.message ?? "Attendance Submitted Successfully")), ); // Reset fields setState(() { selectedType = null; provider.dateController.clear(); checkInTime.clear(); checkInLocation.clear(); checkInDescription.clear(); checkInProof = null; checkOutTime.clear(); checkOutLocation.clear(); checkOutDescription.clear(); checkOutProof = null; }); _fetchInitialLocation(); } else { String errorMessage = provider.errorMessage ?? "Failed to submit attendance"; if (errorMessage.contains("Check In is not Available")) { errorMessage = "Cannot submit Check Out without a Check In record for this date"; } if (errorMessage.contains("2")){ errorMessage = "Only One manual Request can be added in a month !"; } ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(errorMessage), backgroundColor: Colors.red), ); } } // it's date picker need to take day before yesterday, yesterday and today Future _onBackPressed(BuildContext context) async { Navigator.pop(context, true); return true; } @override Widget build(BuildContext context) { switch (_source.keys.toList()[0]) { case ConnectivityResult.mobile: case ConnectivityResult.wifi: connection = 'Online'; break; default: connection = 'Offline'; } return (connection == "Online") ? Platform.isAndroid ? WillPopScope( onWillPop: () => _onBackPressed(context), child: SafeArea( top: false, bottom: true, child: _scaffold(context), ), ) : _scaffold(context) : NoNetwork(context); } Widget _scaffold(BuildContext context) { return Consumer( builder: (context, provider, child) { return Scaffold( backgroundColor: Colors.white, appBar: appbar2New( context, "Add Manual Attendance", () {}, SizedBox.shrink(), 0xFFFFFFFF, ), body: Scrollbar( child: SingleChildScrollView( padding: const EdgeInsets.all(12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // TextWidget(context, "Date"), GestureDetector( onTap: () => provider.showDatePickerDialog(context), child: AbsorbPointer( child: textControllerWidget( context, provider.dateController, "Date", "Select Date", (v) {}, TextInputType.text, false, null, null, null, TextInputAction.next, ), ), ), errorWidget(context, dateError), TextWidget(context, "Type"), DropdownButtonHideUnderline( child: Row( children: [ Expanded( child: DropdownButton2( isExpanded: true, hint: const Text("Select Type"), items: types .map((e) => DropdownMenuItem( value: e, child: Text(e), )) .toList(), value: selectedType, onChanged: (val) { setState(() => selectedType = val); }, buttonStyleData: ddtheme.buttonStyleData, iconStyleData: ddtheme.iconStyleData, menuItemStyleData: ddtheme.menuItemStyleData, dropdownStyleData: ddtheme.dropdownStyleData, ), ), ], ), ), errorWidget(context, typeError), if (selectedType == "Check In" || selectedType == "Check In/Out") _buildSection("Check In"), if (selectedType == "Check Out" || selectedType == "Check In/Out") _buildSection("Check Out"), SizedBox(height: 80), ], ), ), ), bottomNavigationBar: InkResponse( onTap: provider.isSubmitting ? null : () => _submitForm(context), child: Container( height: 45, alignment: Alignment.center, margin: const EdgeInsets.all(12), decoration: BoxDecoration( color: AppColors.app_blue, borderRadius: BorderRadius.circular(15), ), child: provider.isSubmitting ? CircularProgressIndicator.adaptive( valueColor: AlwaysStoppedAnimation(AppColors.white), ) : const Text( "Submit", style: TextStyle( fontSize: 15, fontFamily: "JakartaMedium", color: Colors.white, ), ), ), ), ); }, ); } Widget _buildSection(String title) { final isCheckIn = title == "Check In"; final timeCtrl = isCheckIn ? checkInTime : checkOutTime; final locCtrl = isCheckIn ? checkInLocation : checkOutLocation; final descCtrl = isCheckIn ? checkInDescription : checkOutDescription; final proofFile = isCheckIn ? checkInProof : checkOutProof; final proofError = isCheckIn ? checkInProofError : checkOutProofError; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // TextWidget(context, "$title Time"), GestureDetector( onTap: () => _pickTime(timeCtrl), // ⏰ open time picker child: AbsorbPointer( child: textControllerWidget( context, timeCtrl, "$title Time", "Select Time", (v) {}, TextInputType.text, false, null, null, null, TextInputAction.next, ), ), ), errorWidget(context, isCheckIn ? checkInTimeError : checkOutTimeError), textControllerWidget( context, locCtrl, "$title Location", "Enter Location", (v) {}, TextInputType.text, false, null, null, null, TextInputAction.next, ), errorWidget(context, isCheckIn ? checkInLocError : checkOutLocError), textControllerWidget( context, descCtrl, "$title Description", "Enter Description", (v) {}, TextInputType.text, false, null, null, null, TextInputAction.done, ), errorWidget(context, isCheckIn ? checkInDescError : checkOutDescError), InkResponse( onTap: () => _pickFile(isCheckIn), child: Container( margin: const EdgeInsets.symmetric(vertical: 10), height: 45, width: double.infinity, decoration: BoxDecoration( color: const Color(0xFFE6F6FF), borderRadius: BorderRadius.circular(12), border: Border.all(color: AppColors.app_blue, width: 0.5), ), child: Center( child: Text( "Attach $title Proof", style: TextStyle( fontFamily: "JakartaMedium", color: AppColors.app_blue, ), ), ), ), ), if (proofFile != null) Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: Text( proofFile.name, maxLines: 2, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 12), ), ), IconButton( icon: const Icon(Icons.close, color: Colors.red, size: 18), onPressed: () { setState(() { if (isCheckIn) checkInProof = null; else checkOutProof = null; }); }, ) ], ), errorWidget(context, proofError), ], ); } }