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,805 @@
import 'dart:convert';
import 'dart:io';
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:get/get.dart' hide Response;
import 'package:http/http.dart' as http;
import 'package:minio/io.dart';
import 'package:minio/minio.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:shimmer/shimmer.dart';
import 'package:image_picker/image_picker.dart';
import '../../constants/color_constants.dart';
import '../../constants/font_constants.dart';
import '../../controllers/account_controller/profile.dart';
import '../../domain/repository/authentication/auth_repository.dart';
import '../../modules/authentication/auth.dart';
import '../../modules/authentication/getbyid.dart';
import '../../widgets/text_widget.dart';
class EditProfilePage extends StatefulWidget {
const EditProfilePage({super.key});
@override
State<EditProfilePage> createState() => _EditProfilePageState();
}
class _EditProfilePageState extends State<EditProfilePage> {
CustomerFullView? profile;
bool isLoading = true;
File? pickedImage;
final AccountController accountController = Get.find<AccountController>();
String Name = '';
String Adress = '';
String Profile = '';
String Number = '';
Future<void> _pickImage() async {
final pickedFile =
await ImagePicker().pickImage(source: ImageSource.gallery);
if (pickedFile != null) {
setState(() {
pickedImage = File(pickedFile.path);
});
}
}
// Controllers for editable fields
final TextEditingController _nameController = TextEditingController();
final TextEditingController _contactController = TextEditingController();
final TextEditingController _dobController = TextEditingController();
final TextEditingController _genderController = TextEditingController();
final TextEditingController _addressController = TextEditingController();
@override
void initState() {
super.initState();
_loadProfile();
}
Future<void> _loadProfile() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
int? id = prefs.getInt('customerId');
if (id == null) {
Get.snackbar("Error", "Customer ID not found");
return;
}
setState(() => isLoading = true);
final repo = LoginRepository();
final fetchedProfile = await repo.fetchProfile(id.toString());
if (fetchedProfile != null) {
_nameController.text = fetchedProfile.firstname ?? '';
_contactController.text = fetchedProfile.contactno ?? '';
_dobController.text = fetchedProfile.dob != null
? fetchedProfile.dob!.toIso8601String()
: ''; _genderController.text = fetchedProfile.gender ?? '';
_addressController.text = fetchedProfile.address ?? '';
Name = fetchedProfile.firstname ?? '';
Profile = fetchedProfile.profileimage ?? '';
Number = fetchedProfile.contactno ?? '';
Adress = fetchedProfile.address ?? '';
}
setState(() {
profile = fetchedProfile;
isLoading = false;
});
}
Future<String?> uploadImageAndSave(
File selectedImage, int customerId) async {
try {
var rng = Random();
const String region = "sgp1";
const String accessKey = "DO00NQER7N2FRYZAB2HR";
const String secretKey = "nMDewX25IBEu1FM5dakK+v28/WbW3TzBAwq913+dxP0";
const String bucketName = "nearle";
const String folderName = "deals";
String fileName = 'profile-${rng.nextInt(1000)}-$customerId.jpg';
String endpointUrl =
"https://$bucketName.$region.digitaloceanspaces.com/$folderName/$fileName";
// Initialize Minio client
final minio = Minio(
endPoint: '$region.digitaloceanspaces.com',
accessKey: accessKey,
secretKey: secretKey,
region: region,
useSSL: true,
);
// Upload file
await minio.fPutObject(
bucketName,
'$folderName/$fileName',
selectedImage.path,
metadata: {
'Content-Type': 'image/jpeg',
'x-amz-acl': 'public-read', // Set ACL to public-read if needed
},
);
print("File uploaded successfully: $endpointUrl");
return endpointUrl;
} catch (e) {
Get.snackbar("Error", "Image upload failed: $e");
print("Upload error: $e");
return null;
}
}
Future<Map<String, String>> fetchAddressDetails(String address) async {
final url = Uri.parse(
'https://nominatim.openstreetmap.org/search'
'?q=${Uri.encodeComponent(address)}'
'&format=json'
'&addressdetails=1',
);
final response = await http.get(
url,
headers: {'User-Agent': 'FlutterApp'},
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
if (data.isNotEmpty) {
final item = data[0];
final addr = item['address'] ?? {};
return {
"suburb": addr['suburb'] ?? addr['neighbourhood'] ?? '',
"city": addr['city'] ?? addr['town'] ?? addr['village'] ?? '',
"state": addr['state'] ?? '',
"postcode": addr['postcode'] ?? '',
"landmark": addr['road'] ?? addr['attraction'] ?? '',
"latitude": item['lat'] ?? '',
"longitude": item['lon'] ?? '',
};
}
}
/// fallback (never null)
return {
"suburb": '',
"city": '',
"state": '',
"postcode": '',
"landmark": '',
"latitude": '',
"longitude": '',
};
}
Future<void> _updateProfile() async {
if (profile == null) {
Get.snackbar("Error", "Profile data not loaded");
return;
}
SharedPreferences prefs = await SharedPreferences.getInstance();
int? customerId = prefs.getInt('customerId');
if (customerId == null) {
Get.snackbar("Error", "Customer ID not found");
return;
}
setState(() => isLoading = true);
String? uploadedFileUrl;
/// Upload image if selected
if (pickedImage != null) {
uploadedFileUrl = await uploadImageAndSave(pickedImage!, customerId);
}
/// 🌍 AUTO-FETCH ADDRESS DETAILS
final addressDetails =
await fetchAddressDetails(_addressController.text.trim());
final data = {
"customerid": customerId,
"configid": profile!.configid ?? 1,
"firstname": _nameController.text.trim(),
"applocationid": profile!.applocationid ?? 91,
"contactno": _contactController.text.trim(),
"address": _addressController.text.trim(),
"gender": _genderController.text.trim(),
"dob": _dobController.text.trim(),
"profileimage": uploadedFileUrl ?? profile!.profileimage,
// ✅ AUTO FILLED
"doorno": "",
"suburb": addressDetails['suburb'],
"city": addressDetails['city'],
"state": addressDetails['state'],
"postcode": addressDetails['postcode'],
"landmark": addressDetails['landmark'],
"latitude": addressDetails['latitude'],
"longitude": addressDetails['longitude'],
};
print("PROFILE UPDATE REQUEST => $data");
try {
final repo = LoginRepository();
final response = await repo.updateProfile(data);
setState(() => isLoading = false);
if (response != null && response['status'] == true) {
Get.snackbar("Success", response['message'] ?? "Profile updated");
Navigator.pop(context, true);
} else {
Get.snackbar(
"Error",
response?['message'] ?? "Profile update failed",
);
}
} catch (e) {
setState(() => isLoading = false);
Get.snackbar("Error", e.toString());
}
}
List<dynamic> predictions = [];
// Replace with your API key
final String googleApiKey = "AIzaSyBhkGfnq27sN0wV5y_S-M2KojpFTk_by-Q";
Future<void> searchPlace(String input) async {
if (input.isEmpty) {
setState(() {
predictions = [];
});
return;
}
String url =
'https://maps.googleapis.com/maps/api/place/autocomplete/json?input=$input&types=geocode&components=country:in&key=$googleApiKey';
final response = await http.get(Uri.parse(url));
if (response.statusCode == 200) {
final data = json.decode(response.body);
setState(() {
predictions = data['predictions'];
});
}
}
@override
Widget build(BuildContext context) {
final Size screenSize = MediaQuery.of(context).size;
return SafeArea(
top: false,
bottom: true,
child: Scaffold(
backgroundColor: const Color(0xFFFAFAFA),
appBar: AppBar(
backgroundColor: Colors.white,
elevation: 0,
shadowColor: Colors.black.withOpacity(0.05),
leading: IconButton(
icon: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(10),
),
child: const Icon(Icons.arrow_back_ios_new,
color: Colors.black87, size: 18),
),
onPressed: () => Navigator.of(context).pop(),
),
title: const ReusableTextWidget(
text: "Edit Profile",
color: Colors.black87,
fontFamily: FontConstants.fontFamily,
fontSize: 20,
fontWeight: FontWeight.w700,
),
centerTitle: true,
),
body: isLoading
? _buildShimmer(screenSize)
: SingleChildScrollView(
padding: EdgeInsets.symmetric(
horizontal: screenSize.width * 0.05,
vertical: screenSize.height * 0.02,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Profile Image Section
Center(
child: Column(
children: [
Stack(
alignment: Alignment.bottomRight,
children: [
Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: LinearGradient(
colors: [
ColorConstants.primaryColor
.withOpacity(0.1),
ColorConstants.primaryColor
.withOpacity(0.05),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
boxShadow: [
BoxShadow(
color: ColorConstants.primaryColor
.withOpacity(0.15),
blurRadius: 20,
offset: const Offset(0, 10),
),
],
),
child: Container(
margin: const EdgeInsets.all(4),
decoration: const BoxDecoration(
shape: BoxShape.circle,
color: Colors.white,
),
child: CircleAvatar(
radius: screenSize.height * 0.09,
backgroundColor: Colors.grey.shade100,
backgroundImage: pickedImage != null
? FileImage(pickedImage!)
as ImageProvider
: (profile?.profileimage != null &&
profile!.profileimage!.isNotEmpty
? NetworkImage(
profile!.profileimage!)
: null),
child: (pickedImage == null &&
(profile?.profileimage == null ||
profile!.profileimage!.isEmpty))
? Icon(
Icons.person_outline,
size: 60,
color: Colors.grey.shade400,
)
: null,
),
),
),
GestureDetector(
onTap: _pickImage,
child: Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: ColorConstants.primaryColor,
shape: BoxShape.circle,
border: Border.all(
color: Colors.white,
width: 3,
),
boxShadow: [
BoxShadow(
color: ColorConstants.primaryColor
.withOpacity(0.3),
blurRadius: 8,
offset: const Offset(0, 4),
),
],
),
child: const Icon(
Icons.camera_alt_rounded,
color: Colors.white,
size: 20,
),
),
),
],
),
SizedBox(height: screenSize.height * 0.015),
Text(
"Tap to change profile photo",
style: TextStyle(
color: Colors.grey.shade600,
fontSize: 13,
fontFamily: FontConstants.fontFamily,
),
),
],
),
),
SizedBox(height: screenSize.height * 0.04),
// Form Section
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.04),
blurRadius: 15,
offset: const Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildLabel("Full Name"),
const SizedBox(height: 8),
_buildEditableField(
_nameController,
Icons.person_outline_rounded,
"Enter your name",
),
SizedBox(height: screenSize.height * 0.025),
_buildLabel("Contact Number"),
const SizedBox(height: 8),
_buildEditableField(
_contactController,
Icons.phone_outlined,
"Enter your phone number",
),
SizedBox(height: screenSize.height * 0.025),
_buildLabel("Address"),
const SizedBox(height: 8),
Container(
decoration: BoxDecoration(
color: const Color(0xFFF5F6FA),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: Colors.grey.shade200,
width: 1,
),
),
child: TextFormField(
controller: _addressController,
style: const TextStyle(
fontSize: 15,
fontFamily: FontConstants.fontFamily,
),
decoration: InputDecoration(
hintText: "Search location...",
hintStyle: TextStyle(
color: Colors.grey.shade400,
fontSize: 15,
),
filled: true,
fillColor: Colors.transparent,
contentPadding: const EdgeInsets.symmetric(
vertical: 16,
horizontal: 16,
),
prefixIcon: Icon(
Icons.location_on_outlined,
color: ColorConstants.primaryColor,
size: 22,
),
suffixIcon: _addressController.text.isNotEmpty
? IconButton(
icon: Icon(
Icons.close_rounded,
color: Colors.grey.shade400,
),
onPressed: () {
_addressController.clear();
setState(() {
predictions = [];
});
},
)
: null,
border: InputBorder.none,
enabledBorder: InputBorder.none,
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(
color: ColorConstants.primaryColor,
width: 1.5,
),
),
),
onChanged: (value) {
searchPlace(value);
setState(() {});
},
),
),
const SizedBox(height: 8),
// Display suggestions
if (predictions.isNotEmpty)
Container(
constraints: BoxConstraints(
maxHeight: screenSize.height * 0.3,
),
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(
color: Colors.grey.shade200,
),
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.08),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: ListView.separated(
shrinkWrap: true,
padding: const EdgeInsets.symmetric(vertical: 8),
itemCount: predictions.length,
separatorBuilder: (context, index) => Divider(
height: 1,
color: Colors.grey.shade100,
),
itemBuilder: (context, index) {
final prediction = predictions[index];
return ListTile(
dense: true,
leading: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: ColorConstants.primaryColor
.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(
Icons.location_on,
color: ColorConstants.primaryColor,
size: 18,
),
),
title: Text(
prediction['description'],
style: const TextStyle(
fontSize: 14,
fontFamily: FontConstants.fontFamily,
),
),
onTap: () {
_addressController.text =
prediction['description'];
setState(() {
predictions = [];
});
FocusScope.of(context).unfocus();
},
);
},
),
),
],
),
),
SizedBox(height: screenSize.height * 0.1),
],
),
),
// Bottom Button
bottomNavigationBar: Container(
padding: EdgeInsets.symmetric(
horizontal: screenSize.width * 0.05,
vertical: screenSize.height * 0.02,
),
decoration: BoxDecoration(
color: Colors.white,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, -5),
),
],
),
child: Container(
height: screenSize.height * 0.065,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
ColorConstants.primaryColor,
ColorConstants.primaryColor.withOpacity(0.8),
],
begin: Alignment.centerLeft,
end: Alignment.centerRight,
),
borderRadius: BorderRadius.circular(14),
boxShadow: [
BoxShadow(
color: ColorConstants.primaryColor.withOpacity(0.3),
blurRadius: 15,
offset: const Offset(0, 8),
),
],
),
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.transparent,
shadowColor: Colors.transparent,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
),
),
onPressed: _updateProfile,
child: const ReusableTextWidget(
text: "Update Profile",
color: Colors.white,
fontFamily: FontConstants.fontFamily,
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
),
),
);
}
/// Label
Widget _buildLabel(String text) {
return ReusableTextWidget(
text: text,
color: const Color(0xFF2D3142),
fontFamily: FontConstants.fontFamily,
fontSize: 14,
fontWeight: FontWeight.w600,
);
}
/// Editable Text Field
Widget _buildEditableField(
TextEditingController controller,
IconData icon,
String hint,
) {
return Container(
decoration: BoxDecoration(
color: const Color(0xFFF5F6FA),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: Colors.grey.shade200,
width: 1,
),
),
child: TextFormField(
controller: controller,
style: const TextStyle(
fontSize: 15,
fontFamily: FontConstants.fontFamily,
),
decoration: InputDecoration(
hintText: hint,
hintStyle: TextStyle(
color: Colors.grey.shade400,
fontSize: 15,
),
prefixIcon: Icon(
icon,
color: ColorConstants.primaryColor,
size: 22,
),
filled: true,
fillColor: Colors.transparent,
contentPadding: const EdgeInsets.symmetric(
vertical: 16,
horizontal: 16,
),
border: InputBorder.none,
enabledBorder: InputBorder.none,
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(
color: ColorConstants.primaryColor,
width: 1.5,
),
),
),
),
);
}
/// Shimmer effect while loading
Widget _buildShimmer(Size screenSize) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(height: screenSize.height * 0.04),
/// Profile image shimmer
Shimmer.fromColors(
baseColor: Colors.grey.shade200,
highlightColor: Colors.grey.shade50,
child: Container(
width: screenSize.height * 0.18,
height: screenSize.height * 0.18,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.grey.shade300,
),
),
),
SizedBox(height: screenSize.height * 0.01),
/// Helper text shimmer
Shimmer.fromColors(
baseColor: Colors.grey.shade200,
highlightColor: Colors.grey.shade50,
child: Container(
height: 14,
width: screenSize.width * 0.4,
decoration: BoxDecoration(
color: Colors.grey.shade300,
borderRadius: BorderRadius.circular(4),
),
),
),
SizedBox(height: screenSize.height * 0.04),
/// Form container shimmer
Shimmer.fromColors(
baseColor: Colors.grey.shade200,
highlightColor: Colors.grey.shade50,
child: Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.grey.shade300,
borderRadius: BorderRadius.circular(16),
),
child: Column(
children: List.generate(
3,
(_) => Padding(
padding: const EdgeInsets.symmetric(vertical: 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
height: 14,
width: 100,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(4),
),
),
const SizedBox(height: 8),
Container(
height: 50,
width: double.infinity,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
),
),
],
),
),
),
),
),
),
],
),
);
}
}