Files
2026-05-26 18:01:57 +05:30

874 lines
26 KiB
Dart
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:google_places_flutter/google_places_flutter.dart';
import 'package:google_places_flutter/model/prediction.dart';
import 'package:nearledaily/constants/color_constants.dart';
import 'package:nearledaily/view/cart/cart_view.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:geolocator/geolocator.dart';
import 'package:geocoding/geocoding.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../../modules/authentication/auth.dart';
import '../../constants/font_constants.dart';
import '../../domain/provider/authentication/location.dart';
import '../../main.dart';
import '../../widgets/text_widget.dart';
class LocationPage extends StatefulWidget {
const LocationPage({super.key});
@override
State<LocationPage> createState() => _LocationPageState();
}
class _LocationPageState extends State<LocationPage> with RouteAware {
final CustomerLocationProvider locationProvider = CustomerLocationProvider();
List<Authentication> fetchedLocations = [];
bool isLoading = true;
String? newAddress;
String? newLat;
String? newLong;
int? selectedLocationId;
Authentication? selectedLocation;
String searchQuery = "";
@override
void initState() {
super.initState();
_fetchLocations();
}
@override
void didPopNext() {
_fetchLocations();
super.didPopNext();
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
routeObserver.subscribe(this, ModalRoute.of(context)!);
}
@override
void dispose() {
routeObserver.unsubscribe(this);
super.dispose();
}
Future<void> _fetchLocations() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
final id = prefs.getInt('customerId');
setState(() => isLoading = true);
try {
final locations = await locationProvider.fetchCustomerLocations(id!);
setState(() {
fetchedLocations = locations;
});
} catch (e) {
print('Error fetching locations: $e');
} finally {
setState(() => isLoading = false);
}
}
Future<void> _addNewAddress() async {
await Get.to(() => const MapPickerPage())?.then((result) async {
if (result == true) {
print("Refreshing locations now ✅");
await _fetchLocations();
}
});
}
Widget _badge({
required IconData icon,
required String label,
required bool isSelected,
}) {
const primaryColor = Color(0xFF662582);
return ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 220), // ✅ prevents overflow
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration(
color: isSelected ? const Color(0xFFF3E8FA) : Colors.grey.shade100,
borderRadius: BorderRadius.circular(20),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
icon,
size: 10,
color: isSelected ? primaryColor : Colors.grey.shade500,
),
const SizedBox(width: 4),
Flexible( // ✅ allows text to shrink and ellipsis
child: Text(
label,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 11,
fontFamily: FontConstants.fontFamily,
color: isSelected ? primaryColor : Colors.grey.shade500,
),
),
),
],
),
),
);
}
Widget _addressCard({
required String address,
required String doorNo,
required String landmark,
required VoidCallback onTap,
required bool isSelected,
bool isAddNew = false,
}) {
const primaryColor = Color(0xFF662582);
if (isAddNew) {
return GestureDetector(
onTap: onTap,
behavior: HitTestBehavior.opaque,
child: Container(
margin: const EdgeInsets.symmetric(vertical: 5),
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 14),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(14),
border: Border.all(
color: primaryColor.withOpacity(0.35),
width: 1,
),
),
child: Row(
children: [
Container(
width: 34,
height: 34,
decoration: const BoxDecoration(
color: Color(0xFFF3E8FA),
shape: BoxShape.circle,
),
child: const Icon(
Icons.add_location_alt_rounded,
size: 17,
color: primaryColor,
),
),
const SizedBox(width: 12),
ReusableTextWidget(
text: "Add new address",
fontSize: 13,
fontWeight: FontWeight.w500,
fontFamily: FontConstants.fontFamily,
color: primaryColor,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
),
);
}
return GestureDetector(
onTap: onTap,
behavior: HitTestBehavior.opaque,
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
curve: Curves.easeInOut,
margin: const EdgeInsets.symmetric(vertical: 5),
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 14),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(14),
border: Border.all(
color: isSelected ? primaryColor : Colors.grey.withOpacity(0.25),
width: isSelected ? 1.5 : 0.5,
),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Icon circle
AnimatedContainer(
duration: const Duration(milliseconds: 200),
width: 34,
height: 34,
decoration: BoxDecoration(
color: isSelected
? const Color(0xFFF3E8FA)
: Colors.grey.shade100,
shape: BoxShape.circle,
),
child: Icon(
Icons.location_on_rounded,
size: 17,
color: isSelected ? primaryColor : Colors.grey.shade500,
),
),
const SizedBox(width: 12),
// Address + badges — Expanded so it never overflows
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Main address bold (first 2 parts)
ReusableTextWidget(
text: address.split(',').take(2).join(',').trim(),
fontSize: 13,
fontWeight: FontWeight.w500,
fontFamily: FontConstants.fontFamily,
color: Colors.black.withOpacity(0.87),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 2),
// Rest of address muted
ReusableTextWidget(
text: address.split(',').skip(2).join(',').trim(),
fontSize: 12,
fontWeight: FontWeight.w400,
fontFamily: FontConstants.fontFamily,
color: Colors.grey.shade500,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 8),
// Badges — each individually constrained
if (doorNo.isNotEmpty || landmark.isNotEmpty)
Wrap(
spacing: 6,
runSpacing: 4,
children: [
if (doorNo.isNotEmpty)
_badge(
icon: Icons.door_front_door_outlined,
label: "Door: $doorNo",
isSelected: isSelected,
),
if (landmark.isNotEmpty)
_badge(
icon: Icons.near_me_outlined,
label: "Near: $landmark",
isSelected: false,
),
],
),
],
),
),
const SizedBox(width: 10),
// Radio indicator
AnimatedContainer(
duration: const Duration(milliseconds: 200),
width: 18,
height: 18,
margin: const EdgeInsets.only(top: 2),
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: isSelected
? primaryColor
: Colors.grey.withOpacity(0.4),
width: 1.5,
),
),
child: AnimatedScale(
duration: const Duration(milliseconds: 200),
scale: isSelected ? 1 : 0,
child: Center(
child: Container(
width: 8,
height: 8,
decoration: const BoxDecoration(
color: primaryColor,
shape: BoxShape.circle,
),
),
),
),
),
],
),
),
);
}
List<Widget> _buildAddressList() {
List<Widget> list = [];
// 1⃣ Add API fetched addresses
for (var loc in fetchedLocations) {
final addressText = loc.address ?? '';
if (addressText.toLowerCase().contains(searchQuery.toLowerCase())) {
list.add(_addressCard(
address: addressText,
doorNo: loc.doorno ?? '',
landmark: loc.landmark ?? '',
isSelected: selectedLocationId == loc.locationid,
onTap: () {
setState(() {
selectedLocationId = loc.locationid;
selectedLocation = loc;
});
},
));
}
}
// 2⃣ Add new address (default, unchanged)
if (newAddress != null &&
newAddress!.toLowerCase().contains(searchQuery.toLowerCase())) {
list.add(_addressCard(
address: newAddress!,
doorNo: '',
landmark: '',
isSelected: selectedLocationId == -1,
onTap: () {
setState(() {
selectedLocationId = -1;
selectedLocation = Authentication(
locationid: 0,
customerid: "0",
address: newAddress ?? "",
suburb: "",
city: "",
state: "",
landmark: "",
doorno: "",
postcode: "",
latitude: newLat ?? "",
longitude: newLong ?? "",
);
});
},
));
}
// 3⃣ Always show "Add New Address" option
list.add(_addressCard(
address: "Add new address",
doorNo: '',
landmark: '',
isSelected: false,
isAddNew: true,
onTap: _addNewAddress,
));
return list;
}
void _showPaymentBottomSheet() {
if (selectedLocation != null) {
print("Selected Location Details:");
print("locationid: ${selectedLocation!.locationid}");
print("customerid: ${selectedLocation!.customerid}");
print("address: ${selectedLocation!.address}");
print("suburb: ${selectedLocation!.suburb}");
print("city: ${selectedLocation!.city}");
print("state: ${selectedLocation!.state}");
print("landmark: ${selectedLocation!.landmark}");
print("doorno: ${selectedLocation!.doorno}");
print("postcode: ${selectedLocation!.postcode}");
print("latitude: ${selectedLocation!.latitude}");
print("longitude: ${selectedLocation!.longitude}");
Navigator.pop(context, selectedLocation);
}
}
@override
Widget build(BuildContext context) {
return SafeArea(
top: false,
bottom: true,
child: Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
backgroundColor: Colors.white,
elevation: 1,
leadingWidth: double.infinity,
centerTitle: false,
leading: Row(
children: [
IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.black),
onPressed: () => Navigator.of(context).pop(),
),
ReusableTextWidget(
text: "Select Location",
color: Colors.black,
fontFamily: FontConstants.fontFamily,
fontSize: 20,
fontWeight: FontWeight.bold,
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
],
),
actions: [
IconButton(
icon: const Icon(Icons.add_location_alt, color: Color(0xFF662582)),
tooltip: "Add New Location",
onPressed: _addNewAddress,
),
],
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
TextField(
onChanged: (val) {
setState(() => searchQuery = val);
},
decoration: InputDecoration(
hintText: "Search Address",
prefixIcon: const Icon(Icons.search),
filled: true,
fillColor: Colors.grey.shade100,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
),
),
const SizedBox(height: 16),
Expanded(
child: isLoading
? const Center(child: CircularProgressIndicator())
: ListView(
children: _buildAddressList(),
),
),
],
),
),
bottomNavigationBar: Padding(
padding: const EdgeInsets.all(16.0),
child: ElevatedButton(
onPressed: selectedLocationId == null ? null : _showPaymentBottomSheet,
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF662582),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
padding: const EdgeInsets.symmetric(vertical: 16),
),
child: ReusableTextWidget(
text: "Confirm Address",
color: Colors.white,
fontFamily: FontConstants.fontFamily,
fontSize: 16,
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
),
),
),
);
}
}
class MapPickerPage extends StatefulWidget {
const MapPickerPage({super.key});
@override
State<MapPickerPage> createState() => _MapPickerPageState();
}
class _MapPickerPageState extends State<MapPickerPage> {
LatLng? selectedLatLng;
String? selectedAddress;
GoogleMapController? mapController;
LatLng currentLatLng = const LatLng(11.0168, 76.9558); // default Coimbatore
static const String googleApiKey = "AIzaSyBhkGfnq27sN0wV5y_S-M2KojpFTk_by-Q";
@override
void initState() {
super.initState();
_checkPermissionAndGetLocation();
}
// Search function
Future<void> _checkPermissionAndGetLocation() async {
bool serviceEnabled = await Geolocator.isLocationServiceEnabled();
if (!serviceEnabled) {
Get.snackbar("Location Disabled", "Please enable location services");
return;
}
LocationPermission permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied) {
permission = await Geolocator.requestPermission();
}
if (permission == LocationPermission.deniedForever) {
Get.snackbar("Permission Denied",
"Location permission is permanently denied, please enable it in settings");
return;
}
if (permission == LocationPermission.whileInUse ||
permission == LocationPermission.always) {
await _goToCurrentLocation();
}
}
Future<void> _getAddressFromLatLng(LatLng latLng) async {
setState(() {
selectedAddress = "Loading address...";
});
try {
List<Placemark> placemarks =
await placemarkFromCoordinates(latLng.latitude, latLng.longitude);
if (placemarks.isNotEmpty) {
final place = placemarks.first;
setState(() {
selectedAddress =
"${place.name}, ${place.locality}, ${place.administrativeArea}, ${place.postalCode}";
});
} else {
setState(() {
selectedAddress = "Unknown location";
});
}
} catch (e) {
setState(() {
selectedAddress = "Failed to get address";
});
}
}
Future<void> _goToCurrentLocation() async {
try {
Position position = await Geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.high);
LatLng latLng = LatLng(position.latitude, position.longitude);
setState(() {
selectedLatLng = latLng;
});
mapController?.animateCamera(CameraUpdate.newLatLngZoom(latLng, 16));
await _getAddressFromLatLng(latLng);
} catch (e) {
// Get.snackbar();
}
}
@override
Widget build(BuildContext context) {
return SafeArea(
top: false,
child: Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
backgroundColor: Colors.white,
title: const Text("Pick Location"),
actions: [
IconButton(
onPressed: _goToCurrentLocation,
icon: Container(
decoration: BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.black26,
blurRadius: 4,
offset: Offset(0, 2),
),
],
),
child: const Padding(
padding: EdgeInsets.all(8.0),
child: Icon(Icons.my_location, color: Colors.black),
),
),
),
],
),
body: Stack(
children: [
GoogleMap(
initialCameraPosition:
CameraPosition(target: currentLatLng, zoom: 14),
onMapCreated: (controller) => mapController = controller,
onTap: (latLng) async {
setState(() {
selectedLatLng = latLng;
});
await _getAddressFromLatLng(latLng);
},
markers: selectedLatLng != null
? {
Marker(
markerId: const MarkerId("picked"),
position: selectedLatLng!)
}
: {},
myLocationEnabled: true,
myLocationButtonEnabled: false,
),
// Floating button for current location
// Address card
if (selectedAddress != null)
Positioned(
bottom: 80,
left: 16,
right: 16,
child: Card(
child: Padding(
padding: const EdgeInsets.all(12),
child: Text(
selectedAddress!,
style: const TextStyle(fontSize: 14),
),
),
),
),
],
),
bottomNavigationBar: Padding(
padding: const EdgeInsets.all(16),
child: ElevatedButton(
onPressed: selectedLatLng == null
? null
: () async {
String address = selectedAddress ?? "";
String suburb = "";
String city = "";
String state = "";
String postcode = "";
try {
List<Placemark> placemarks =
await placemarkFromCoordinates(
selectedLatLng!.latitude,
selectedLatLng!.longitude);
if (placemarks.isNotEmpty) {
final place = placemarks.first;
suburb = place.subLocality ?? "";
city = place.locality ?? "";
state = place.administrativeArea ?? "";
postcode = place.postalCode ?? "";
final result = await Get.to(() => AddressDetailsPage(
address: address,
suburb: suburb,
city: city,
state: state,
postcode: postcode,
latitude: selectedLatLng!.latitude.toString(),
longitude: selectedLatLng!.longitude.toString(),
));
if (result == true) {
Get.back(result: true);
}
}
} catch (e) {
print("Error parsing placemark: $e");
}
},
style: ElevatedButton.styleFrom(
backgroundColor: ColorConstants.primaryColor,
padding: const EdgeInsets.symmetric(vertical: 16)),
child: const Text(
"Confirm Location",
style: TextStyle(color: Colors.white),
),
),
),
),
);
}
}
class AddressDetailsPage extends StatefulWidget {
final String address;
final String? suburb;
final String? city;
final String? state;
final String? postcode;
final String? latitude;
final String? longitude;
const AddressDetailsPage({
super.key,
required this.address,
this.suburb,
this.city,
this.state,
this.postcode,
this.latitude,
this.longitude,
});
@override
State<AddressDetailsPage> createState() => _AddressDetailsPageState();
}
class _AddressDetailsPageState extends State<AddressDetailsPage> {
final _formKey = GlobalKey<FormState>();
late TextEditingController addressController;
late TextEditingController doorController;
late TextEditingController landmarkController;
bool isLoading = false;
final CustomerLocationProvider provider = CustomerLocationProvider();
@override
void initState() {
super.initState();
addressController = TextEditingController(text: widget.address);
doorController = TextEditingController();
landmarkController = TextEditingController();
}
@override
void dispose() {
addressController.dispose();
doorController.dispose();
landmarkController.dispose();
super.dispose();
}
void submitAddress() async {
if (!_formKey.currentState!.validate()) return;
setState(() => isLoading = true);
final SharedPreferences prefs = await SharedPreferences.getInstance();
final id = prefs.getInt('customerId');
final success = await provider.createCustomerLocation(
customerId: id!, // Replace with your dynamic customer ID
address: addressController.text,
doorNo: doorController.text,
landmark: landmarkController.text,
suburb: widget.suburb ?? "",
city: widget.city ?? "",
state: widget.state ?? "",
postcode: widget.postcode ?? "",
latitude: widget.latitude ?? "",
longitude: widget.longitude ?? "",
defaultAddress: "Yes",
primaryAddress: 1,
status: 1,
);
setState(() => isLoading = false);
Get.until((route) => route.settings.name == '/LocationPage');
if (success == true) {
print("API Success ✅");
Get.snackbar("Success", "Address submitted successfully");
await Future.delayed(const Duration(milliseconds: 800));
Get.back(result: true);
} else {
print("API failed ❌");
Get.snackbar("Error", "Failed to submit address");
}
}
@override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
backgroundColor: Colors.grey[200],
appBar: AppBar(title: const Text("Edit Address"),backgroundColor: Colors.grey[200],),
body: Padding(
padding: const EdgeInsets.all(16),
child: Form(
key: _formKey,
child: ListView(
children: [
_buildTextField("Address", addressController),
const SizedBox(height: 12),
_buildTextField("Door Number", doorController),
const SizedBox(height: 12),
_buildTextField("Landmark", landmarkController),
const SizedBox(height: 20),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF662582), // Purple color
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
minimumSize: const Size(double.infinity, 50), // full width
),
onPressed: isLoading ? null : submitAddress,
child: isLoading
? const CircularProgressIndicator(color: Colors.white)
: const Text(
"Submit Address",
style: TextStyle(color: Colors.white, fontSize: 16),
),
)
],
),
),
),
),
);
}
Widget _buildTextField(String label, TextEditingController controller) {
return TextFormField(
controller: controller,
decoration: InputDecoration(
labelText: label,
border: const OutlineInputBorder(),
),
validator: (value) => value == null || value.isEmpty ? "Enter $label" : null,
);
}
}