first commit

This commit is contained in:
Anbarasu
2026-05-26 18:01:57 +05:30
commit 6d59c8daf6
297 changed files with 35238 additions and 0 deletions

View File

@@ -0,0 +1,873 @@
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,
);
}
}