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,854 @@
import 'dart:io';
import 'package:dotted_line/dotted_line.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:nearledaily/view/account/share_app.dart';
import 'package:nearledaily/view/authentication/login_view.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:shimmer/shimmer.dart';
import '../../constants/color_constants.dart';
import '../../constants/font_constants.dart';
import '../../controllers/account_controller/profile.dart';
import '../../controllers/authentication/auth_controller.dart';
import '../../domain/repository/authentication/auth_repository.dart';
import '../../service/bindings.dart';
import '../../widgets/text_widget.dart';
import '../orders/orders_by_tenant.dart';
import 'edit_profile_view.dart';
import 'faq_view.dart';
import 'help/create_request.dart';
import 'notification_settings_view.dart';
class AccountPage extends StatefulWidget {
const AccountPage({super.key});
@override
State<AccountPage> createState() => _AccountPageState();
}
class _AccountPageState extends State<AccountPage> {
static const Color primaryColor = Color(0xFF662582);
final controller = Get.put(AccountController());
String Name = '';
String Profile = '';
String Number = '';
@override
void initState() {
super.initState();
_loadProfile();
}
Future<void> _loadProfile() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
int? id = prefs.getInt('customerId');
if (id == null) return;
final repo = LoginRepository();
final fetchedProfile = await repo.fetchProfile(id.toString());
if (fetchedProfile != null) {
setState(() {
Name = fetchedProfile.firstname ?? '';
Profile = fetchedProfile.profileimage ?? '';
Number = fetchedProfile.contactno ?? '';
});
print(Name);
print(Profile);
print(Number);
}
}
Widget _profileShimmer() {
return Container(
margin: const EdgeInsets.all(16),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
),
child: Row(
children: [
Shimmer.fromColors(
baseColor: Colors.grey.shade300, // shimmer base
highlightColor: Colors.grey.shade100, // shimmer highlight
child: CircleAvatar(
radius: 28,
backgroundColor: Colors.grey.shade200, // light background
child: Icon(
Icons.person,
size: 28,
color: Colors.grey.shade500, // darker icon color
),
),
),
const SizedBox(width: 12),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Shimmer.fromColors(
baseColor: Colors.grey[300]!,
highlightColor: Colors.grey[100]!,
child: Container(
height: 14,
width: 120,
decoration: BoxDecoration(
color: Colors.white, // <-- background color goes here
borderRadius: BorderRadius.circular(8), // <-- rounded corners
),
),
),
const SizedBox(height: 6),
Shimmer.fromColors(
baseColor: Colors.grey[300]!,
highlightColor: Colors.grey[100]!,
child: Container(
height: 12,
width: 90,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8), // <-- add radius here too
),
),
),
],
)
],
),
);
}
@override
Widget build(BuildContext context) {
SystemChrome.setSystemUIOverlayStyle(
const SystemUiOverlayStyle(
statusBarColor: Colors.white, // or transparent
statusBarIconBrightness: Brightness.dark, // Android
statusBarBrightness: Brightness.light, // iOS
),
);
return SafeArea(
child: Scaffold(
extendBodyBehindAppBar: false,
backgroundColor: Color(0xFFF6F6F6),
appBar: AppBar(
backgroundColor: Colors.white,
surfaceTintColor: Colors.transparent,
// 🔥 Prevent color overlay when scrolled
scrolledUnderElevation: 0,
animateColor: false, // ✨ prevent color change on scroll
elevation: 0,
title: ReusableTextWidget(
text: "Profile",
fontSize: 20,
fontWeight: FontWeight.w600,
fontFamily: FontConstants.fontFamily,
color: Colors.black,
),
),
body: Obx(
() => SingleChildScrollView(
physics: const ClampingScrollPhysics(),
child: Column(
children: [
const SizedBox(height: 20),
/// PROFILE CARD (EXACT LIKE IMAGE)
controller.isLoading.value
? _profileShimmer()
: GestureDetector(
onTap: () async {
final res = await Get.to(
() => EditProfilePage(),
// transition: Transition.fade, // Your desired transition
// duration: Duration(milliseconds: 400), // Duration of the transition
);
if (res != null && res['status'] == true) {
_loadProfile();
}
},
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 11),
padding: const EdgeInsets.only(
left: 16,
right: 16,
top: 30,
bottom: 30,
),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
Color(0xFF1B1333), // Dark background (luxury dark purple/indigo)
Color(0xFF662582).withOpacity(0.9), // Primary color accent
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: Colors.black12,
width: 0.30
),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.03),
blurRadius: 2,
offset: const Offset(0, 3),
),
],
),
child: Row(
children: [
CircleAvatar(
radius: 28,
backgroundColor: Colors.grey.shade300,
backgroundImage:
Profile.isNotEmpty ? NetworkImage(Profile) : null,
child: Profile.isEmpty
? const Icon(Icons.person, size: 26)
: null,
),
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ReusableTextWidget(
text: Name,
fontSize: 16,
fontWeight: FontWeight.w600,
fontFamily: FontConstants.fontFamily,
color: Colors.white,
),
const SizedBox(height: 4),
ReusableTextWidget(
text: Number,
fontSize: 13,
fontWeight: FontWeight.w400,
fontFamily: FontConstants.fontFamily,
color: Colors.white,
),
],
),
),
],
),
),
),
const SizedBox(height: 24),
/// ACCOUNT
controller.isLoading.value
? accountListShimmerSingleBox()
:
_section(
title: "Account",
children: [
_tile(
icon: Icons.person,
title: "Manage Profile",
onTap: () async {
final res = await Get.to(
() => EditProfilePage(),
// transition: Transition.fade, // Your desired transition
// duration: Duration(milliseconds: 400), // Duration of the transition
);
if (res != null && res['status'] == true) {
_loadProfile();
}
},
),
_divider(),
_tile(
icon: Icons.question_answer,
title: "Faq",
onTap: () => Get.to(
() => FaqView(),
// transition: Transition.fade, // or any transition you like
// duration: Duration(milliseconds: 400),
),
),
_divider(),
_tile(
icon: Icons.reorder,
title: "Your Orders",
onTap: () => Get.to(
() => const OrdersByStoreScreen(showBackArrow: true),
// transition: Transition.fade, // or any transition you prefer
// duration: Duration(milliseconds: 400),
),
),
// _divider(),
],
),
/// PREFERENCES
controller.isLoading.value
? Preferences()
:
_section(
title: "Preferences",
children: [
_tile(
icon: Icons.star_rate,
title: "Rate the app in Playstore",
onTap: controller.rateApp,
),
_divider(),
// _tile(
// icon: Icons.group_add,
// title: "Refer a Friend",
// onTap: () => Get.to(
// () => const ShowContactsScreen(),
// // transition: Transition.fade, // or any style you like
// // duration: Duration(milliseconds: 400),
// ),
// ),
],
),
/// SUPPORT
controller.isLoading.value
? Preferences()
:
_section(
title: "Support",
children: [
_tile(
icon: Icons.support_agent,
title: "Help & Support",
onTap: () => Get.to(
() => Help_Support(),
// transition: Transition.fade, // simple fade
// duration: Duration(milliseconds: 400),
),
),
_divider(),
_tile(
icon: Icons.logout,
title: "Logout",
isLogout: true,
onTap: () {
showLogoutDialog();
},
),
],
),
const SizedBox(height: 20),
/// LOGOUT
// GestureDetector(
// onTap: showLogoutDialog,
// child: Container(
// margin: const EdgeInsets.symmetric(horizontal: 16),
// padding: const EdgeInsets.symmetric(vertical: 14),
// decoration: BoxDecoration(
// color: Colors.white,
// borderRadius: BorderRadius.circular(16),
// border: Border.all(color: Colors.red, width: 0.4),
// ),
// child: Center(
// child: ReusableTextWidget(
// text: "Logout",
// color: Colors.red,
// fontSize: 16,
// fontWeight: FontWeight.w600,
// fontFamily: FontConstants.fontFamily,
// ),
// ),
// ),
// ),
const SizedBox(height: 30),
],
),
),
),
),
);
}
/// SECTION CARD
Widget _section({
required String title,
required List<Widget> children,
}) {
return Padding(
padding: const EdgeInsets.fromLTRB(11, 0, 11, 11),
child: Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: Colors.black12,
width: 0.20
),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.03),
blurRadius: 2,
offset: const Offset(0, 3),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(16, 14, 16, 6),
child: ReusableTextWidget(
text: title,
fontSize: 14,
fontWeight: FontWeight.bold,
color: Colors.black,
fontFamily: FontConstants.fontFamily,
),
),
...children,
],
),
),
);
}
/// LIST TILE (EXACT STYLE)
Widget _tile({
required IconData icon,
required String title,
required VoidCallback onTap,
bool isLogout = false,
}) {
final Color mainColor =
isLogout ? Colors.red : Colors.black45;
return ListTile(
dense: true,
minVerticalPadding: 0,
contentPadding: const EdgeInsets.symmetric(horizontal: 12),
visualDensity: const VisualDensity(vertical: 0.10),
leading: Icon(
icon,
size: 22,
color: mainColor,
shadows: [
Shadow(
color: Colors.grey.withOpacity(0.2),
offset: const Offset(0.30, 0.30),
blurRadius: 1,
),
],
),
title: ReusableTextWidget(
text: title,
fontSize: 13,
fontWeight: FontWeight.w700,
fontFamily: FontConstants.fontFamily,
color: isLogout
? Colors.red
: Colors.black.withOpacity(0.7),
),
// 👇 Arrow ALWAYS normal black
trailing: Icon(
Icons.arrow_forward_ios,
size: 14,
weight: 400,
color: Colors.black.withOpacity(0.9),
),
onTap: onTap,
);
}
Widget _divider() {
return Divider(
color: Color(0xFFF6F6F6),
thickness: 1.5,
height: 0.10, //
);
}
/// LOGOUT DIALOG (UNCHANGED LOGIC)
void showLogoutDialog() {
Get.dialog(
Dialog(
elevation: 0,
backgroundColor: Colors.transparent,
child: Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.15),
blurRadius: 20,
offset: const Offset(0, 10),
),
],
),
padding: const EdgeInsets.all(22),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// 🔴 Icon Container
Container(
height: 64,
width: 64,
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: LinearGradient(
colors: [
primaryColor.withOpacity(0.9),
primaryColor,
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
child: const Icon(
Icons.logout_rounded,
size: 30,
color: Colors.white,
),
),
const SizedBox(height: 16),
// 📝 Title
ReusableTextWidget(
text: "Logout",
fontSize: 18,
fontWeight: FontWeight.w600,
fontFamily: FontConstants.fontFamily,
color: Colors.black,
),
const SizedBox(height: 6),
// 🧾 Subtitle
ReusableTextWidget(
text: "Are you sure you want to logout?",
fontSize: 14,
fontWeight: FontWeight.w400,
fontFamily: FontConstants.fontFamily,
color: Colors.black54,
textAlign: TextAlign.center,
),
const SizedBox(height: 24),
// 🔘 Buttons
Row(
children: [
Expanded(
child: OutlinedButton(
onPressed: () => Navigator.pop(context),
style: OutlinedButton.styleFrom(
foregroundColor: primaryColor,
side: BorderSide(color: primaryColor),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
padding: const EdgeInsets.symmetric(vertical: 12),
),
child: ReusableTextWidget(
text: "Cancel",
fontFamily: FontConstants.fontFamily,
fontWeight: FontWeight.w500,
),
),
),
const SizedBox(width: 12),
Expanded(
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: primaryColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
elevation: 4,
padding: const EdgeInsets.symmetric(vertical: 12),
),
onPressed: () async {
final prefs = await SharedPreferences.getInstance();
String fcmToken = prefs.getString('fcmToken') ?? '';
String deviceId =
prefs.getString('currentDeviceId') ?? '';
await prefs.clear();
await prefs.setString('fcmToken', fcmToken);
await prefs.setString('currentDeviceId', deviceId);
Get.deleteAll();
GlobalBinding().dependencies();
Get.offAll(() => Login_view());
},
child: ReusableTextWidget(
text: "Logout",
fontFamily: FontConstants.fontFamily,
color: Colors.white,
fontWeight: FontWeight.w600,
),
),
),
],
),
],
),
),
),
);
}
Widget accountListShimmerSingleBox() {
return Container(
margin: const EdgeInsets.all(16),
padding: const EdgeInsets.symmetric(vertical: 8),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
),
child: Column(
children: List.generate(4, (index) {
return Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 14,
),
child: Row(
children: [
// Leading avatar
Shimmer.fromColors(
baseColor: Colors.grey.shade300,
highlightColor: Colors.grey.shade100,
child: Container(
height: 44,
width: 44,
decoration: const BoxDecoration(
color: Colors.white,
),
),
),
const SizedBox(width: 14),
// Title + subtitle
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Shimmer.fromColors(
baseColor: Colors.grey.shade300,
highlightColor: Colors.grey.shade100,
child: Padding(
padding: const EdgeInsets.only(right: 18.0),
child: Container(
height: 14,
width: double.infinity,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(6),
),
),
),
),
const SizedBox(height: 8),
Shimmer.fromColors(
baseColor: Colors.grey.shade300,
highlightColor: Colors.grey.shade100,
child: Container(
height: 12,
width: 120,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(6),
),
),
),
],
),
),
const SizedBox(width: 12),
// Trailing arrow shimmer
Shimmer.fromColors(
baseColor: Colors.grey.shade300,
highlightColor: Colors.grey.shade100,
child: Container(
height: 18,
width: 18,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(4),
),
),
),
],
),
),
// Divider (except last)
if (index != 3)
Padding(
padding: const EdgeInsets.only(left: 74),
child: Divider(
height: 1,
thickness: 0.8,
color: Colors.grey.shade200,
),
),
],
);
}),
),
);
}
Widget Preferences() {
return Container(
margin: const EdgeInsets.all(16),
padding: const EdgeInsets.symmetric(vertical: 8),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
),
child: Column(
children: List.generate(2, (index) {
return Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 14,
),
child: Row(
children: [
// Leading avatar
Shimmer.fromColors(
baseColor: Colors.grey.shade300,
highlightColor: Colors.grey.shade100,
child: Container(
height: 44,
width: 44,
decoration: const BoxDecoration(
color: Colors.white,
),
),
),
const SizedBox(width: 14),
// Title + subtitle
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Shimmer.fromColors(
baseColor: Colors.grey.shade300,
highlightColor: Colors.grey.shade100,
child: Padding(
padding: const EdgeInsets.only(right: 18.0),
child: Container(
height: 14,
width: double.infinity,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(6),
),
),
),
),
const SizedBox(height: 8),
Shimmer.fromColors(
baseColor: Colors.grey.shade300,
highlightColor: Colors.grey.shade100,
child: Container(
height: 12,
width: 120,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(6),
),
),
),
],
),
),
const SizedBox(width: 12),
// Trailing arrow shimmer
Shimmer.fromColors(
baseColor: Colors.grey.shade300,
highlightColor: Colors.grey.shade100,
child: Container(
height: 18,
width: 18,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(4),
),
),
),
],
),
),
// Divider (except last)
if (index != 1)
Padding(
padding: const EdgeInsets.only(left: 74),
child: Divider(
height: 1,
thickness: 0.8,
color: Colors.grey.shade200,
),
),
],
);
}),
),
);
}
}

View File

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),
),
),
],
),
),
),
),
),
),
],
),
);
}
}

View File

@@ -0,0 +1,43 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:webview_flutter/webview_flutter.dart';
import '../../controllers/account_controller/faq_controller.dart';
class FaqView extends GetView<FaqController> {
FaqView({super.key});
final FaqController controller = Get.put(FaqController());
@override
Widget build(BuildContext context) {
return AnnotatedRegion<SystemUiOverlayStyle>(
value: const SystemUiOverlayStyle(
statusBarColor: Colors.white, // White background
statusBarIconBrightness: Brightness.dark, // Dark icons
statusBarBrightness: Brightness.light, // iOS
),
child: Scaffold(
appBar: AppBar(
backgroundColor: Colors.white,
elevation: 0,
title: const Text(
'FAQ',
style: TextStyle(color: Colors.black),
),
iconTheme: const IconThemeData(color: Colors.black),
),
body: Obx(
() => Stack(
children: [
if (controller.webViewController != null)
WebViewWidget(controller: controller.webViewController!),
if (controller.isLoading.value)
const Center(child: CircularProgressIndicator()),
],
),
),
),
);
}
}

View File

@@ -0,0 +1,328 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:lottie/lottie.dart';
import 'package:provider/provider.dart';
import 'package:shimmer/shimmer.dart';
import '../../../constants/color_constants.dart';
import '../../../constants/font_constants.dart';
import '../../../domain/provider/profile/create_request.dart';
import '../../../widgets/text_widget.dart';
import 'request_page.dart';
class Help_Support extends StatefulWidget {
const Help_Support({Key? key}) : super(key: key);
@override
State<Help_Support> createState() => _Help_SupportState();
}
class _Help_SupportState extends State<Help_Support> {
@override
Widget build(BuildContext context) {
return AnnotatedRegion<SystemUiOverlayStyle>(
value: const SystemUiOverlayStyle(
statusBarColor: Colors.white,
statusBarIconBrightness: Brightness.dark,
statusBarBrightness: Brightness.light,
),
child: ChangeNotifierProvider(
create: (_) => CustomerRequestProvider(),
builder: (context, child) {
WidgetsBinding.instance.addPostFrameCallback((_) {
context.read<CustomerRequestProvider>().fetchCustomerRequests();
});
return Consumer<CustomerRequestProvider>(
builder: (context, provider, _) {
return SafeArea(
top: false,
child: PopScope(
canPop: true,
onPopInvokedWithResult: (didPop, result) {
if (!didPop) {
Get.back();
}
},
child: Scaffold(
backgroundColor: Colors.grey[100],
/// APPBAR
appBar: AppBar(
elevation: 0,
backgroundColor: Colors.white,
surfaceTintColor: Colors.white,
leadingWidth: 200,
leading: Row(
children: [
IconButton(
onPressed: () =>
Navigator.of(context).pop(),
icon: const Icon(Icons.arrow_back,
color: Colors.black),
),
const Expanded(
child: ReusableTextWidget(
text: "Help & Support",
color: Colors.black,
fontFamily: FontConstants.fontFamily,
fontSize: 16,
fontWeight: FontWeight.bold,
textAlign: TextAlign.start,
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
),
],
),
),
/// BODY
body: provider.isLoading
? ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: 5,
itemBuilder: (_, index) => Container(
margin:
const EdgeInsets.symmetric(vertical: 8),
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: Colors.white,
borderRadius:
BorderRadius.circular(14),
),
child: Shimmer.fromColors(
baseColor: Colors.grey.shade300,
highlightColor:
Colors.grey.shade100,
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Container(
width: double.infinity,
height: 18,
color: Colors.white,
),
const SizedBox(height: 10),
Container(
width: double.infinity,
height: 14,
color: Colors.white,
),
const SizedBox(height: 10),
Container(
width: 80,
height: 14,
color: Colors.white,
),
],
),
),
),
)
/// EMPTY STATE
: provider.requests.isEmpty
? Center(
child: Column(
mainAxisAlignment:
MainAxisAlignment.center,
children: [
SizedBox(
height: 160,
child: Lottie.asset(
'assets/lotties/help.json',
fit: BoxFit.contain,
),
),
const SizedBox(height: 16),
const ReusableTextWidget(
text: "No requests found",
color: Colors.black,
fontFamily:
FontConstants.fontFamily,
fontSize: 16,
fontWeight: FontWeight.w500,
textAlign: TextAlign.center,
),
],
),
)
/// LIST
: ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: provider.requests.length,
itemBuilder: (context, index) {
final request =
provider.requests[index];
return Container(
margin: const EdgeInsets.only(
bottom: 12),
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: Colors.white,
borderRadius:
BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black
.withOpacity(0.04),
blurRadius: 8,
offset:
const Offset(0, 3),
)
],
),
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
/// TOP ROW
Row(
mainAxisAlignment:
MainAxisAlignment
.spaceBetween,
children: [
Expanded(
child:
ReusableTextWidget(
text:
"Subject : ${request.subject}",
color: Colors.black,
fontFamily:
FontConstants
.fontFamily,
fontSize: 14,
fontWeight:
FontWeight.bold,
overflow:
TextOverflow
.ellipsis,
maxLines: 1,
),
),
const SizedBox(width: 8),
ReusableTextWidget(
text: request.created
.split('T')
.first,
color: Colors.grey,
fontFamily:
FontConstants
.fontFamily,
fontSize: 11,
fontWeight:
FontWeight.w500,
),
],
),
const SizedBox(height: 8),
/// REMARK
ReusableTextWidget(
text:
"Remarks : ${request.remarks}",
color: Colors.black87,
fontFamily:
FontConstants.fontFamily,
fontSize: 13,
fontWeight:
FontWeight.normal,
overflow:
TextOverflow.ellipsis,
maxLines: 2,
),
const SizedBox(height: 10),
/// STATUS BADGE
Row(
children: [
const ReusableTextWidget(
text: "Status : ",
color: Colors.black,
fontFamily:
FontConstants
.fontFamily,
fontSize: 13,
),
Container(
padding:
const EdgeInsets
.symmetric(
horizontal: 10,
vertical: 4),
decoration: BoxDecoration(
color: request
.status ==
1
? Colors.green
.withOpacity(
0.1)
: Colors.red
.withOpacity(
0.1),
borderRadius:
BorderRadius
.circular(20),
),
child: ReusableTextWidget(
text: request.status ==
1
? "Completed"
: "Pending",
color: request.status ==
1
? Colors.green
: Colors.red,
fontFamily:
FontConstants
.fontFamily,
fontSize: 11,
fontWeight:
FontWeight.bold,
),
),
],
),
],
),
);
},
),
/// FAB
floatingActionButton: FloatingActionButton(
elevation: 4,
onPressed: () async {
final result = await Get.to(
() => const CustomerRequestPage(),
);
if (result == true) {
context
.read<
CustomerRequestProvider>()
.fetchCustomerRequests();
}
},
backgroundColor:
ColorConstants.primaryColor,
child: const Icon(Icons.add,
color: Colors.white),
),
),
),
);
},
);
},
),
);
}
}

View File

@@ -0,0 +1,230 @@
import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:get/get.dart';
import '../../../constants/color_constants.dart';
import '../../../constants/font_constants.dart';
import '../../../domain/provider/profile/create_request.dart';
import '../../../modules/profile/customer_request.dart';
import '../../../widgets/text_widget.dart';
class CustomerRequestPage extends StatefulWidget {
const CustomerRequestPage({super.key});
@override
State<CustomerRequestPage> createState() => _CustomerRequestPageState();
}
class _CustomerRequestPageState extends State<CustomerRequestPage> {
final _formKey = GlobalKey<FormState>();
final TextEditingController subjectController = TextEditingController();
final TextEditingController remarkController = TextEditingController();
final CustomerRequestProvider provider = CustomerRequestProvider();
Future<void> _submitRequest() async {
if (_formKey.currentState!.validate()) {
final model = CustomerRequestModel(
referencedate: DateTime.now().toUtc().toIso8601String(),
referencetype: "",
customerid: 6164,
tenantid: 0,
locationid: 0,
subject: subjectController.text.trim(),
remarks: remarkController.text.trim(),
status: 0,
apptypeid: 98,
);
final success = await provider.sendRequest(
subjectController.text.trim(),
remarkController.text.trim(),
);
if (success) {
Fluttertoast.showToast(
msg: "Request submitted successfully!",
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.TOP,
backgroundColor: Colors.green,
textColor: Colors.white,
fontSize: 14,
);
Navigator.pop(context, true);
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text("Failed to submit request!")),
);
}
}
}
@override
void dispose() {
subjectController.dispose();
remarkController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return SafeArea(
top: false,
child: Scaffold(
backgroundColor: Colors.grey[100],
appBar: AppBar(
elevation: 0,
leadingWidth: 200,
leading: Row(
children: [
IconButton(
onPressed: () => Navigator.of(context).pop(),
icon: const Icon(Icons.arrow_back, color: Colors.black),
),
const Expanded(
child: ReusableTextWidget(
text: "Help & Support",
color: Colors.black,
fontFamily: FontConstants.fontFamily,
fontSize: 16,
fontWeight: FontWeight.bold,
textAlign: TextAlign.start,
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
),
],
),
backgroundColor: Colors.white,
),
body: Padding(
padding: const EdgeInsets.all(16),
child: Form(
key: _formKey,
child: SingleChildScrollView(
child: Container(
padding: const EdgeInsets.all(18),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
spreadRadius: 2,
offset: const Offset(0, 4),
)
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
/// Your Requested Text Widget Usage
ReusableTextWidget(
text: "Customer Support",
color: Colors.black.withOpacity(0.7),
fontFamily: FontConstants.fontFamily,
fontSize: 10,
fontWeight: FontWeight.bold,
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
const SizedBox(height: 20),
/// SUBJECT
const ReusableTextWidget(
text: "Subject",
color: Colors.black,
fontFamily: FontConstants.fontFamily,
fontSize: 14,
fontWeight: FontWeight.w600,
),
const SizedBox(height: 8),
TextFormField(
controller: subjectController,
decoration: InputDecoration(
hintText: "Enter subject",
filled: true,
fillColor: Colors.grey[100],
contentPadding: const EdgeInsets.symmetric(
horizontal: 14, vertical: 14),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
),
validator: (value) =>
value!.isEmpty ? "Please enter subject" : null,
),
const SizedBox(height: 20),
/// REMARK
const ReusableTextWidget(
text: "Remark",
color: Colors.black,
fontFamily: FontConstants.fontFamily,
fontSize: 14,
fontWeight: FontWeight.w600,
),
const SizedBox(height: 8),
TextFormField(
controller: remarkController,
maxLines: 4,
decoration: InputDecoration(
hintText: "Enter your remark",
filled: true,
fillColor: Colors.grey[100],
contentPadding: const EdgeInsets.symmetric(
horizontal: 14, vertical: 14),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
),
validator: (value) =>
value!.isEmpty ? "Please enter remark" : null,
),
const SizedBox(height: 30),
/// SUBMIT BUTTON
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _submitRequest,
style: ElevatedButton.styleFrom(
elevation: 3,
backgroundColor: ColorConstants.primaryColor,
padding: const EdgeInsets.symmetric(vertical: 15),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
),
),
child: const Text(
"Submit Request",
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
),
),
],
),
),
),
),
),
),
);
}
}

View File

@@ -0,0 +1,265 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../../constants/font_constants.dart';
import '../../widgets/text_widget.dart';
class NotificationSettingsView extends StatefulWidget {
const NotificationSettingsView({super.key});
@override
State<NotificationSettingsView> createState() =>
_NotificationSettingsViewState();
}
class _NotificationSettingsViewState extends State<NotificationSettingsView>
with SingleTickerProviderStateMixin {
bool notificationsEnabled = true;
bool soundEnabled = true;
bool vibrationEnabled = true;
static const Color primaryColor = Color(0xFF662582);
late AnimationController _controller;
late Animation<double> _fadeAnim;
late Animation<double> _scaleAnim;
@override
void initState() {
super.initState();
_loadSettings();
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 500),
);
_fadeAnim = CurvedAnimation(
parent: _controller,
curve: Curves.easeInOut,
);
_scaleAnim = Tween<double>(begin: 0.95, end: 1).animate(
CurvedAnimation(parent: _controller, curve: Curves.easeOutBack),
);
_controller.forward();
}
Future<void> _loadSettings() async {
final prefs = await SharedPreferences.getInstance();
setState(() {
notificationsEnabled = prefs.getBool('notificationsEnabled') ?? true;
soundEnabled = prefs.getBool('notificationSound') ?? true;
vibrationEnabled = prefs.getBool('notificationVibration') ?? true;
});
}
Future<void> _saveSetting(String key, bool value) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setBool(key, value);
}
Widget _animatedSettingCard({
required IconData icon,
required String title,
required String subtitle,
required bool value,
required ValueChanged<bool> onChanged,
}) {
return AnimatedScale(
duration: const Duration(milliseconds: 250),
scale: value ? 1 : 0.98,
child: Container(
margin: const EdgeInsets.only(bottom: 10),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.06),
blurRadius: 12,
offset: const Offset(0, 6),
),
],
),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
primaryColor,
primaryColor.withOpacity(0.7),
],
),
shape: BoxShape.circle,
),
child: Icon(icon, color: Colors.white, size: 20),
),
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ReusableTextWidget(
text: title,
fontSize: 14,
fontWeight: FontWeight.w800,
fontFamily: FontConstants.fontFamily,
color: Colors.black.withOpacity(0.65),
),
const SizedBox(height: 4),
ReusableTextWidget(
text: subtitle,
fontSize: 12,
fontWeight: FontWeight.w700,
fontFamily: FontConstants.fontFamily,
color: Colors.grey[500],
),
],
),
),
Switch.adaptive(
value: value,
activeColor: primaryColor,
onChanged: onChanged,
),
],
),
),
);
}
@override
Widget build(BuildContext context) {
return AnnotatedRegion<SystemUiOverlayStyle>(
// 🔹 Status bar like Account page
value: const SystemUiOverlayStyle(
statusBarColor: Colors.white, // white background
statusBarIconBrightness: Brightness.dark, // dark icons
statusBarBrightness: Brightness.light, // iOS
),
child: Scaffold(
backgroundColor: const Color(0xFFF6F6F6),
appBar: AppBar(
elevation: 0,
backgroundColor: Colors.white,
title: ReusableTextWidget(
text: "Notifications",
fontSize: 20,
fontWeight: FontWeight.w600,
fontFamily: FontConstants.fontFamily,
color: Colors.black,
),
iconTheme: const IconThemeData(color: Colors.black),
),
body: FadeTransition(
opacity: _fadeAnim,
child: ScaleTransition(
scale: _scaleAnim,
child: ListView(
padding: const EdgeInsets.all(12),
children: [
/// 🔔 Animated Header
Container(
padding: const EdgeInsets.all(20),
margin: const EdgeInsets.only(bottom: 24),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(18),
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
primaryColor,
primaryColor.withOpacity(0.85),
],
),
boxShadow: [
BoxShadow(
color: primaryColor.withOpacity(0.35),
blurRadius: 4,
offset: const Offset(1, 1),
),
],
),
child: Row(
children: [
const Icon(Icons.notifications_active,
color: Colors.white, size: 30),
const SizedBox(width: 16),
Expanded(
child: ReusableTextWidget(
text:
"Control alerts, audio and vibrations\nfor Nearle Daily notifications",
fontSize: 13,
fontWeight: FontWeight.w600,
fontFamily: FontConstants.fontFamily,
color: Colors.white,
),
),
],
),
),
/// 🔕 MASTER SWITCH
_animatedSettingCard(
icon: Icons.notifications_off_outlined,
title: "Enable Notifications",
subtitle: "Turn all notifications on or off",
value: notificationsEnabled,
onChanged: (val) async {
setState(() => notificationsEnabled = val);
await _saveSetting('notificationsEnabled', val);
},
),
/// 🔊 SUB SETTINGS
IgnorePointer(
ignoring: !notificationsEnabled,
child: AnimatedOpacity(
duration: const Duration(milliseconds: 300),
opacity: notificationsEnabled ? 1 : 0.4,
child: Column(
children: [
_animatedSettingCard(
icon: Icons.volume_up_outlined,
title: "Notification Sound",
subtitle: "Play sound for notifications",
value: soundEnabled,
onChanged: (val) async {
setState(() => soundEnabled = val);
await _saveSetting('notificationSound', val);
},
),
_animatedSettingCard(
icon: Icons.vibration,
title: "Vibration",
subtitle: "Vibrate on notification",
value: vibrationEnabled,
onChanged: (val) async {
setState(() => vibrationEnabled = val);
await _saveSetting(
'notificationVibration', val);
},
),
],
),
),
),
],
),
),
),
),
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
}

View File

@@ -0,0 +1,162 @@
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:http/http.dart' as http;
import '../../domain/provider/product/all_products.dart';
import '../../modules/product/product.dart';
class ProductsController extends GetxController {
final ProductsProvider provider = ProductsProvider();
var isConnected = true.obs;
var isLoading = false.obs;
var productResponse = Rxn<ProductResponse>();
var selectedIndex = 0.obs;
var searchQuery = ''.obs;
var isSearching = false.obs;
/// In-memory cache: key is "categoryId_tenantId"
final Map<String, ProductResponse> _cache = {};
@override
void onInit() {
super.onInit();
// Listen for connectivity changes
Connectivity().onConnectivityChanged.listen((status) {
isConnected.value = (status != ConnectivityResult.none);
});
}
Future<bool> hasInternet() async {
try {
final response = await http.get(Uri.parse('https://www.google.com'))
.timeout(const Duration(seconds: 5));
if (response.statusCode == 200) {
return true;
}
return false;
} catch (e) {
return false;
}
}
Future<void> fetchProducts(int categoryId, int tenantId, int locationId) async {
final cacheKey = '${categoryId}_${tenantId}_$locationId'; // ✅ Include locationId in cache key
// 1⃣ Use cache if available
if (_cache.containsKey(cacheKey)) {
productResponse.value = _cache[cacheKey];
return;
}
isLoading.value = true;
bool connected = await hasInternet();
if (!connected) {
isLoading.value = false;
isConnected = false.obs;
return; // Stop fetching
}
// 2⃣ Otherwise fetch from API
try {
isLoading.value = true;
final response = await provider.getProductsBySubCategory(
categoryId: categoryId,
tenantId: tenantId,
locationId: locationId, // ✅ Pass locationId to API
);
productResponse.value = response;
// 3⃣ Save in cache
_cache[cacheKey] = response!;
} finally {
isLoading.value = false;
}
}
/// Force refresh API and update cache
Future<void> refreshProducts(int categoryId, int tenantId, int locationId) async {
final cacheKey = '${categoryId}_${tenantId}_$locationId'; // ✅ Include locationId
try {
isLoading.value = true;
final response = await provider.getProductsBySubCategory(
categoryId: categoryId,
tenantId: tenantId,
locationId: locationId, // ✅ Pass locationId to API
);
productResponse.value = response;
// ✅ Update cache with new key
_cache[cacheKey] = response!;
} finally {
isLoading.value = false;
}
}
/// Returns products depending on search query and selected subcategory
List<Product> get filteredProducts {
// Check if nested data exists (main API)
final details = productResponse.value?.data?.details;
if (details != null && details.isNotEmpty) {
if (searchQuery.value.isEmpty) {
final selectedDetail = details[selectedIndex.value];
return selectedDetail.products ?? [];
}
List<Product> allProducts = [];
for (var detail in details) {
allProducts.addAll(detail.products ?? []);
}
return allProducts
.where((p) =>
(p.productname ?? '')
.toLowerCase()
.contains(searchQuery.value.toLowerCase()))
.toList();
}
// If flat details exist (variants API)
final variantDetails = productResponse.value?.details ?? [];
if (variantDetails.isNotEmpty) {
if (searchQuery.value.isEmpty) return variantDetails;
return variantDetails
.where((p) =>
(p.productname ?? '')
.toLowerCase()
.contains(searchQuery.value.toLowerCase()))
.toList();
}
return [];
}
// NEW: Dedicated method for subcategory-specific screen
List<Product> getProductsBySubcategory(String subCategoryName) {
final details = productResponse.value?.data?.details ?? [];
if (details.isEmpty) {
return [];
}
// Find matching subcategory (case-insensitive, trimmed for safety)
final matchingDetail = details.firstWhere(
(detail) =>
(detail.subcategoryname ?? '').trim().toLowerCase() ==
subCategoryName.trim().toLowerCase(),
orElse: () => Detail(), // fallback - make sure Detail() is valid in your modules
);
// Return the products of that subcategory (or empty if no match)
return matchingDetail.products ?? [];
}
}

View File

@@ -0,0 +1,379 @@
// import 'package:flutter/material.dart';
// import 'package:flutter_contacts/flutter_contacts.dart';
// import 'package:nearledaily/constants/color_constants.dart';
// import 'package:permission_handler/permission_handler.dart'
// as permission_handler;
// import 'package:url_launcher/url_launcher.dart';
// import 'package:flutter/services.dart';
//
// import '../../constants/font_constants.dart';
// import '../../widgets/text_widget.dart';
//
// class ShowContactsScreen extends StatefulWidget {
// const ShowContactsScreen({super.key});
//
// @override
// State<ShowContactsScreen> createState() => _ShowContactsScreenState();
// }
//
// class _ShowContactsScreenState extends State<ShowContactsScreen>
// with WidgetsBindingObserver {
// List<Contact> _contacts = [];
// bool _loading = false;
// bool _permissionDenied = false;
//
// /// 🔹 ADDED
// bool _showDisclaimer = true;
//
// @override
// void initState() {
// super.initState();
// WidgetsBinding.instance.addObserver(this);
// _loadContacts();
// }
//
// @override
// void dispose() {
// WidgetsBinding.instance.removeObserver(this);
// super.dispose();
// }
//
// Future<void> _loadContacts() async {
// setState(() {
// _loading = true;
// _permissionDenied = false;
// });
//
// final bool granted = await FlutterContacts.requestPermission();
//
// if (!granted) {
// setState(() {
// _loading = false;
// _permissionDenied = true;
// });
// return;
// }
//
// try {
// final List<Contact> contacts = await FlutterContacts.getContacts(
// withProperties: true,
// withPhoto: true,
// );
//
// setState(() {
// _contacts = contacts
// .where((c) => c.phones.isNotEmpty)
// .toList()
// ..sort((a, b) => a.displayName.compareTo(b.displayName));
// _loading = false;
// });
// } catch (e) {
// setState(() {
// _loading = false;
// });
// ScaffoldMessenger.of(context).showSnackBar(
// SnackBar(
// content: ReusableTextWidget(
// text: "Error loading contacts: $e",
// fontSize: 14,
// fontWeight: FontWeight.w400,
// fontFamily: FontConstants.fontFamily,
// color: Colors.white,
// ),
// ),
// );
// }
// }
//
// Widget _buildAvatar(Contact contact) {
// if (contact.photo != null && contact.photo!.isNotEmpty) {
// return CircleAvatar(
// backgroundImage: MemoryImage(contact.photo!),
// );
// } else {
// String initials = "";
// final names = contact.displayName.split(" ");
// if (names.isNotEmpty) initials += names[0][0];
// if (names.length > 1) initials += names[1][0];
// return CircleAvatar(
// backgroundColor: Colors.primaries[
// contact.displayName.hashCode % Colors.primaries.length],
// child: ReusableTextWidget(
// text: initials.toUpperCase(),
// fontSize: 16,
// fontWeight: FontWeight.bold,
// fontFamily: FontConstants.fontFamily,
// color: Colors.white,
// ),
// );
// }
// }
//
// Future<void> _openWhatsApp(Contact contact) async {
// if (contact.phones.isEmpty) return;
//
// String phoneNumber =
// contact.phones.first.number.replaceAll(RegExp(r'\D'), '');
// final Uri url = Uri.parse("https://wa.me/$phoneNumber");
//
// if (await canLaunchUrl(url)) {
// await launchUrl(url, mode: LaunchMode.externalApplication);
// } else {
// ScaffoldMessenger.of(context).showSnackBar(
// const SnackBar(
// content: ReusableTextWidget(
// text: "Could not open WhatsApp",
// fontSize: 14,
// fontWeight: FontWeight.w400,
// fontFamily: FontConstants.fontFamily,
// color: Colors.white,
// ),
// ),
// );
// }
// }
//
// Future<void> _inviteWhatsApp(Contact contact) async {
// if (contact.phones.isEmpty) return;
//
// String phoneNumber =
// contact.phones.first.number.replaceAll(RegExp(r'\D'), '');
//
// final String message = Uri.encodeComponent(
// "Hey! Join me on Nearle Daily 🚀");
//
// final Uri url = Uri.parse("https://wa.me/$phoneNumber?text=$message");
//
// if (await canLaunchUrl(url)) {
// await launchUrl(url, mode: LaunchMode.externalApplication);
// } else {
// ScaffoldMessenger.of(context).showSnackBar(
// const SnackBar(
// content: ReusableTextWidget(
// text: "Could not open WhatsApp",
// fontSize: 14,
// fontWeight: FontWeight.w400,
// fontFamily: FontConstants.fontFamily,
// color: Colors.white,
// ),
// ),
// );
// }
// }
//
// @override
// Widget build(BuildContext context) {
// return AnnotatedRegion<SystemUiOverlayStyle>(
// value: const SystemUiOverlayStyle(
// statusBarColor: Colors.white, // White background
// statusBarIconBrightness: Brightness.dark, // Dark icons
// statusBarBrightness: Brightness.light, // iOS
// ),
// child: Scaffold(
// backgroundColor: Colors.white,
// appBar: AppBar(
// backgroundColor: Colors.white,
// surfaceTintColor: Colors.transparent,
// scrolledUnderElevation: 0,
// titleSpacing: -5,
// animateColor: false,
// elevation: 0,
// title: ReusableTextWidget(
// text: "Refer a friend",
// fontSize: 20,
// fontWeight: FontWeight.w600,
// fontFamily: FontConstants.fontFamily,
// color: Colors.black,
// ),
// iconTheme: const IconThemeData(color: Colors.black),
// ),
// body: Padding(
// padding: const EdgeInsets.only(left: 12.0, right: 12, bottom: 12),
// child: Column(
// children: [
// /// 🔹 MODIFIED DISCLAIMER ONLY
// if (_showDisclaimer)
// Stack(
// children: [
// Padding(
// padding: const EdgeInsets.only(top: 12.0),
// child: Container(
// width: double.infinity,
// padding: const EdgeInsets.all(14),
// margin: const EdgeInsets.only(bottom: 16),
// decoration: BoxDecoration(
// color: ColorConstants.primaryColor.withOpacity(0.08),
// borderRadius: BorderRadius.circular(12),
// ),
// child: const ReusableTextWidget(
// text:
// "We access contacts only to let you share\nor recommend to friends. Nothing is stored.",
// fontSize: 13,
// fontWeight: FontWeight.w500,
// fontFamily: FontConstants.fontFamily,
// color: Colors.black87,
// ),
// ),
// ),
// Positioned(
// top: 6,
// right: -3,
// child: IconButton(
// icon: const Icon(Icons.close, size: 18),
// onPressed: () {
// setState(() {
// _showDisclaimer = false;
// });
// },
// ),
// ),
// ],
// ),
//
// if (_loading)
// const Expanded(
// child: Center(child: CircularProgressIndicator()),
// ),
//
// if (_permissionDenied)
// Expanded(
// child: Center(
// child: Container(
// margin: const EdgeInsets.symmetric(horizontal: 24),
// padding: const EdgeInsets.all(24),
// decoration: BoxDecoration(
// color: Colors.red.withOpacity(0.05),
// borderRadius: BorderRadius.circular(20),
// border: Border.all(
// color: Colors.red.withOpacity(0.2),
// ),
// ),
// child: Column(
// mainAxisSize: MainAxisSize.min,
// children: [
// Container(
// padding: const EdgeInsets.all(18),
// decoration: BoxDecoration(
// color: Colors.red.withOpacity(0.12),
// shape: BoxShape.circle,
// ),
// child: const Icon(
// Icons.info_outline,
// color: Colors.red,
// size: 48,
// ),
// ),
// const SizedBox(height: 20),
// const ReusableTextWidget(
// text: "Contacts Access Needed",
// fontSize: 18,
// fontWeight: FontWeight.w600,
// fontFamily: FontConstants.fontFamily,
// color: Colors.black,
// ),
// const SizedBox(height: 8),
// const ReusableTextWidget(
// text:
// "Allow contacts permission to view\nand invite your friends easily.",
// fontSize: 14,
// fontWeight: FontWeight.w400,
// fontFamily: FontConstants.fontFamily,
// color: Colors.black54,
// textAlign: TextAlign.center,
// ),
// const SizedBox(height: 24),
// SizedBox(
// width: double.infinity,
// child: ElevatedButton(
// onPressed: permission_handler.openAppSettings,
// style: ElevatedButton.styleFrom(
// backgroundColor: Colors.red,
// elevation: 0,
// padding: const EdgeInsets.symmetric(vertical: 14),
// shape: RoundedRectangleBorder(
// borderRadius: BorderRadius.circular(12),
// ),
// ),
// child: const ReusableTextWidget(
// text: "Open Settings",
// fontSize: 15,
// fontWeight: FontWeight.w600,
// fontFamily: FontConstants.fontFamily,
// color: Colors.white,
// ),
// ),
// ),
// ],
// ),
// ),
// ),
// ),
//
// if (_contacts.isNotEmpty && !_loading && !_permissionDenied)
// Expanded(
// child: RefreshIndicator(
// onRefresh: _loadContacts,
// child: ListView.builder(
// itemCount: _contacts.length,
// itemBuilder: (context, index) {
// final contact = _contacts[index];
// final phones =
// contact.phones.map((p) => p.number).toList();
// final subtitle = phones.length > 1
// ? phones.sublist(0, 2).join(", ")
// : phones.first;
//
// return ListTile(
// leading: _buildAvatar(contact),
// title: ReusableTextWidget(
// text: contact.displayName.isEmpty
// ? "No Name"
// : contact.displayName,
// fontSize: 14,
// fontWeight: FontWeight.w600,
// fontFamily: FontConstants.fontFamily,
// color: Colors.black,
// ),
// subtitle: ReusableTextWidget(
// text: subtitle,
// fontSize: 13,
// fontWeight: FontWeight.w400,
// fontFamily: FontConstants.fontFamily,
// color: Colors.grey,
// ),
// trailing: TextButton(
// onPressed: () => _inviteWhatsApp(contact),
// child: const ReusableTextWidget(
// text: "Invite",
// fontSize: 14,
// fontWeight: FontWeight.bold,
// fontFamily: FontConstants.fontFamily,
// color: Colors.green,
// ),
// ),
// onTap: () => _openWhatsApp(contact),
// );
// },
// ),
// ),
// ),
//
// if (_contacts.isEmpty && !_loading && !_permissionDenied)
// const Expanded(
// child: Center(
// child: ReusableTextWidget(
// text: "No contacts found with phone numbers",
// fontSize: 16,
// fontWeight: FontWeight.w400,
// fontFamily: FontConstants.fontFamily,
// color: Colors.grey,
// ),
// ),
// ),
// ],
// ),
// ),
// ),
// );
// }
// }

View File

@@ -0,0 +1,25 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:webview_flutter/webview_flutter.dart';
import '../../controllers/account_controller/faq_controller.dart';
class test extends GetView<FaqController> {
test({super.key});
final FaqController controller = Get.put(FaqController());
@override
Widget build(BuildContext context) {
return AnnotatedRegion<SystemUiOverlayStyle>(
value: const SystemUiOverlayStyle(
statusBarColor: Colors.white, // White background
statusBarIconBrightness: Brightness.dark, // Dark icons
statusBarBrightness: Brightness.light, // iOS
),
child: Scaffold(
),
);
}
}

View File

@@ -0,0 +1,152 @@
import 'package:flutter/material.dart';
import 'package:in_app_update/in_app_update.dart';
import 'package:lottie/lottie.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:nearledaily/constants/color_constants.dart';
import 'package:new_version_plus/new_version_plus.dart';
import 'package:url_launcher/url_launcher.dart';
class AppUpdateView extends StatefulWidget {
const AppUpdateView({super.key});
@override
State<AppUpdateView> createState() => _AppUpdateViewState();
}
class _AppUpdateViewState extends State<AppUpdateView> {
bool isUpdating = false;
String? errorMessage;
Future<void> _performUpdate() async {
setState(() {
isUpdating = true;
errorMessage = null;
});
try {
final newVersion = NewVersionPlus(androidId: "com.nearle.gear");
final status = await newVersion.getVersionStatus();
if (status == null) {
throw Exception("Could not check version status");
}
if (status.canUpdate) {
print("Launching Play Store for update...");
await newVersion.launchAppStore(status.appStoreLink);
// Note: App will close and open Play Store
} else {
throw Exception("No update available (should not happen)");
}
} catch (e) {
setState(() {
isUpdating = false;
errorMessage = "Failed to open Play Store: $e";
});
// Fallback: Force open Play Store link manually
try {
final Uri playStoreUrl = Uri.parse(
"https://play.google.com/store/apps/details?id=com.nearle.gear");
if (await canLaunchUrl(playStoreUrl)) {
await launchUrl(playStoreUrl);
}
} catch (_) {
setState(() {
errorMessage = "Please update app from Play Store manually";
});
}
}
}
@override
Widget build(BuildContext context) {
final size = MediaQuery.of(context).size;
return Scaffold(
backgroundColor: Colors.white,
body: SafeArea(
child: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// 🌟 Beautiful Lottie animation for update
Lottie.asset(
'assets/lotties/update.json',
height: size.height * 0.35,
repeat: true,
fit: BoxFit.contain,
),
const SizedBox(height: 32),
// 📝 Title
Text(
"New Update Available!",
style: GoogleFonts.lato(
fontSize: 22,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
const SizedBox(height: 12),
// 💬 Description
// Text(
// "Weve made improvements and fixed some bugs to make your experience even better. Please update to continue using the app.",
// textAlign: TextAlign.center,
// style: GoogleFonts.lato(
// fontSize: 15,
// color: Colors.grey[700],
// height: 1.5,
// ),
// ),
const SizedBox(height: 40),
// 🔘 Update Button
if (isUpdating)
const CircularProgressIndicator()
else
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: ColorConstants.primaryColor,
padding: const EdgeInsets.symmetric(
horizontal: 60,
vertical: 16,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
),
),
onPressed: _performUpdate,
child: Text(
"Update Now",
style: GoogleFonts.lato(
color: Colors.white,
fontWeight: FontWeight.w600,
fontSize: 16,
),
),
),
// // ⚠️ Error Message
// if (errorMessage != null) ...[
// const SizedBox(height: 20),
// Text(
// errorMessage!,
// style: GoogleFonts.lato(
// color: Colors.redAccent,
// fontSize: 14,
// ),
// textAlign: TextAlign.center,
// ),
// ],
],
),
),
),
),
);
}
}

View File

@@ -0,0 +1,591 @@
import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:geocoding/geocoding.dart';
import 'package:geolocator/geolocator.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:http/http.dart' as http;
import 'package:shared_preferences/shared_preferences.dart';
import '../../constants/color_constants.dart';
import '../../constants/font_constants.dart';
import '../../controllers/tenant_controller /tenant_list.dart';
import '../../widgets/text_widget.dart';
import '../home_view.dart';
class CustomerCreateView extends StatefulWidget {
final String mobileNumber;
const CustomerCreateView({super.key,required this.mobileNumber});
@override
State<CustomerCreateView> createState() => _CustomerCreateViewState();
}
class _CustomerCreateViewState extends State<CustomerCreateView> {
Map<String, dynamic>? selectedLocationData;
bool isFetching = false;
final TenantController tenantController = Get.put(TenantController());
final TextEditingController nameController = TextEditingController();
final TextEditingController landmarkController = TextEditingController();
Future<void> createCustomer(Map<String, dynamic> locationData) async {
try {
SharedPreferences prefs = await SharedPreferences.getInstance();
String? fcmToken = prefs.getString('fcmToken') ?? '';
String deviceId = prefs.getString('currentDeviceId') ?? '';
String deviceType = Platform.isAndroid ? "android" : "ios";
final url = Uri.parse('https://fiesta.nearle.app/live/api/v1/mob/customers/create');
final Map<String, dynamic> body = {
"configid": 2,
"firstname": nameController.text.trim(),
"applocationid": 1,
"profileimage": "",
"dialcode": "+91",
"contactno": widget.mobileNumber,
"devicetype": deviceType,
"deviceid": deviceId,
"customertoken": fcmToken,
"address": locationData["address"] ?? "",
"suburb": locationData["suburb"] ?? "",
"city": locationData["city"] ?? "",
"state": locationData["state"] ?? "",
"postcode": locationData["postcode"] ?? "",
"landmark": landmarkController.text.isEmpty ? "near" : landmarkController.text.trim(),
"doorno": locationData["doorno"] ?? "",
"latitude": locationData["latitude"] ?? "",
"longitude": locationData["longitude"] ?? "",
"tenantid": 630,
"email": "",
"primaryaddress": 1,
"gender": "Male",
"dob": "2025-06-30"
};
Fluttertoast.showToast(
msg: "Creating customer...",
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.TOP,
backgroundColor: Colors.black.withOpacity(0.8),
textColor: Colors.white,
fontSize: 15,
);
final response = await http.post(
url,
headers: {"Content-Type": "application/json"},
body: jsonEncode(body),
);
final data = jsonDecode(response.body);
final bool status = data['status'] ?? false;
final String message = data['message'] ?? 'Unknown response';
if (status) {
final details = data['details'];
if (details != null) {
// ✅ Save important details to SharedPreferences
final customerIdStr = details['customerid']?.toString() ?? '0';
await prefs.setInt('customerId', int.tryParse(customerIdStr) ?? 0);
await prefs.setString('customerFirstname', details['firstname'] ?? '');
await prefs.setString('customertoken', details['customertoken'] ?? '');
await prefs.setInt('deliverylocationid', details['deliverylocationid'] ?? 0);
await prefs.setInt('contactno', int.tryParse(details['contactno'] ?? '0') ?? 0);
await prefs.setString('customerAddress', details['address'] ?? '');
await prefs.setString('customerSuburb', details['suburb'] ?? '');
await prefs.setString('customerCity', details['city'] ?? '');
await prefs.setString('customerState', details['state'] ?? '');
await prefs.setString('customerLandmark', details['landmark'] ?? '');
await prefs.setString('customerDoorNo', details['doorno'] ?? '');
debugPrint("✅ Customer info saved to SharedPreferences.");
}
tenantController.loadTenants();
print(data);
// Get.put(TenantController());
Get.offAll(() => BottomNavigation());
// ✅ Use message from API
Fluttertoast.showToast(
msg: "Customer created successfully!",
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.TOP,
backgroundColor: Colors.green.withOpacity(0.8),
textColor: Colors.white,
fontSize: 15,
);
} else {
// ❌ Handle failure message from API
debugPrint("❌ API returned failure: $message");
Fluttertoast.showToast(
msg: "Customer already available",
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.TOP,
backgroundColor: Colors.black.withOpacity(0.8),
textColor: Colors.white,
fontSize: 15,
);
}
} catch (e, stacktrace) {
debugPrint(" Something went wrong");
debugPrint("Stacktrace: $stacktrace");
Fluttertoast.showToast(
msg: "Something went wrong",
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.TOP,
backgroundColor: Colors.black.withOpacity(0.8),
textColor: Colors.white,
fontSize: 15,
);
}
}
@override
Widget build(BuildContext context) {
final size = MediaQuery.of(context).size;
final width = size.width;
final height = size.height;
double scaleFont(double size) {
if (width > 800) return size * 1.5;
if (width > 600) return size * 1.3;
return size;
}
return SafeArea(
top: false,
child: Scaffold(
backgroundColor: Colors.grey.shade100,
appBar: AppBar(
backgroundColor: Colors.white,
elevation: 0,
centerTitle: true,
leadingWidth: 300,
leading: Row(
children: [
IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.black87),
onPressed: () => Navigator.pop(context),
),
ReusableTextWidget(
text: "Create Account",
color: ColorConstants.blackColor,
fontWeight: FontWeight.w700,
fontSize: scaleFont(17),
fontFamily: FontConstants.fontFamily,
)
],
),
),
body: SingleChildScrollView(
padding: EdgeInsets.symmetric(horizontal: width * 0.06, vertical: height * 0.02),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: EdgeInsets.only(bottom: height * 0.02),
child: ReusableTextWidget(
text: "Welcome 👋\nPlease enter your details below",
color: Colors.black87,
fontWeight: FontWeight.w600,
fontSize: scaleFont(13),
fontFamily: FontConstants.fontFamily,
),
),
_buildLabel("Full Name", scaleFont),
_buildTextField("Enter your name", Icons.person, width, controller: nameController),
SizedBox(height: height * 0.03),
_buildLabel("Location", scaleFont),
Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(10),
border: Border.all(color: Colors.black54, width: 0.40),
),
child: ListTile(
leading: Icon(Icons.location_on, color: ColorConstants.primaryColor),
title: ReusableTextWidget(
text: selectedLocationData == null
? "Use my current location"
: selectedLocationData!["address"],
color: selectedLocationData == null
? ColorConstants.primaryColor
: Colors.black,
fontWeight: FontWeight.w600,
fontSize: 12,
fontFamily: FontConstants.fontFamily,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
subtitle: selectedLocationData == null
? ReusableTextWidget(
text: "Fetching current location...",
color: Colors.grey,
fontWeight: FontWeight.w400,
fontSize: 9,
fontFamily: FontConstants.fontFamily,
)
: null,
trailing: isFetching
? const SizedBox(
width: 22,
height: 22,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.arrow_forward_ios_rounded,
color: Colors.grey, size: 18),
onTap: () async {
setState(() => isFetching = true);
final result = await Get.to(() => const MapPickerPage1());
if (result != null) {
setState(() {
selectedLocationData = result;
});
}
setState(() => isFetching = false);
},
),
),
SizedBox(height: height * 0.03),
_buildLabel("Door No / Landmark", scaleFont),
_buildTextField("Enter door no / landmark", Icons.home_filled, width,
controller: landmarkController),
SizedBox(height: height * 0.05),
],
),
),
bottomNavigationBar: Container(
padding: EdgeInsets.symmetric(horizontal: width * 0.06, vertical: height * 0.02),
decoration: BoxDecoration(
color: Colors.white,
boxShadow: [
BoxShadow(
color: Colors.grey.shade300,
blurRadius: 10,
offset: const Offset(0, -2),
)
],
),
child: SizedBox(
height: height * 0.065,
width: double.infinity,
child: ElevatedButton(
onPressed: selectedLocationData == null
? null
: () {
if (nameController.text.isEmpty) {
Get.snackbar("Error", "Please enter your name");
return;
}
createCustomer(selectedLocationData!);
},
style: ElevatedButton.styleFrom(
backgroundColor: ColorConstants.primaryColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
elevation: 2,
),
child: Text(
"Submit",
style: TextStyle(
fontFamily: FontConstants.fontFamily,
fontSize: scaleFont(17),
fontWeight: FontWeight.w700,
color: Colors.white,
),
),
),
),
),
),
);
}
Widget _buildLabel(String text, double Function(double) scaleFont) {
return Padding(
padding: const EdgeInsets.only(bottom: 6),
child: ReusableTextWidget(
text: text,
color: Colors.black87,
fontWeight: FontWeight.w700,
fontSize: scaleFont(15),
fontFamily: FontConstants.fontFamily,
),
);
}
Widget _buildTextField(String hint, IconData icon, double width,
{TextEditingController? controller}) {
return TextFormField(
controller: controller,
style: const TextStyle(fontSize: 14),
decoration: InputDecoration(
hintText: hint,
hintStyle: TextStyle(
fontFamily: FontConstants.fontFamily,
fontWeight: FontWeight.w400,
color: Colors.grey,
),
prefixIcon: Icon(icon, color: Colors.grey[700]),
filled: true,
fillColor: Colors.white,
contentPadding: const EdgeInsets.symmetric(vertical: 14, horizontal: 12),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide.none,
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide(color: Colors.black54, width: 0.40),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide(color: ColorConstants.primaryColor, width: 1.3),
),
),
);
}
}
class MapPickerPage1 extends StatefulWidget {
const MapPickerPage1({super.key});
@override
State<MapPickerPage1> createState() => _MapPickerPage1State();
}
class _MapPickerPage1State extends State<MapPickerPage1> {
GoogleMapController? mapController;
LatLng? selectedLatLng;
final TextEditingController searchController = TextEditingController();
@override
void initState() {
super.initState();
_getCurrentLocation();
}
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.resumed) {
// App came back from background, retry location
_getCurrentLocation();
}
}
Future<void> _getCurrentLocation() async {
try {
bool serviceEnabled = await Geolocator.isLocationServiceEnabled();
if (!serviceEnabled) {
// Get.snackbar("Location Disabled", "Please enable GPS to continue");
await Geolocator.openLocationSettings();
_getCurrentLocation();
serviceEnabled = await Geolocator.isLocationServiceEnabled();
if (!serviceEnabled) return _getCurrentLocation();
}
LocationPermission permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied) {
permission = await Geolocator.requestPermission();
if (permission == LocationPermission.denied) return;
}
if (permission == LocationPermission.deniedForever) {
Get.snackbar("Permission Denied Forever",
"Please enable location in app settings.");
await Geolocator.openAppSettings();
return;
}
Position position = await Geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.high,
);
setState(() {
selectedLatLng = LatLng(position.latitude, position.longitude);
});
mapController?.animateCamera(CameraUpdate.newLatLngZoom(selectedLatLng!, 16));
} catch (e) {
Get.snackbar("Error", "Failed to get location: $e");
}
}
Future<void> _goToSearchedPlace(double lat, double lng) async {
setState(() {
selectedLatLng = LatLng(lat, lng);
});
mapController?.animateCamera(
CameraUpdate.newCameraPosition(
CameraPosition(target: selectedLatLng!, zoom: 16),
),
);
}
@override
Widget build(BuildContext context) {
return SafeArea(
top: false,
child: Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
backgroundColor: Colors.white,
title: const Text("Pick Location"),
),
body: Stack(
children: [
selectedLatLng == null
? const Center(child: CircularProgressIndicator())
: GoogleMap(
initialCameraPosition:
CameraPosition(target: selectedLatLng!, zoom: 16),
onMapCreated: (controller) => mapController = controller,
onTap: (latLng) {
setState(() => selectedLatLng = latLng);
},
markers: selectedLatLng != null
? {
Marker(
markerId: const MarkerId("selected"),
position: selectedLatLng!,
draggable: true,
onDragEnd: (newPos) =>
setState(() => selectedLatLng = newPos),
),
}
: {},
),
Positioned(
top: 10,
left: 15,
right: 15,
child: Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.2),
blurRadius: 5,
),
],
),
child: GooglePlaceAutoCompleteTextField(
textEditingController: searchController,
googleAPIKey: "AIzaSyBhkGfnq27sN0wV5y_S-M2KojpFTk_by-Q",
inputDecoration: const InputDecoration(
hintText: "Search location...",
border: InputBorder.none,
contentPadding: EdgeInsets.all(12),
),
debounceTime: 400,
countries: ["in"],
isLatLngRequired: true,
getPlaceDetailWithLatLng: (Prediction prediction) {
double lat = double.parse(prediction.lat!);
double lng = double.parse(prediction.lng!);
_goToSearchedPlace(lat, lng);
},
itemClick: (Prediction prediction) {
searchController.text = prediction.description!;
FocusScope.of(context).unfocus();
},
),
),
),
Positioned(
bottom: 30,
left: 20,
right: 20,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: ColorConstants.primaryColor,
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12)),
),
onPressed: selectedLatLng == null
? null
: () async {
try {
List<Placemark> placemarks =
await placemarkFromCoordinates(
selectedLatLng!.latitude,
selectedLatLng!.longitude,
);
if (placemarks.isNotEmpty) {
final place = placemarks.first;
String address =
"${place.name}, ${place.locality}, ${place.administrativeArea}, ${place.postalCode}, ${place.country}";
Map<String, dynamic> selectedLocation = {
"address": address,
"suburb": place.subLocality ?? "",
"city": place.locality ?? "",
"state": place.administrativeArea ?? "",
"postcode": place.postalCode ?? "",
"doorno": place.name ?? "",
"landmark": "near",
"latitude": selectedLatLng!.latitude.toString(),
"longitude":
selectedLatLng!.longitude.toString(),
};
Navigator.of(Get.context!).pop(selectedLocation);
}
} catch (e) {
Get.snackbar("Error", "Failed to get location: $e");
}
},
child: const Text(
"Confirm Location",
style: TextStyle(color: Colors.white, fontSize: 16),
),
),
),
],
),
),
);
}
}

View File

@@ -0,0 +1,443 @@
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:webview_flutter/webview_flutter.dart';
import '../../controllers/authentication/auth_controller.dart';
import '../authentication/verification_view.dart';
class Login_view extends StatelessWidget {
Login_view({super.key});
final TextEditingController phoneController = TextEditingController();
// Fix: RxString mirrors the field so Obx rebuilds on every keystroke/clear
final RxString phoneValue = ''.obs;
final RxBool isAgreed = false.obs;
@override
Widget build(BuildContext context) {
Size screenSize = MediaQuery.of(context).size;
return Scaffold(
backgroundColor: Colors.white,
body: Stack(
children: [
// Top curved purple background
Positioned(
top: 0,
left: 0,
right: 0,
child: CustomPaint(
size: Size(screenSize.width, screenSize.height * 0.52),
painter: _TopCurvePainter(),
),
),
// Decorative circles
Positioned(
top: screenSize.height * 0.04,
right: -30,
child: Container(
width: 120,
height: 120,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.white.withOpacity(0.06),
),
),
),
Positioned(
top: screenSize.height * 0.10,
left: -20,
child: Container(
width: 80,
height: 80,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.white.withOpacity(0.06),
),
),
),
SafeArea(
child: SingleChildScrollView(
physics: const ClampingScrollPhysics(),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header area
Padding(
padding: EdgeInsets.symmetric(
horizontal: screenSize.width * 0.06,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: screenSize.height * 0.03),
// Logo / brand chip
SizedBox(height: screenSize.height * 0.02),
const Text(
"Groceries & More,\nDelivered in Minutes!",
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.w800,
fontSize: 26,
height: 1.25,
letterSpacing: -0.5,
),
),
SizedBox(height: screenSize.height * 0.008),
const Text(
"Sign in to enjoy lightning-fast delivery!",
style: TextStyle(
color: Colors.white70,
fontSize: 14,
height: 1.4,
),
),
],
),
),
// Image — right-aligned, overlapping curve
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Image.asset(
"assets/images/loginImage.png",
height: screenSize.height * 0.30,
fit: BoxFit.contain,
),
],
),
// White card form area
Container(
margin: EdgeInsets.symmetric(
horizontal: screenSize.width * 0.05),
padding: EdgeInsets.symmetric(
horizontal: screenSize.width * 0.05,
vertical: screenSize.height * 0.03,
),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(24),
boxShadow: [
BoxShadow(
color: const Color(0xFF662582).withOpacity(0.08),
blurRadius: 30,
offset: const Offset(0, 8),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
"Login or Signup",
style: TextStyle(
color: Color(0xFF1A1A2E),
fontWeight: FontWeight.w800,
fontSize: 20,
letterSpacing: -0.3,
),
),
const SizedBox(height: 4),
const Text(
"Enter your mobile number to continue",
style: TextStyle(
color: Color(0xFF9CA3AF),
fontSize: 13,
),
),
SizedBox(height: screenSize.height * 0.025),
// Phone input
TextFormField(
controller: phoneController,
maxLength: 10,
onChanged: (value) {
phoneValue.value = value; // Fix: keep Rx in sync
if (value.length == 10) {
FocusScope.of(context).unfocus();
}
},
decoration: InputDecoration(
labelText: "Mobile Number",
hintText: "Enter 10-digit number",
counterText: "",
labelStyle: const TextStyle(
color: Color(0xFF662582), fontSize: 13),
hintStyle:
const TextStyle(color: Color(0xFFD1D5DB)),
prefixIcon: Container(
width: screenSize.width * 0.2,
padding:
const EdgeInsets.symmetric(horizontal: 12),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
"+91",
style: TextStyle(
color: Color(0xFF662582),
fontWeight: FontWeight.w600,
fontSize: 15,
),
),
const SizedBox(width: 8),
Container(
width: 1,
height: 20,
color: const Color(0xFFE5E7EB),
),
],
),
),
// filled: true,
// fillColor: const Color(0xFFF9F5FF),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(
color: Color(0xFFE9D5FF), width: 1.2),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(
color: Color(0xFFE9D5FF), width: 1.2),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(
color: Color(0xFF662582), width: 1.8),
),
),
style: const TextStyle(
color: Color(0xFF1A1A2E),
fontWeight: FontWeight.w500,
fontSize: 16,
letterSpacing: 1,
),
cursorColor: const Color(0xFF662582),
keyboardType: TextInputType.phone,
),
SizedBox(height: screenSize.height * 0.02),
// Agree checkbox row
Obx(() => GestureDetector(
onTap: () => isAgreed.value = !isAgreed.value,
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
AnimatedContainer(
duration:
const Duration(milliseconds: 200),
width: 20,
height: 20,
decoration: BoxDecoration(
color: isAgreed.value
? const Color(0xFF662582)
: Colors.white,
borderRadius: BorderRadius.circular(5),
border: Border.all(
color: isAgreed.value
? const Color(0xFF662582)
: const Color(0xFFD1D5DB),
width: 1.5,
),
),
child: isAgreed.value
? const Icon(Icons.check,
size: 13, color: Colors.white)
: null,
),
const SizedBox(width: 10),
Expanded(
child: RichText(
text: TextSpan(
style: const TextStyle(
color: Color(0xFF6B7280),
fontSize: 12.5,
),
children: [
const TextSpan(
text: "I agree to the "),
TextSpan(
text: "Terms & Privacy Policy",
style: const TextStyle(
color: Color(0xFF662582),
fontWeight: FontWeight.w600,
),
recognizer:
TapGestureRecognizer()
..onTap = () {
Get.to(() => WebViewScreen(
url:
"https://nearle.in/privacy",
title:
"Terms & Privacy Policy",
));
},
),
],
),
),
),
],
),
)),
SizedBox(height: screenSize.height * 0.025),
// Continue button
SizedBox(
width: double.infinity,
height: 52,
child: Obx(() {
final authController =
Get.find<AuthController>();
final phone = phoneValue.value.trim(); // Fix: reactive read
bool isValidMobile(String phone) {
return RegExp(r'^[6-9]\d{9}$').hasMatch(phone);
}
final bool isPhoneValid = isValidMobile(phone);
final bool canProceed = isPhoneValid &&
isAgreed.value &&
!authController.isLoading.value;
return ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF662582),
foregroundColor: Colors.white,
disabledBackgroundColor:
const Color(0xFFD8B4FE),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
),
elevation: 0,
),
onPressed: canProceed
? () =>
authController.signIn(context, phone)
: null,
child: authController.isLoading.value
? const SizedBox(
width: 22,
height: 22,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor:
AlwaysStoppedAnimation<Color>(
Colors.white),
),
)
: const Row(
mainAxisAlignment:
MainAxisAlignment.center,
children: [
Text(
"Continue",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w700,
letterSpacing: 0.3,
),
),
SizedBox(width: 8),
Icon(Icons.arrow_forward_rounded,
size: 18),
],
),
);
}),
),
],
),
),
SizedBox(height: screenSize.height * 0.04),
],
),
),
),
],
),
);
}
}
// ── Custom painter for top curved purple background ──────────────────────────
class _TopCurvePainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..shader = const LinearGradient(
colors: [Color(0xFF8B2FC9), Color(0xFF662582)],
begin: Alignment.topRight,
end: Alignment.bottomLeft,
).createShader(Rect.fromLTWH(0, 0, size.width, size.height));
final path = Path();
path.lineTo(0, size.height * 0.85);
path.quadraticBezierTo(
size.width * 0.25,
size.height * 1.0,
size.width * 0.5,
size.height * 0.92,
);
path.quadraticBezierTo(
size.width * 0.75,
size.height * 0.84,
size.width,
size.height * 0.94,
);
path.lineTo(size.width, 0);
path.close();
canvas.drawPath(path, paint);
}
@override
bool shouldRepaint(_TopCurvePainter oldDelegate) => false;
}
// ── WebView screen (unchanged) ────────────────────────────────────────────────
class WebViewScreen extends StatefulWidget {
final String url;
final String title;
const WebViewScreen({
super.key,
required this.url,
required this.title,
});
@override
State<WebViewScreen> createState() => _WebViewScreenState();
}
class _WebViewScreenState extends State<WebViewScreen> {
late final WebViewController controller;
@override
void initState() {
super.initState();
controller = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..loadRequest(Uri.parse(widget.url));
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
backgroundColor: Colors.white,
foregroundColor: Colors.black,
),
body: WebViewWidget(controller: controller),
);
}
}

View File

@@ -0,0 +1,69 @@
import 'package:flutter/material.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:geolocator/geolocator.dart';
class MapView extends StatefulWidget {
const MapView({super.key});
@override
State<MapView> createState() => _MapViewState();
}
class _MapViewState extends State<MapView> {
GoogleMapController? _mapController;
LatLng? _currentLatLng;
@override
void initState() {
super.initState();
_getCurrentLocation();
}
Future<void> _getCurrentLocation() async {
bool serviceEnabled = await Geolocator.isLocationServiceEnabled();
if (!serviceEnabled) {
return Future.error('Location services are disabled.');
}
LocationPermission permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied) {
permission = await Geolocator.requestPermission();
if (permission == LocationPermission.denied) {
return Future.error('Location permission denied');
}
}
if (permission == LocationPermission.deniedForever) {
return Future.error('Location permission permanently denied');
}
Position position = await Geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.high);
setState(() {
_currentLatLng = LatLng(position.latitude, position.longitude);
});
_mapController?.animateCamera(CameraUpdate.newCameraPosition(
CameraPosition(target: _currentLatLng!, zoom: 15),
));
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("Select Location")),
body: _currentLatLng == null
? const Center(child: CircularProgressIndicator())
: GoogleMap(
initialCameraPosition:
CameraPosition(target: _currentLatLng!, zoom: 15),
myLocationEnabled: true,
myLocationButtonEnabled: true,
onMapCreated: (controller) {
_mapController = controller;
},
),
);
}
}

View File

@@ -0,0 +1,331 @@
import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:get/get.dart';
import 'package:otp_timer_button/otp_timer_button.dart';
import 'package:sms_autofill/sms_autofill.dart';
import '../../controllers/authentication/auth_controller.dart';
class VerificationUiPage extends StatefulWidget {
final String phoneNumber;
final bool isNewUser; // true if new user, false if existing
const VerificationUiPage({
super.key,
required this.phoneNumber,
required this.isNewUser,
});
@override
State<VerificationUiPage> createState() => _VerificationUiPageState();
}
class _VerificationUiPageState extends State<VerificationUiPage>
with CodeAutoFill {
String? otpCode;
final AuthController authController = Get.find<AuthController>(); // ✅ Reuses existing instance with isNewUser state
// final AuthController authController = Get.put(AuthController()); // ✅ Controller instance
final OtpTimerButtonController otpTimerController = OtpTimerButtonController();
@override
void initState() {
super.initState();
listenForCode();
}
@override
void codeUpdated() {
setState(() {
otpCode = code;
});
// Auto-verify when OTP is received
if (otpCode != null && otpCode!.length == 6) {
authController.validateOtp(otpCode!, context);
}
}
@override
void dispose() {
cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
Size screenSize = MediaQuery.of(context).size;
return Scaffold(
backgroundColor: const Color(0xFF662582),
body: Stack(
alignment: Alignment.bottomCenter,
children: [
/// Top Section
Container(
width: double.infinity,
padding: EdgeInsets.only(
top: screenSize.height * 0.07,
left: screenSize.width * 0.06,
right: screenSize.width * 0.06,
),
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [Color(0xFF662582), Color(0xFF8546A6)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
"Groceries, Essentials & More Delivered in Minutes!",
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 26,
height: 1.3,
),
),
SizedBox(height: screenSize.height * 0.02),
const Text(
"Sign in to enjoy lightning-fast delivery!",
style: TextStyle(
color: Colors.white70,
fontSize: 16,
),
),
Align(
alignment: Alignment.centerRight,
child: Image.asset(
"assets/images/loginImage.png",
height: screenSize.height * 0.35,
fit: BoxFit.contain,
),
),
],
),
),
/// Bottom OTP Section
SingleChildScrollView(
child: Container(
width: screenSize.width,
padding: EdgeInsets.all(screenSize.width * 0.07),
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
boxShadow: [
BoxShadow(
color: Colors.black12,
blurRadius: 10,
spreadRadius: 2,
offset: Offset(0, -3),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
/// Title
const Text(
"Verify with OTP",
style: TextStyle(
color: Colors.black87,
fontWeight: FontWeight.w700,
fontSize: 22,
),
),
SizedBox(height: screenSize.height * 0.01),
Text(
"6 digit OTP has been sent to your number",
style: TextStyle(
color: Colors.grey[600],
fontSize: 16,
),
),
SizedBox(height: screenSize.height * 0.02),
/// Number + Change
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
widget.phoneNumber,
style: const TextStyle(
color: Colors.black87,
fontWeight: FontWeight.bold,
fontSize: 18,
),
),
Row(
children: [
const Text(
"Not Yours?",
style: TextStyle(
color: Colors.black54,
fontWeight: FontWeight.w500,
fontSize: 15,
),
),
const SizedBox(width: 6),
InkWell(
onTap: () => Navigator.of(context).pop(),
child: const Text(
"Change",
style: TextStyle(
color: Color(0xFF662582),
fontWeight: FontWeight.bold,
fontSize: 15,
),
),
),
],
),
],
),
SizedBox(height: screenSize.height * 0.04),
/// OTP Input
Center(
child: PinFieldAutoFill(
codeLength: 6,
decoration: BoxLooseDecoration(
strokeColorBuilder:
FixedColorBuilder(Colors.grey.shade400),
bgColorBuilder:
FixedColorBuilder(Colors.grey.shade100),
gapSpace: 12,
radius: const Radius.circular(10),
textStyle: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
onCodeChanged: (code) {
otpCode = code;
if (code != null && code.length == 6) {
authController.validateOtp(otpCode!, context, widget.isNewUser);
}
},
),
),
SizedBox(height: screenSize.height * 0.04),
/// Resend OTP
Center(
child: Column(
children: [
Text(
"Didnt receive an OTP?",
style: TextStyle(
color: Colors.grey[600],
fontSize: 15,
),
),
OtpTimerButton(
controller: otpTimerController,
onPressed: () async {
await authController.receiveSmsOtp(); // ✅ Resend OTP
Fluttertoast.showToast(
msg: "A new OTP has been sent to your number",
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.TOP,
backgroundColor: Colors.green.withOpacity(0.8),
textColor: Colors.white,
fontSize: 15,
);
},
text: const Text(
"Resend Again",
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.bold,
color: Color(0xFF662582),
),
),
duration: 60,
buttonType: ButtonType.text_button,
),
],
),
),
SizedBox(height: screenSize.height * 0.04),
/// Verify Button
SizedBox(
width: double.infinity,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF662582),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
padding:
const EdgeInsets.symmetric(vertical: 14),
),
onPressed: () {
if (otpCode != null && otpCode!.length == 6) {
authController.validateOtp(otpCode!, context);
} else {
Fluttertoast.showToast(
msg: "Enter a valid OTP",
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.TOP,
backgroundColor: Colors.green.withOpacity(0.8),
textColor: Colors.white,
fontSize: 15,
);
}
},
child: const Text(
"Verify OTP",
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
),
),
SizedBox(height: screenSize.height * 0.03),
/// Terms
RichText(
textAlign: TextAlign.center,
text: TextSpan(
style: TextStyle(
color: Colors.grey[600],
fontSize: 12,
),
children: const [
TextSpan(text: "By continuing, you agree to the "),
TextSpan(
text: "Terms & Privacy Policy",
style: TextStyle(
color: Colors.blue,
fontWeight: FontWeight.bold,
),
),
],
),
),
SizedBox(height: screenSize.height * 0.02),
],
),
),
),
],
),
);
}
}

1870
lib/view/cart/cart_view.dart Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,282 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../constants/font_constants.dart';
import '../../controllers/cart_controller/cart.dart';
import '../../controllers/order_controller/create_order_controller.dart';
import '../../modules/orders/create_order.dart';
import '../../widgets/text_widget.dart';
import '../orders/order_succes.dart';
class OrderCountdownPage extends StatefulWidget {
final CreateOrder order;
final OrderController orderCtrl;
final CartController cartCtrl;
final String customerName;
const OrderCountdownPage({
super.key,
required this.order,
required this.orderCtrl,
required this.cartCtrl,
required this.customerName,
});
@override
State<OrderCountdownPage> createState() => _OrderCountdownPageState();
}
class _OrderCountdownPageState extends State<OrderCountdownPage>
with SingleTickerProviderStateMixin {
static const int _totalSeconds = 10;
int _remainingSeconds = _totalSeconds;
Timer? _timer;
late AnimationController _animController;
bool _cancelled = false;
@override
void initState() {
super.initState();
_animController = AnimationController(
vsync: this,
duration: const Duration(seconds: _totalSeconds),
)..forward();
_startTimer();
}
void _startTimer() {
_timer?.cancel();
_timer = null;
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
// ✅ Stop immediately if cancelled or unmounted
if (_cancelled || !mounted) {
timer.cancel();
_timer = null;
return;
}
// ✅ Check zero BEFORE setState
if (_remainingSeconds <= 0) {
timer.cancel();
_timer = null;
_placeOrder();
return;
}
setState(() {
_remainingSeconds--;
});
});
}
@override
void dispose() {
_cancelled = true; // ✅ prevent any late callbacks
_timer?.cancel();
_timer = null;
_animController.dispose();
super.dispose();
}
String get _formattedTime {
final minutes = _remainingSeconds ~/ 60;
final seconds = _remainingSeconds % 60;
return '${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
}
Future<void> _placeOrder() async {
if (_cancelled || !mounted) return;
await widget.orderCtrl.createOrder(CreateOrderRequest(orders: widget.order));
if (!mounted || _cancelled) return;
if (!widget.orderCtrl.isLoading.value) {
Get.offAll(() => OrderSuccessView());
widget.cartCtrl.clearCart();
await widget.cartCtrl.notifyAdmin(
title: 'Nearle Deals - New Order',
body: 'A new order has been placed successfully by ${widget.customerName}!',
);
}
}
void _cancelOrder() {
// ✅ Stop timer immediately before showing dialog
_timer?.cancel();
_timer = null;
_animController.stop();
showDialog(
context: context,
barrierDismissible: false,
builder: (dialogCtx) {
return AlertDialog(
title: const Text('Cancel Order?'),
content: const Text('Are you sure you want to cancel this order?'),
actions: [
// No — resume
TextButton(
onPressed: () {
Navigator.of(dialogCtx).pop();
if (!_cancelled) {
_startTimer();
_animController.forward(from: _animController.value);
}
},
child: const Text('No'),
),
// Yes — go back
TextButton(
onPressed: () {
_cancelled = true; // ✅ set first
_timer?.cancel();
_timer = null;
_animController.stop();
// ✅ close dialog then navigate
Navigator.of(dialogCtx).pop();
Navigator.of(context).pop(); // ✅ use Navigator, not Get.back()
},
child: const Text(
'Yes, Cancel',
style: TextStyle(color: Colors.red),
),
),
],
);
},
);
}
@override
Widget build(BuildContext context) {
final progress = _remainingSeconds / _totalSeconds;
return Scaffold(
backgroundColor: Colors.white,
body: SafeArea(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 32),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Spacer(),
// Icon
Container(
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: Colors.green.withOpacity(0.1),
shape: BoxShape.circle,
),
child: const Icon(
Icons.access_time_filled,
size: 64,
color: Colors.green,
),
),
const SizedBox(height: 32),
// Title
ReusableTextWidget(
text: 'Order Pending',
color: Colors.black.withOpacity(0.8),
fontFamily: FontConstants.fontFamily,
fontSize: 22,
fontWeight: FontWeight.bold,
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
const SizedBox(height: 12),
// Subtitle
ReusableTextWidget(
text: 'Your order will be placed automatically.\nYou can cancel within the time below.',
color: Colors.black.withOpacity(0.5),
fontFamily: FontConstants.fontFamily,
fontSize: 10,
fontWeight: FontWeight.normal,
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,
maxLines: 3,
),
const SizedBox(height: 48),
// Progress + Timer
Stack(
alignment: Alignment.center,
children: [
SizedBox(
width: 160,
height: 160,
child: AnimatedBuilder(
animation: _animController,
builder: (_, __) => CircularProgressIndicator(
value: progress,
strokeWidth: 10,
backgroundColor: Colors.grey.shade200,
valueColor: const AlwaysStoppedAnimation<Color>(Colors.green),
),
),
),
ReusableTextWidget(
text: _formattedTime,
color: Colors.black.withOpacity(0.8),
fontFamily: FontConstants.fontFamily,
fontSize: 28,
fontWeight: FontWeight.bold,
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
],
),
const Spacer(),
// Cancel Button
SizedBox(
width: double.infinity,
child: OutlinedButton(
onPressed: _cancelOrder,
style: OutlinedButton.styleFrom(
side: const BorderSide(color: Colors.red),
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: ReusableTextWidget(
text: 'Cancel Order',
color: Colors.red,
fontFamily: FontConstants.fontFamily,
fontSize: 10,
fontWeight: FontWeight.bold,
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
),
),
const SizedBox(height: 16),
],
),
),
),
);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,611 @@
import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:http/http.dart' as http;
import 'package:nearledaily/constants/color_constants.dart';
import '../../constants/font_constants.dart';
import '../../controllers/tenant_controller /tenant_list.dart';
import '../../widgets/text_widget.dart';
import '../product/tenant_products.dart';
// ─── Search result modules ─────────────────────────────────────────────────────
class _SearchResult {
final String tenantName;
final String productName;
final String subCatName;
const _SearchResult({
required this.tenantName,
required this.productName,
required this.subCatName,
});
factory _SearchResult.fromJson(Map<String, dynamic> json) => _SearchResult(
tenantName: json['tenantname'] ?? '',
productName: json['productname'] ?? '',
subCatName: json['subcatname'] ?? '',
);
}
// ─── Screen ──────────────────────────────────────────────────────────────────
class SearchScreen extends StatefulWidget {
const SearchScreen({Key? key}) : super(key: key);
@override
State<SearchScreen> createState() => _SearchScreenState();
}
class _SearchScreenState extends State<SearchScreen>
with SingleTickerProviderStateMixin {
final TextEditingController _searchController = TextEditingController();
final FocusNode _focusNode = FocusNode();
final TenantController tenantController = Get.find();
late AnimationController _animationController;
late Animation<double> _fadeAnimation;
late Animation<Offset> _slideAnimation;
Timer? _debounce;
bool _isSearching = false;
List<_SearchResult> _searchResults = [];
String _lastQuery = '';
static const String _searchBaseUrl =
'https://fiesta.nearle.app/live/api/v1/mob/tenants/searchbykeyword';
@override
void initState() {
super.initState();
_animationController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 400),
);
_fadeAnimation = CurvedAnimation(
parent: _animationController,
curve: Curves.easeOut,
);
_slideAnimation = Tween<Offset>(
begin: const Offset(0, 0.05),
end: Offset.zero,
).animate(CurvedAnimation(
parent: _animationController,
curve: Curves.easeOut,
));
_animationController.forward();
_focusNode.addListener(() => setState(() {}));
_searchController.addListener(_onSearchChanged);
}
@override
void dispose() {
_debounce?.cancel();
_animationController.dispose();
_searchController.removeListener(_onSearchChanged);
_searchController.dispose();
_focusNode.dispose();
super.dispose();
}
void _onSearchChanged() {
final query = _searchController.text.trim();
setState(() {});
if (query == _lastQuery) return;
_lastQuery = query;
_debounce?.cancel();
if (query.isEmpty) {
setState(() {
_searchResults = [];
_isSearching = false;
});
return;
}
_debounce = Timer(const Duration(milliseconds: 400), () {
_fetchSearchResults(query);
});
}
Future<void> _fetchSearchResults(String keyword) async {
if (!mounted) return;
setState(() => _isSearching = true);
try {
final uri = Uri.parse(
'$_searchBaseUrl?keyword=${Uri.encodeComponent(keyword)}');
final response =
await http.get(uri).timeout(const Duration(seconds: 10));
if (!mounted) return;
if (response.statusCode == 200) {
final data = jsonDecode(response.body) as Map<String, dynamic>;
if (data['status'] == true && data['details'] is List) {
final results = (data['details'] as List)
.map((e) => _SearchResult.fromJson(e as Map<String, dynamic>))
.toList();
setState(() {
_searchResults = results;
_isSearching = false;
});
return;
}
}
setState(() {
_searchResults = [];
_isSearching = false;
});
} catch (_) {
if (!mounted) return;
setState(() {
_searchResults = [];
_isSearching = false;
});
}
}
List<dynamic> get _filteredTenants {
final query = _searchController.text.trim();
if (query.isEmpty) return tenantController.searchtenants;
final matchedNames =
_searchResults.map((r) => r.tenantName.toLowerCase()).toSet();
return tenantController.searchtenants
.where(
(t) => matchedNames.contains((t.tenantname ?? '').toLowerCase()))
.toList();
}
Widget _imagePlaceholder() {
return Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [Colors.grey.shade100, Colors.grey.shade400],
),
),
child: Center(
child: Icon(Icons.store_outlined,
size: 48, color: Colors.grey.shade400),
),
);
}
void _navigateTo(dynamic item) {
HapticFeedback.lightImpact();
Get.to(
() => ProductsScreen(
tenantId: item.tenantid!,
locationId: item.locationid!,
categoryId: item.categoryid!,
tenantName: item.tenantname!,
locationname: item.locationname!,
tenantLocation: item.suburb!,
tenantImage: item.tenantimage!,
tenantloc: item.locationid!,
subCategoryName: "",
),
transition: Transition.cupertino,
duration: const Duration(milliseconds: 300),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFFF6F6F6),
body: Column(
children: [
// ── Search bar ────────────────────────────────────────────────
SafeArea(
bottom: false,
child: FadeTransition(
opacity: _fadeAnimation,
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 12),
child: Hero(
tag: 'search_bar',
child: Material(
color: Colors.transparent,
child: Container(
height: 52,
padding: const EdgeInsets.symmetric(horizontal: 12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: _focusNode.hasFocus
? ColorConstants.primaryColor
: Colors.grey.shade300,
width: _focusNode.hasFocus ? 2 : 1,
),
boxShadow: [
BoxShadow(
color: _focusNode.hasFocus
? ColorConstants.primaryColor.withOpacity(0.15)
: Colors.black.withOpacity(0.06),
blurRadius: _focusNode.hasFocus ? 12 : 8,
offset: const Offset(0, 4),
),
],
),
child: Row(
children: [
IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () => Navigator.pop(context),
),
const SizedBox(width: 6),
Expanded(
child: TextField(
controller: _searchController,
focusNode: _focusNode,
autofocus: true,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
decoration: InputDecoration(
hintText: 'Search stores or products...',
hintStyle: TextStyle(
color: Colors.grey.shade400,
fontWeight: FontWeight.normal,
),
border: InputBorder.none,
),
),
),
if (_isSearching)
SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
color: ColorConstants.primaryColor,
),
)
else if (_searchController.text.isNotEmpty)
IconButton(
icon: const Icon(Icons.clear, size: 20),
onPressed: () {
_searchController.clear();
setState(() {
_searchResults = [];
_lastQuery = '';
});
},
),
],
),
),
),
),
),
),
),
// ── List ──────────────────────────────────────────────────────
Expanded(
child: Obx(() {
if (tenantController.isLoading.value) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(
color: ColorConstants.primaryColor),
const SizedBox(height: 16),
Text(
'Loading stores...',
style: TextStyle(
color: Colors.grey.shade600, fontSize: 14),
),
],
),
);
}
final displayTenants = _filteredTenants;
final hasQuery = _searchController.text.trim().isNotEmpty;
if (displayTenants.isEmpty) {
return FadeTransition(
opacity: _fadeAnimation,
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.search_off_rounded,
size: 80, color: Colors.grey.shade300),
const SizedBox(height: 16),
Text(
hasQuery
? 'No matching stores found'
: 'No stores found',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: Colors.grey.shade600,
),
),
const SizedBox(height: 8),
Text(
'Try searching with different keywords',
style: TextStyle(
fontSize: 14, color: Colors.grey.shade500),
),
],
),
),
);
}
return ListView.builder(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 30),
physics: const BouncingScrollPhysics(),
itemCount: displayTenants.length,
itemBuilder: (context, index) {
final item = displayTenants[index];
return FadeTransition(
opacity: _fadeAnimation,
child: SlideTransition(
position: _slideAnimation,
child: _StoreListItem(
item: item,
index: index,
imagePlaceholder: _imagePlaceholder(),
onTap: () => _navigateTo(item),
),
),
);
},
);
}),
),
],
),
);
}
}
// ─── List item: banner image on top (outside card), info card below ──────────
class _StoreListItem extends StatelessWidget {
final dynamic item;
final int index;
final Widget imagePlaceholder;
final VoidCallback onTap;
const _StoreListItem({
required this.item,
required this.index,
required this.imagePlaceholder,
required this.onTap,
});
@override
Widget build(BuildContext context) {
final hasBanner = item.tenantbanner != null &&
(item.tenantbanner as String).isNotEmpty;
return Padding(
padding: const EdgeInsets.only(bottom: 20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// ── Banner — tappable, rounded top corners, NO card background ─
GestureDetector(
onTap: onTap,
child: Hero(
tag: 'store_${item.tenantid}_$index',
child: ClipRRect(
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(16),
topRight: Radius.circular(16),
),
child: Container(
height: 180,
width: double.infinity,
color: Colors.grey.shade200,
child: hasBanner
? Image.network(
item.tenantbanner as String,
fit: BoxFit.cover,
loadingBuilder: (ctx, child, progress) =>
progress == null ? child : _ShimmerLoading(),
errorBuilder: (ctx, _, __) => imagePlaceholder,
)
: imagePlaceholder,
),
),
),
),
// ── Info card — flush below the image ───────────────────────
_StoreInfoCard(item: item, onTap: onTap),
],
),
);
}
}
// ─── Info card: name + location only ─────────────────────────────────────────
class _StoreInfoCard extends StatefulWidget {
final dynamic item;
final VoidCallback onTap;
const _StoreInfoCard({required this.item, required this.onTap});
@override
State<_StoreInfoCard> createState() => _StoreInfoCardState();
}
class _StoreInfoCardState extends State<_StoreInfoCard>
with SingleTickerProviderStateMixin {
late AnimationController _scaleController;
late Animation<double> _scaleAnimation;
bool _isPressed = false;
@override
void initState() {
super.initState();
_scaleController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 150),
);
_scaleAnimation = Tween<double>(begin: 1.0, end: 0.98).animate(
CurvedAnimation(parent: _scaleController, curve: Curves.easeInOut),
);
}
@override
void dispose() {
_scaleController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTapDown: (_) {
setState(() => _isPressed = true);
_scaleController.forward();
},
onTapUp: (_) {
setState(() => _isPressed = false);
_scaleController.reverse();
widget.onTap();
},
onTapCancel: () {
setState(() => _isPressed = false);
_scaleController.reverse();
},
child: ScaleTransition(
scale: _scaleAnimation,
child: AnimatedContainer(
duration: const Duration(milliseconds: 150),
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(16),
bottomRight: Radius.circular(16),
),
boxShadow: [
BoxShadow(
color: _isPressed
? Colors.black.withOpacity(0.04)
: Colors.black.withOpacity(0.08),
blurRadius: _isPressed ? 3 : 6,
offset: Offset(0, _isPressed ? 1 : 3),
),
],
),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Store name
ReusableTextWidget(
text: widget.item.tenantname ?? '',
fontFamily: FontConstants.fontFamily,
fontSize: 16,
fontWeight: FontWeight.bold,
),
const SizedBox(height: 4),
// Location
Row(
children: [
Icon(Icons.location_on_outlined,
size: 13, color: Colors.grey.shade500),
const SizedBox(width: 3),
Expanded(
child: ReusableTextWidget(
text: widget.item.locationname ?? '',
fontSize: 12,
color: Colors.black54,
),
),
],
),
],
),
),
Icon(Icons.arrow_forward_ios_rounded,
size: 14, color: Colors.grey.shade400),
],
),
),
),
);
}
}
// ─── Shimmer loading ──────────────────────────────────────────────────────────
class _ShimmerLoading extends StatefulWidget {
@override
State<_ShimmerLoading> createState() => _ShimmerLoadingState();
}
class _ShimmerLoadingState extends State<_ShimmerLoading>
with SingleTickerProviderStateMixin {
late AnimationController _ctrl;
@override
void initState() {
super.initState();
_ctrl = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 1500),
)..repeat();
}
@override
void dispose() {
_ctrl.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _ctrl,
builder: (_, __) => Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Colors.grey.shade200,
Colors.grey.shade100,
Colors.grey.shade200,
],
stops: [
(_ctrl.value - 0.3).clamp(0.0, 1.0),
_ctrl.value.clamp(0.0, 1.0),
(_ctrl.value + 0.3).clamp(0.0, 1.0),
],
),
),
),
);
}
}

View File

@@ -0,0 +1,596 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:url_launcher/url_launcher.dart';
import 'package:nearledaily/constants/color_constants.dart';
import '../../constants/font_constants.dart';
import '../../widgets/text_widget.dart';
// ─────────────────────────────────────────────
// MODEL
// ─────────────────────────────────────────────
class TenantDetails {
final int tenantid;
final String tenantname;
final String tenanttype; // "D" = delivery-only
final String registrationno;
final String companyname;
final String primaryemail;
final String primarycontact;
final String address;
final String city;
final String state;
final String postcode;
final String latitude;
final String longitude;
final String status; // "Active" / else
const TenantDetails({
required this.tenantid,
required this.tenantname,
required this.tenanttype,
required this.registrationno,
required this.companyname,
required this.primaryemail,
required this.primarycontact,
required this.address,
required this.city,
required this.state,
required this.postcode,
required this.latitude,
required this.longitude,
required this.status,
});
factory TenantDetails.fromJson(Map<String, dynamic> j) => TenantDetails(
tenantid: j['tenantid'] ?? 0,
tenantname: j['tenantname'] ?? '',
tenanttype: j['tenanttype'] ?? '',
registrationno: j['registrationno'] ?? '',
companyname: j['companyname'] ?? '',
primaryemail: j['primaryemail'] ?? '',
primarycontact: j['primarycontact'] ?? '',
address: j['address'] ?? '',
city: j['city'] ?? '',
state: j['state'] ?? '',
postcode: j['postcode'] ?? '',
latitude: j['latitude'] ?? '',
longitude: j['longitude'] ?? '',
status: j['status'] ?? '',
);
bool get isDeliveryOnly => tenanttype == 'D';
bool get isActive => status == 'Active';
}
// ─────────────────────────────────────────────
// API SERVICE
// ─────────────────────────────────────────────
class TenantApiService {
static Future<TenantDetails> fetch(int tenantId) async {
final res = await http.get(Uri.parse(
'https://fiesta.nearle.app/live/api/v1/mob/tenants/gettenantinfo/?tenantid=$tenantId'));
if (res.statusCode == 200) {
final body = jsonDecode(res.body);
if (body['status'] == true) {
return TenantDetails.fromJson(body['details']);
}
throw Exception(body['message']);
}
throw Exception('HTTP ${res.statusCode}');
}
}
// ─────────────────────────────────────────────
// SCREEN
// ─────────────────────────────────────────────
class StoreOverviewScreen extends StatefulWidget {
final int tenantId;
const StoreOverviewScreen({super.key, this.tenantId = 1091});
@override
State<StoreOverviewScreen> createState() => _StoreOverviewScreenState();
}
class _StoreOverviewScreenState extends State<StoreOverviewScreen> {
late Future<TenantDetails> _future;
@override
void initState() {
super.initState();
_future = TenantApiService.fetch(widget.tenantId);
}
// ── Dialer ───────────────────────────────────
Future<void> _launchDialer(String phone) async {
final uri = Uri(scheme: 'tel', path: phone);
if (await canLaunchUrl(uri)) {
await launchUrl(uri);
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Could not open dialer')),
);
}
}
// ── Maps ─────────────────────────────────────
Future<void> _openMap(String lat, String lng, String label) async {
final encoded = Uri.encodeComponent(label);
final uri = Uri.parse(
'https://www.google.com/maps/search/?api=1&query=$lat,$lng($encoded)',
);
if (await canLaunchUrl(uri)) {
await launchUrl(uri, mode: LaunchMode.externalApplication);
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Could not open maps')),
);
}
}
// ── Bad-experience bottom sheet ──────────────────
void _showBadExperienceSheet(TenantDetails tenant) {
final reasons = [
'Wrong items delivered',
'Poor food quality',
'Late delivery',
'Rude behaviour',
'Other',
];
String? selected;
showModalBottomSheet(
backgroundColor: Colors.white,
context: context,
isScrollControlled: true,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
builder: (_) => StatefulBuilder(
builder: (ctx, setSheet) => Padding(
padding: EdgeInsets.only(
left: 20,
right: 20,
top: 20,
bottom: MediaQuery.of(ctx).viewInsets.bottom + 24,
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Handle bar
Center(
child: Container(
width: 40,
height: 4,
decoration: BoxDecoration(
color: Colors.grey.shade300,
borderRadius: BorderRadius.circular(2),
),
),
),
const SizedBox(height: 16),
ReusableTextWidget(
text: 'What went wrong?',
color: Colors.black87,
fontFamily: FontConstants.fontFamily,
fontSize: 17,
fontWeight: FontWeight.bold,
),
const SizedBox(height: 4),
ReusableTextWidget(
text: 'Tell us about your experience at ${tenant.tenantname}',
color: Colors.grey,
fontFamily: FontConstants.fontFamily,
fontSize: 12,
fontWeight: FontWeight.w500,
),
const SizedBox(height: 16),
// Reason chips
Wrap(
spacing: 8,
runSpacing: 8,
children: reasons.map((r) {
final picked = selected == r;
return GestureDetector(
onTap: () => setSheet(() => selected = r),
child: AnimatedContainer(
duration: const Duration(milliseconds: 180),
padding: const EdgeInsets.symmetric(
horizontal: 14, vertical: 8),
decoration: BoxDecoration(
color: picked
? const Color(0xFF6A1B9A)
: Colors.grey.shade100,
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: picked
? const Color(0xFF6A1B9A)
: Colors.grey.shade300,
),
),
child: ReusableTextWidget(
text: r,
color: picked ? Colors.white : Colors.black87,
fontFamily: FontConstants.fontFamily,
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
);
}).toList(),
),
const SizedBox(height: 20),
// Hide store option
const SizedBox(height: 14),
// Submit button
SizedBox(
width: double.infinity,
height: 48,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF6A1B9A),
disabledBackgroundColor: Colors.grey.shade200,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(13),
),
),
onPressed: selected == null
? null
: () {
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Feedback submitted: $selected'),
behavior: SnackBarBehavior.floating,
),
);
},
child: ReusableTextWidget(
text: 'Submit Feedback',
color: selected == null ? Colors.grey : Colors.white,
fontFamily: FontConstants.fontFamily,
fontSize: 15,
fontWeight: FontWeight.w600,
),
),
),
],
),
),
),
);
}
// ─────────────────────────────────────────────
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Color(0xFFF9F9F9), Color(0xFFF1F1F1)],
),
),
child: SafeArea(
child: FutureBuilder<TenantDetails>(
future: _future,
builder: (context, snap) {
if (snap.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
if (snap.hasError) {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.error_outline,
color: Colors.red, size: 48),
const SizedBox(height: 12),
Text('Failed to load\n${snap.error}',
textAlign: TextAlign.center),
const SizedBox(height: 12),
ElevatedButton(
onPressed: () => setState(
() => _future = TenantApiService.fetch(widget.tenantId)),
child: const Text('Retry'),
),
],
),
);
}
final t = snap.data!;
return Column(
children: [
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.all(12),
child: Column(
children: [
_topBar(),
const SizedBox(height: 16),
_storeCard(t),
const SizedBox(height: 12),
_badExperienceCard(t),
const SizedBox(height: 12),
_legalCard(t),
],
),
),
),
_bottomButton(),
],
);
},
),
),
),
);
}
// ── Top bar ──────────────────────────────────
Widget _topBar() => Row(
children: [
CircleAvatar(
backgroundColor: Colors.white,
child: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () => Navigator.pop(context),
),
),
const Spacer(),
],
);
// ── Store card ───────────────────────────────
Widget _storeCard(TenantDetails t) {
return Container(
padding: const EdgeInsets.all(16),
decoration: _card(),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// tenantname
ReusableTextWidget(
text: t.tenantname,
color: Colors.black.withOpacity(0.75),
fontFamily: FontConstants.fontFamily,
fontSize: 23,
fontWeight: FontWeight.bold,
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
const SizedBox(height: 8),
// address
ReusableTextWidget(
text: t.address,
color: Colors.black87,
fontFamily: FontConstants.fontFamily,
fontSize: 11,
fontWeight: FontWeight.w600,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 12),
// Call & Directions
Row(
children: [
GestureDetector(
onTap: () => _launchDialer(t.primarycontact),
child: _circleIcon(Icons.call),
),
const SizedBox(width: 12),
GestureDetector(
onTap: () => _openMap(t.latitude, t.longitude, t.tenantname),
child: _circleIcon(Icons.near_me_outlined),
),
],
),
const Divider(height: 24, thickness: 0.5),
// status → Open / Closed
_infoRow(
icon: Icons.access_time,
title: t.isActive ? 'Open now' : 'Currently Closed',
titleColor: t.isActive ? Colors.green : Colors.red,
),
// tenanttype == "D" → delivery-only row
if (t.isDeliveryOnly) ...[
const Divider(thickness: 0.5),
_infoRow(
icon: Icons.store_mall_directory_outlined,
title: 'This is a delivery-only kitchen',
subtitle:
'There are multiple brands delivering from this kitchen',
),
],
const Divider(thickness: 0.5),
// city + state + postcode
_infoRow(
icon: Icons.location_city_outlined,
title: '${t.city}, ${t.state} ${t.postcode}',
),
],
),
);
}
// ── Bad experience card ──────────────────────
Widget _badExperienceCard(TenantDetails t) => Container(
decoration: _card(),
child: ListTile(
leading: Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: Colors.red.shade50,
shape: BoxShape.circle,
),
child: Icon(Icons.sentiment_dissatisfied_outlined,
color: Colors.red.shade400, size: 20),
),
title: ReusableTextWidget(
text: 'Had a bad experience here?',
color: Colors.black87,
fontFamily: FontConstants.fontFamily,
fontSize: 13,
fontWeight: FontWeight.w600,
),
subtitle: ReusableTextWidget(
text: 'Report an issue or hide this store',
color: Colors.black54,
fontFamily: FontConstants.fontFamily,
fontSize: 12,
fontWeight: FontWeight.w400,
),
trailing: const Icon(Icons.chevron_right, color: Colors.black87),
onTap: () => _showBadExperienceSheet(t),
),
);
// ── Legal card — only real non-empty API fields ──
Widget _legalCard(TenantDetails t) => Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: _card(),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_labelText('Legal Name', t.companyname),
const SizedBox(height: 12),
_labelText('GST Number', t.registrationno),
const SizedBox(height: 12),
_labelText('Contact', t.primarycontact),
const SizedBox(height: 12),
_labelText('Email', t.primaryemail),
],
),
);
// ── Bottom button ────────────────────────────
Widget _bottomButton() => Container(
padding: const EdgeInsets.fromLTRB(16, 10, 16, 13),
color: Colors.white,
child: SizedBox(
width: double.infinity,
height: 48,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF6A1B9A),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(13)),
),
onPressed: () => Navigator.pop(context),
child: ReusableTextWidget(
text: 'Go back to menu',
color: Colors.white,
fontFamily: FontConstants.fontFamily,
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
);
// ── Helpers ──────────────────────────────────
Widget _circleIcon(IconData icon) => Container(
width: 40,
height: 40,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.white,
border: Border.all(color: Colors.grey.shade200),
),
child: Center(
child: Icon(icon, size: 22, color: ColorConstants.primaryColor),
),
);
Widget _infoRow({
required IconData icon,
required String title,
String? subtitle,
Color? titleColor,
}) =>
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(icon, size: 20, color: Colors.black87),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ReusableTextWidget(
text: title,
color: titleColor ?? Colors.black87,
fontFamily: FontConstants.fontFamily,
fontSize: 12,
fontWeight: FontWeight.w600,
),
if (subtitle != null)
ReusableTextWidget(
text: subtitle,
color: Colors.grey,
fontFamily: FontConstants.fontFamily,
fontSize: 11,
fontWeight: FontWeight.w600,
maxLines: 2,
),
],
),
),
const Icon(Icons.chevron_right, color: Colors.black87),
],
);
Widget _labelText(String label, String value) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ReusableTextWidget(
text: label,
color: Colors.grey,
fontFamily: FontConstants.fontFamily,
fontSize: 11,
fontWeight: FontWeight.w600,
),
const SizedBox(height: 4),
ReusableTextWidget(
text: value,
color: Colors.black87,
fontFamily: FontConstants.fontFamily,
fontSize: 11,
fontWeight: FontWeight.w600,
),
],
);
BoxDecoration _card() => BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.04),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
);
}

443
lib/view/home_view.dart Normal file
View File

@@ -0,0 +1,443 @@
import 'dart:ui';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:http/http.dart' as http;
import 'package:lottie/lottie.dart';
import 'package:nearledaily/view/qr_scaner/qr_scaner.dart';
import '../constants/font_constants.dart';
import '../controllers/cart_controller/cart.dart';
import '../widgets/text_widget.dart';
import 'account/account_view.dart';
import 'cart/cart_view.dart';
import 'dashboard_view/dashboard_view.dart';
import 'orders/orders_by_tenant.dart';
// ─── Colors ───────────────────────────────────────────────────────────────────
const Color _kPrimary = Color(0xFFDE9BFB);
const Color _kActive = Colors.white;
const Color _kInactive = Color(0xFFCBA8E4);
// ─── Screens ──────────────────────────────────────────────────────────────────
final List<Widget> _screens = [
DashboardPage(),
const OrdersByStoreScreen(showBackArrow: false),
QrScannerPage(),
CartPage(),
AccountPage(),
];
// ─── Controller ───────────────────────────────────────────────────────────────
class BottomNavController extends GetxController {
var isRetrying = false.obs;
var currentIndex = 0.obs;
var isConnected = true.obs;
@override
void onInit() {
super.onInit();
checkConnection();
Connectivity().onConnectivityChanged.listen((status) async {
isConnected.value = status == ConnectivityResult.none
? false
: await hasInternet();
});
}
Future<void> checkConnection() async {
final r = await Connectivity().checkConnectivity();
isConnected.value =
r == ConnectivityResult.none ? false : await hasInternet();
}
Future<bool> hasInternet() async {
try {
final res = await http
.get(Uri.parse('https://www.google.com'))
.timeout(const Duration(seconds: 5));
return res.statusCode == 200;
} catch (_) {
return false;
}
}
}
// ─── Root Widget ──────────────────────────────────────────────────────────────
class BottomNavigation extends StatelessWidget {
final BottomNavController controller = Get.put(BottomNavController());
final CartController cartController = Get.put(CartController());
BottomNavigation({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Obx(() {
if (!controller.isConnected.value) {
return Scaffold(
backgroundColor: const Color(0xFFF3E8FF),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Lottie.asset('assets/lotties/no_internet.json',
width: 200, height: 200, fit: BoxFit.contain),
const SizedBox(height: 16),
ReusableTextWidget(
text: 'No Internet Connection',
color: Colors.grey,
fontFamily: FontConstants.fontFamily,
fontSize: 18,
fontWeight: FontWeight.bold,
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
const SizedBox(height: 15),
Obx(() => ElevatedButton(
onPressed: controller.isRetrying.value
? null
: () async {
controller.isRetrying.value = true;
controller.isConnected.value =
await controller.hasInternet();
await Future.delayed(
const Duration(milliseconds: 800));
controller.isRetrying.value = false;
},
style: ElevatedButton.styleFrom(
backgroundColor: _kPrimary,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12)),
padding: const EdgeInsets.symmetric(
horizontal: 30, vertical: 12),
),
child: controller.isRetrying.value
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(
color: Colors.white, strokeWidth: 2))
: const Text('Retry',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold)),
)),
],
),
),
);
}
return Scaffold(
backgroundColor: Colors.white,
extendBody: true,
bottomNavigationBar: Obx(
() => _BottomNavBar(
currentIndex: controller.currentIndex.value,
cartController: cartController,
onTap: (i) => controller.currentIndex.value = i,
),
),
body: Obx(
() => _screens[controller.currentIndex.value],
),
);
});
}
}
// ─── Bottom Nav Bar (matches image exactly) ───────────────────────────────────
class _BottomNavBar extends StatelessWidget {
final int currentIndex;
final CartController cartController;
final ValueChanged<int> onTap;
const _BottomNavBar({
required this.currentIndex,
required this.cartController,
required this.onTap,
});
@override
Widget build(BuildContext context) {
final double bottomPad = MediaQuery.of(context).padding.bottom;
return Container(
decoration: BoxDecoration(
gradient: const LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Color(0xFF662582),
Color(0xFF662582),
Color(0xFF662582),
],
stops: [0.0, 0.5, 1.0],
),
// borderRadius: const BorderRadius.vertical(
// top: Radius.circular(32),
// ),
),
padding: EdgeInsets.fromLTRB(12, 12, 12, bottomPad + 10),
child: _GlassPill(
currentIndex: currentIndex,
cartController: cartController,
onTap: onTap,
),
);
}
}
// ─── Glass Pill ───────────────────────────────────────────────────────────────
class _GlassPill extends StatelessWidget {
final int currentIndex;
final CartController cartController;
final ValueChanged<int> onTap;
const _GlassPill({
required this.currentIndex,
required this.cartController,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return ClipRRect(
borderRadius: BorderRadius.circular(60),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 20, sigmaY: 20),
child: Container(
height: 72,
decoration: BoxDecoration(
// Semi-transparent white glass overlay — matches the frosted pill
// gradient: LinearGradient(
// begin: Alignment.topCenter,
// end: Alignment.bottomCenter,
// colors: [
// Colors.white.withOpacity(0.28),
// Colors.white.withOpacity(0.08),
// ],
// ),
borderRadius: BorderRadius.circular(60),
border: Border.all(
color: Colors.white.withOpacity(0.35),
width: 2.8,
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_NavItem(
icon: Icons.home_rounded,
label: 'Home',
isActive: currentIndex == 0,
onTap: () => onTap(0),
),
_NavItem(
icon: Icons.receipt_long_rounded,
label: 'Order',
isActive: currentIndex == 1,
onTap: () => onTap(1),
),
_NavItem(
icon: Icons.qr_code_scanner_rounded,
label: 'Scan',
isActive: currentIndex == 2,
onTap: () => onTap(2),
),
_CartNavItem(
isActive: currentIndex == 3,
cartController: cartController,
onTap: () => onTap(3),
),
_NavItem(
icon: Icons.person_rounded,
label: 'Profile',
isActive: currentIndex == 4,
onTap: () => onTap(4),
),
],
),
),
),
);
}
}
// ─── Nav Item ─────────────────────────────────────────────────────────────────
class _NavItem extends StatelessWidget {
final IconData icon;
final String label;
final bool isActive;
final VoidCallback onTap;
const _NavItem({
required this.icon,
required this.label,
required this.isActive,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
behavior: HitTestBehavior.opaque,
child: SizedBox(
width: 64,
height: 72,
child: Stack(
alignment: Alignment.center,
children: [
// White radial glow spotlight for active tab (matches image)
if (isActive)
Container(
width: 68,
height: 68,
decoration: BoxDecoration(
shape: BoxShape.circle,
// gradient: RadialGradient(
// colors: [
// Colors.white.withOpacity(0.55),
// Colors.white.withOpacity(0.0),
// ],
// stops: const [0.0, 1.0],
// radius: 0.60,
// ),
),
),
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
icon,
color: isActive ? _kActive : _kInactive,
size: isActive ? 27 : 22,
),
const SizedBox(height: 4),
Text(
label,
style: TextStyle(
color: isActive ? _kActive : _kInactive,
fontSize: 11,
fontWeight:
isActive ? FontWeight.w700 : FontWeight.w500,
letterSpacing: 0.2,
),
),
],
),
],
),
),
);
}
}
// ─── Cart Nav Item ────────────────────────────────────────────────────────────
class _CartNavItem extends StatelessWidget {
final bool isActive;
final CartController cartController;
final VoidCallback onTap;
const _CartNavItem({
required this.isActive,
required this.cartController,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
behavior: HitTestBehavior.opaque,
child: SizedBox(
width: 64,
height: 72,
child: Stack(
alignment: Alignment.center,
children: [
if (isActive)
Container(
width: 68,
height: 68,
decoration: BoxDecoration(
shape: BoxShape.circle,
// gradient: RadialGradient(
// colors: [
// Colors.white.withOpacity(0.55),
// Colors.white.withOpacity(0.0),
// ],
// stops: const [0.0, 1.0],
// radius: 0.60,
// ),
),
),
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Stack(
clipBehavior: Clip.none,
children: [
Icon(
Icons.shopping_cart_rounded,
color: isActive ? _kActive : _kInactive,
size: isActive ? 27 : 22,
),
Obx(() {
final int count = cartController.totalItems;
if (count == 0) return const SizedBox.shrink();
return Positioned(
right: -8,
top: -6,
child: Container(
padding: const EdgeInsets.all(3),
decoration: const BoxDecoration(
color: Colors.red,
shape: BoxShape.circle,
),
constraints: const BoxConstraints(
minWidth: 17, minHeight: 17),
child: Text(
'$count',
style: const TextStyle(
color: Colors.white,
fontSize: 10,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
),
);
}),
],
),
const SizedBox(height: 4),
Text(
'Cart',
style: TextStyle(
color: isActive ? _kActive : _kInactive,
fontSize: 11,
fontWeight:
isActive ? FontWeight.w700 : FontWeight.w500,
letterSpacing: 0.2,
),
),
],
),
],
),
),
);
}
}

View File

@@ -0,0 +1,272 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../controllers/intro_controller/intro_screen_controller.dart';
class IntroScreenView extends StatelessWidget {
IntroScreenView({super.key});
final IntroScreenController controller = Get.find<IntroScreenController>();
@override
Widget build(BuildContext context) {
return GetBuilder<IntroScreenController>(
builder: (controller) {
return Scaffold(
backgroundColor: Colors.white,
body: SafeArea(
top: false,
child: Stack(
children: [
PageView.builder(
controller: controller.pageController,
onPageChanged: controller.onPageChanged,
itemCount: controller.slides.length,
itemBuilder: (context, index) {
return _IntroPage(slide: controller.slides[index]);
},
),
// Bottom Controls
Positioned(
bottom: 0,
left: 0,
right: 0,
child: _BottomControls(controller: controller),
),
],
),
),
);
},
);
}
}
class _IntroPage extends StatelessWidget {
final IntroSlide slide;
const _IntroPage({required this.slide});
@override
Widget build(BuildContext context) {
final size = MediaQuery.of(context).size;
return Column(
children: [
// Image Section with organic shape background
Expanded(
flex: 6,
child: Stack(
children: [
// Background blob
Positioned.fill(
child: CustomPaint(
painter: _BlobPainter(color: slide.bgColor),
),
),
// Decorative circles
Positioned(
top: 60,
right: 30,
child: _FloatingCircle(size: 20, color: slide.accentColor.withOpacity(0.5)),
),
Positioned(
top: 120,
left: 20,
child: _FloatingCircle(size: 12, color: slide.accentColor.withOpacity(0.35)),
),
Positioned(
bottom: 80,
right: 50,
child: _FloatingCircle(size: 16, color: slide.bgColor.withOpacity(0.8)),
),
// Main image
Center(
child: Padding(
padding: const EdgeInsets.only(top: 60, bottom: 20),
child: Hero(
tag: slide.imageAsset,
child: Image.asset(
slide.imageAsset,
height: size.height * 0.38,
fit: BoxFit.contain,
),
),
),
),
],
),
),
// Text Section
Expanded(
flex: 4,
child: Padding(
padding: const EdgeInsets.fromLTRB(32, 24, 32, 100),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Accent chip
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 5),
decoration: BoxDecoration(
color: slide.accentColor.withOpacity(0.12),
borderRadius: BorderRadius.circular(20),
),
child: Text(
slide.chipLabel,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: slide.accentColor,
letterSpacing: 0.8,
),
),
),
const SizedBox(height: 14),
Text(
slide.title,
style: const TextStyle(
fontSize: 28,
fontWeight: FontWeight.w800,
color: Color(0xFF1A1A2E),
height: 1.2,
letterSpacing: -0.5,
),
),
const SizedBox(height: 12),
Text(
slide.description,
style: const TextStyle(
fontSize: 15,
color: Color(0xFF6B7280),
height: 1.6,
fontWeight: FontWeight.w400,
),
),
],
),
),
),
],
);
}
}
class _BottomControls extends StatelessWidget {
final IntroScreenController controller;
const _BottomControls({required this.controller});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.fromLTRB(28, 16, 28, 36),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Colors.white.withOpacity(0), Colors.white, Colors.white],
stops: const [0, 0.3, 1],
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// Page indicators
Row(
children: List.generate(
controller.slides.length,
(index) => AnimatedContainer(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
margin: const EdgeInsets.only(right: 6),
height: 8,
width: controller.currentPage == index ? 24 : 8,
decoration: BoxDecoration(
color: controller.currentPage == index
? controller.slides[controller.currentPage].accentColor
: const Color(0xFFD1D5DB),
borderRadius: BorderRadius.circular(4),
),
),
),
),
// Action button
GestureDetector(
onTap: controller.isLastPage ? controller.onDonePress : controller.nextPage,
child: AnimatedContainer(
duration: const Duration(milliseconds: 300),
width: controller.isLastPage ? 140 : 56,
height: 56,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
controller.slides[controller.currentPage].accentColor,
controller.slides[controller.currentPage].accentColor.withGreen(
(controller.slides[controller.currentPage].accentColor.green + 30).clamp(0, 255),
),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(28),
boxShadow: [
BoxShadow(
color: controller.slides[controller.currentPage].accentColor.withOpacity(0.35),
blurRadius: 16,
offset: const Offset(0, 6),
),
],
),
child: controller.isLastPage
? const Center(
child: Text(
"Get Started",
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.w700,
fontSize: 15,
),
),
)
: const Icon(Icons.arrow_forward_rounded, color: Colors.white, size: 24),
),
),
],
),
);
}
}
class _FloatingCircle extends StatelessWidget {
final double size;
final Color color;
const _FloatingCircle({required this.size, required this.color});
@override
Widget build(BuildContext context) {
return Container(
width: size,
height: size,
decoration: BoxDecoration(color: color, shape: BoxShape.circle),
);
}
}
class _BlobPainter extends CustomPainter {
final Color color;
_BlobPainter({required this.color});
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()..color = color;
final path = Path();
path.moveTo(0, 0);
path.lineTo(size.width, 0);
path.lineTo(size.width, size.height * 0.75);
path.quadraticBezierTo(size.width * 0.75, size.height * 0.95, size.width * 0.5, size.height * 0.88);
path.quadraticBezierTo(size.width * 0.25, size.height * 0.80, 0, size.height * 0.92);
path.close();
canvas.drawPath(path, paint);
}
@override
bool shouldRepaint(_BlobPainter oldDelegate) => oldDelegate.color != color;
}

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,
);
}
}

View File

@@ -0,0 +1,882 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'dart:math' as math;
import '../../constants/color_constants.dart';
import '../../constants/font_constants.dart';
import '../../widgets/text_widget.dart';
class OrderDetailsPage extends StatefulWidget {
final String orderId;
final String gstno;
final String storeName;
final String storeLocation;
final List<Map<String, dynamic>> items;
final double tax;
final double fee;
const OrderDetailsPage({
super.key,
required this.orderId,
required this.gstno,
required this.storeName,
required this.storeLocation,
required this.items,
required this.tax,
required this.fee,
});
@override
State<OrderDetailsPage> createState() => _OrderDetailsPageState();
}
class _OrderDetailsPageState extends State<OrderDetailsPage> with TickerProviderStateMixin {
late AnimationController _pageController;
late AnimationController _storeCardController;
late AnimationController _itemsController;
late AnimationController _billController;
late AnimationController _statusController;
late Animation<double> _pageFadeAnimation;
late Animation<Offset> _pageSlideAnimation;
late Animation<double> _storeScaleAnimation;
late Animation<double> _storeRotateAnimation;
late Animation<double> _itemsFadeAnimation;
late Animation<Offset> _itemsSlideAnimation;
late Animation<double> _billFadeAnimation;
late Animation<Offset> _billSlideAnimation;
late Animation<double> _statusFadeAnimation;
late Animation<double> _statusScaleAnimation;
@override
void initState() {
super.initState();
// Page animation
_pageController = AnimationController(
duration: const Duration(milliseconds: 500),
vsync: this,
);
_pageFadeAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(parent: _pageController, curve: Curves.easeOut),
);
_pageSlideAnimation = Tween<Offset>(
begin: const Offset(0, 0.03),
end: Offset.zero,
).animate(CurvedAnimation(parent: _pageController, curve: Curves.easeOutCubic));
// Store card animation
_storeCardController = AnimationController(
duration: const Duration(milliseconds: 800),
vsync: this,
);
_storeScaleAnimation = Tween<double>(begin: 0.9, end: 1.0).animate(
CurvedAnimation(parent: _storeCardController, curve: Curves.easeOutBack),
);
_storeRotateAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(parent: _storeCardController, curve: Curves.easeOut),
);
// Items card animation
_itemsController = AnimationController(
duration: const Duration(milliseconds: 600),
vsync: this,
);
_itemsFadeAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(parent: _itemsController, curve: Curves.easeOut),
);
_itemsSlideAnimation = Tween<Offset>(
begin: const Offset(0.05, 0),
end: Offset.zero,
).animate(CurvedAnimation(parent: _itemsController, curve: Curves.easeOutCubic));
// Bill summary animation
_billController = AnimationController(
duration: const Duration(milliseconds: 600),
vsync: this,
);
_billFadeAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(parent: _billController, curve: Curves.easeOut),
);
_billSlideAnimation = Tween<Offset>(
begin: const Offset(-0.05, 0),
end: Offset.zero,
).animate(CurvedAnimation(parent: _billController, curve: Curves.easeOutCubic));
// Status animation
_statusController = AnimationController(
duration: const Duration(milliseconds: 700),
vsync: this,
);
_statusFadeAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(parent: _statusController, curve: Curves.easeOut),
);
_statusScaleAnimation = Tween<double>(begin: 0.85, end: 1.0).animate(
CurvedAnimation(parent: _statusController, curve: Curves.easeOutBack),
);
// Start animations in sequence
_startAnimations();
}
void _startAnimations() async {
_pageController.forward();
await Future.delayed(const Duration(milliseconds: 100));
_storeCardController.forward();
await Future.delayed(const Duration(milliseconds: 200));
_itemsController.forward();
await Future.delayed(const Duration(milliseconds: 150));
_billController.forward();
await Future.delayed(const Duration(milliseconds: 150));
_statusController.forward();
}
@override
void dispose() {
_pageController.dispose();
_storeCardController.dispose();
_itemsController.dispose();
_billController.dispose();
_statusController.dispose();
super.dispose();
}
double rs(BuildContext context, double size) {
final width = MediaQuery.of(context).size.width;
if (width > 600) {
return size * 1.2; // Scale up for tablets
}
return size;
}
@override
Widget build(BuildContext context) {
final total = widget.items.fold<double>(
0.0,
(sum, item) => sum + (double.tryParse(item['productSumPrice'].toString()) ?? 0.0),
);
final grandTotal = total + widget.tax + widget.fee;
return SafeArea(
top: false,
child: LayoutBuilder(
builder: (context, constraints) {
final isTablet = constraints.maxWidth > 600;
final padding = isTablet ? 32.0 : 16.0;
final maxWidth = isTablet ? 800.0 : double.infinity;
return Scaffold(
backgroundColor: const Color(0xFFF8F9FA),
appBar: PreferredSize(
preferredSize: Size.fromHeight(isTablet ? 70 : kToolbarHeight),
child: FadeTransition(
opacity: _pageFadeAnimation,
child: AppBar(
elevation: 0,
backgroundColor: Colors.white,
leading: Container(
margin: EdgeInsets.all(isTablet ? 12 : 8),
decoration: BoxDecoration(
color: const Color(0xFFF8F9FA),
borderRadius: BorderRadius.circular(isTablet ? 16 : 12),
),
child: IconButton(
onPressed: () => Navigator.of(context).pop(),
icon: Icon(
Icons.arrow_back_ios_new_rounded,
size: isTablet ? 22 : 18,
color: ColorConstants.primaryColor,
),
),
),
centerTitle: true,
title: Row(
mainAxisSize: MainAxisSize.min,
children: [
TweenAnimationBuilder<double>(
tween: Tween(begin: 0.0, end: 1.0),
duration: const Duration(milliseconds: 600),
curve: Curves.easeOutBack,
builder: (context, value, child) {
return Transform.scale(
scale: value,
child: Container(
padding: EdgeInsets.all(isTablet ? 8 : 6),
decoration: BoxDecoration(
color: ColorConstants.primaryColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(isTablet ? 12 : 8),
),
child: Icon(
Icons.receipt_long_rounded,
color: ColorConstants.primaryColor,
size: isTablet ? 24 : 18,
),
),
);
},
),
SizedBox(width: isTablet ? 14 : 10),
ReusableTextWidget(
text: 'Order #${widget.orderId}',
color: const Color(0xFF1A1A1A),
fontFamily: FontConstants.fontFamily,
fontSize: rs(context, isTablet ? 20 : 16),
fontWeight: FontWeight.w600,
textAlign: TextAlign.center,
),
],
),
),
),
),
body: FadeTransition(
opacity: _pageFadeAnimation,
child: SlideTransition(
position: _pageSlideAnimation,
child: Center(
child: ConstrainedBox(
constraints: BoxConstraints(maxWidth: maxWidth),
child: SingleChildScrollView(
padding: EdgeInsets.all(padding),
physics: const BouncingScrollPhysics(),
child: Column(
children: [
// Store Information Card
_buildStoreCard(isTablet),
SizedBox(height: isTablet ? 28 : 20),
// Order Items Card
_buildItemsCard(isTablet),
SizedBox(height: isTablet ? 28 : 20),
// Bill Summary Card
_buildBillSummary(isTablet, total, grandTotal),
SizedBox(height: isTablet ? 24 : 16),
// Order Status
_buildOrderStatus(isTablet),
SizedBox(height: isTablet ? 32 : 20),
],
),
),
),
),
),
),
);
},
),
);
}
Widget _buildStoreCard(bool isTablet) {
return ScaleTransition(
scale: _storeScaleAnimation,
child: FadeTransition(
opacity: _storeScaleAnimation,
child: Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(isTablet ? 20 : 16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.04),
blurRadius: isTablet ? 15 : 10,
offset: Offset(0, isTablet ? 3 : 2),
),
],
),
child: Column(
children: [
// Store Header
Container(
padding: EdgeInsets.all(isTablet ? 28 : 20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(isTablet ? 20 : 16),
topRight: Radius.circular(isTablet ? 20 : 16),
),
),
child: Row(
children: [
TweenAnimationBuilder<double>(
tween: Tween(begin: 0.0, end: 2 * math.pi),
duration: const Duration(milliseconds: 1000),
curve: Curves.easeOut,
builder: (context, value, child) {
return Transform.rotate(
angle: value,
child: Container(
padding: EdgeInsets.all(isTablet ? 16 : 12),
decoration: BoxDecoration(
color: ColorConstants.primaryColor,
borderRadius: BorderRadius.circular(isTablet ? 16 : 12),
boxShadow: [
BoxShadow(
color: ColorConstants.primaryColor.withOpacity(0.2),
blurRadius: isTablet ? 12 : 8,
offset: Offset(0, isTablet ? 6 : 4),
),
],
),
child: Icon(
Icons.storefront_rounded,
color: Colors.white,
size: isTablet ? 28 : 20,
),
),
);
},
),
SizedBox(width: isTablet ? 20 : 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TweenAnimationBuilder<double>(
tween: Tween(begin: 0.0, end: 1.0),
duration: const Duration(milliseconds: 800),
curve: Curves.easeOut,
builder: (context, value, child) {
return Opacity(
opacity: value,
child: ReusableTextWidget(
text: widget.storeName,
color: const Color(0xFF1A1A1A),
fontFamily: FontConstants.fontFamily,
fontSize: rs(context, isTablet ? 20 : 16),
fontWeight: FontWeight.w600,
textAlign: TextAlign.start,
),
);
},
),
SizedBox(height: isTablet ? 6 : 4),
TweenAnimationBuilder<double>(
tween: Tween(begin: 0.0, end: 1.0),
duration: const Duration(milliseconds: 1000),
curve: Curves.easeOut,
builder: (context, value, child) {
return Opacity(
opacity: value,
child: Row(
children: [
Icon(
Icons.location_on_outlined,
size: isTablet ? 16 : 12,
color: const Color(0xFF6B7280),
),
SizedBox(width: isTablet ? 6 : 4),
Flexible(
child: ReusableTextWidget(
text: widget.storeLocation,
color: const Color(0xFF6B7280),
fontFamily: FontConstants.fontFamily,
fontSize: rs(context, isTablet ? 14 : 12),
fontWeight: FontWeight.w400,
textAlign: TextAlign.start,
overflow: TextOverflow.ellipsis,
),
),
],
),
);
},
),
],
),
),
],
),
),
// GST Information with slide animation
TweenAnimationBuilder<Offset>(
tween: Tween(begin: const Offset(-0.1, 0), end: Offset.zero),
duration: const Duration(milliseconds: 800),
curve: Curves.easeOutCubic,
builder: (context, offset, child) {
return Transform.translate(
offset: Offset(offset.dx * 100, 0),
child: Container(
padding: EdgeInsets.symmetric(
horizontal: isTablet ? 28 : 20,
vertical: isTablet ? 20 : 16,
),
child: Row(
children: [
TweenAnimationBuilder<double>(
tween: Tween(begin: 0.0, end: 1.0),
duration: const Duration(milliseconds: 600),
curve: Curves.easeOutBack,
builder: (context, value, child) {
return Transform.scale(
scale: value,
child: Icon(
Icons.verified_user_outlined,
size: isTablet ? 22 : 16,
color: ColorConstants.primaryColor,
),
);
},
),
SizedBox(width: isTablet ? 16 : 12),
ReusableTextWidget(
text: 'GST Number',
color: const Color(0xFF6B7280),
fontFamily: FontConstants.fontFamily,
fontSize: rs(context, isTablet ? 14 : 12),
fontWeight: FontWeight.w500,
textAlign: TextAlign.start,
),
const Spacer(),
ReusableTextWidget(
text: widget.gstno,
color: const Color(0xFF1A1A1A),
fontFamily: FontConstants.fontFamily,
fontSize: rs(context, isTablet ? 15 : 13),
fontWeight: FontWeight.w600,
textAlign: TextAlign.start,
),
],
),
),
);
},
),
],
),
),
),
);
}
Widget _buildItemsCard(bool isTablet) {
return FadeTransition(
opacity: _itemsFadeAnimation,
child: SlideTransition(
position: _itemsSlideAnimation,
child: Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(isTablet ? 20 : 16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.04),
blurRadius: isTablet ? 15 : 10,
offset: Offset(0, isTablet ? 3 : 2),
),
],
),
child: Column(
children: [
// Header
Container(
padding: EdgeInsets.all(isTablet ? 28 : 20),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(color: const Color(0xFFE5E7EB), width: 1),
),
),
child: Row(
children: [
TweenAnimationBuilder<double>(
tween: Tween(begin: 0.0, end: 1.0),
duration: const Duration(milliseconds: 600),
curve: Curves.easeOutBack,
builder: (context, value, child) {
return Transform.scale(
scale: value,
child: Icon(
Icons.shopping_bag_outlined,
color: ColorConstants.primaryColor,
size: isTablet ? 24 : 18,
),
);
},
),
SizedBox(width: isTablet ? 16 : 12),
ReusableTextWidget(
text: 'Order Items',
color: const Color(0xFF1A1A1A),
fontFamily: FontConstants.fontFamily,
fontSize: rs(context, isTablet ? 18 : 15),
fontWeight: FontWeight.w600,
textAlign: TextAlign.start,
),
const Spacer(),
TweenAnimationBuilder<double>(
tween: Tween(begin: 0.0, end: 1.0),
duration: const Duration(milliseconds: 800),
curve: Curves.easeOutBack,
builder: (context, value, child) {
return Transform.scale(
scale: value,
child: Container(
padding: EdgeInsets.symmetric(
horizontal: isTablet ? 14 : 10,
vertical: isTablet ? 6 : 4,
),
decoration: BoxDecoration(
color: ColorConstants.primaryColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(isTablet ? 10 : 8),
),
child: ReusableTextWidget(
text: '${widget.items.length} ${widget.items.length == 1 ? 'item' : 'items'}',
color: ColorConstants.primaryColor,
fontFamily: FontConstants.fontFamily,
fontSize: rs(context, isTablet ? 14 : 12),
fontWeight: FontWeight.w600,
textAlign: TextAlign.center,
),
),
);
},
),
],
),
),
// Items List with staggered animation
ListView.separated(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: widget.items.length,
separatorBuilder: (context, index) => Divider(
height: 1,
color: const Color(0xFFE5E7EB),
indent: isTablet ? 28 : 20,
endIndent: isTablet ? 28 : 20,
),
itemBuilder: (context, index) {
final item = widget.items[index];
return TweenAnimationBuilder<double>(
tween: Tween(begin: 0.0, end: 1.0),
duration: Duration(milliseconds: 400 + (index * 100)),
curve: Curves.easeOutCubic,
builder: (context, value, child) {
return Transform.translate(
offset: Offset(30 * (1 - value), 0),
child: Opacity(
opacity: value,
child: Padding(
padding: EdgeInsets.symmetric(
horizontal: isTablet ? 28 : 20,
vertical: isTablet ? 20 : 16,
),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ReusableTextWidget(
text: item['name'] ?? '',
color: const Color(0xFF1A1A1A),
fontFamily: FontConstants.fontFamily,
fontSize: rs(context, isTablet ? 16 : 14),
fontWeight: FontWeight.w500,
textAlign: TextAlign.start,
),
SizedBox(height: isTablet ? 6 : 4),
ReusableTextWidget(
text: 'Qty: ${item['quantity']}',
color: const Color(0xFF6B7280),
fontFamily: FontConstants.fontFamily,
fontSize: rs(context, isTablet ? 14 : 12),
fontWeight: FontWeight.w400,
textAlign: TextAlign.start,
),
],
),
),
TweenAnimationBuilder<double>(
tween: Tween(begin: 0.0, end: 1.0),
duration: Duration(milliseconds: 600 + (index * 100)),
curve: Curves.easeOut,
builder: (context, priceValue, child) {
return Opacity(
opacity: priceValue,
child: ReusableTextWidget(
text: '${(item['price'] ?? 0).toStringAsFixed(2)}',
color: const Color(0xFF1A1A1A),
fontFamily: FontConstants.fontFamily,
fontSize: rs(context, isTablet ? 18 : 15),
fontWeight: FontWeight.w600,
textAlign: TextAlign.start,
),
);
},
),
],
),
),
),
);
},
);
},
),
],
),
),
),
);
}
Widget _buildBillSummary(bool isTablet, double total, double grandTotal) {
return FadeTransition(
opacity: _billFadeAnimation,
child: SlideTransition(
position: _billSlideAnimation,
child: Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(isTablet ? 20 : 16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.04),
blurRadius: isTablet ? 15 : 10,
offset: Offset(0, isTablet ? 3 : 2),
),
],
),
child: Column(
children: [
// Header
Container(
padding: EdgeInsets.all(isTablet ? 28 : 20),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(color: const Color(0xFFE5E7EB), width: 1),
),
),
child: Row(
children: [
TweenAnimationBuilder<double>(
tween: Tween(begin: 0.0, end: 1.0),
duration: const Duration(milliseconds: 600),
curve: Curves.easeOutBack,
builder: (context, value, child) {
return Transform.scale(
scale: value,
child: Icon(
Icons.receipt_outlined,
color: ColorConstants.primaryColor,
size: isTablet ? 24 : 18,
),
);
},
),
SizedBox(width: isTablet ? 16 : 12),
ReusableTextWidget(
text: 'Bill Summary',
color: const Color(0xFF1A1A1A),
fontFamily: FontConstants.fontFamily,
fontSize: rs(context, isTablet ? 18 : 15),
fontWeight: FontWeight.w600,
textAlign: TextAlign.start,
),
],
),
),
Padding(
padding: EdgeInsets.all(isTablet ? 28 : 20),
child: Column(
children: [
_buildAnimatedBillRow('Subtotal', total, isTablet, 0),
SizedBox(height: isTablet ? 16 : 12),
_buildAnimatedBillRow('GST', widget.tax, isTablet, 100),
SizedBox(height: isTablet ? 16 : 12),
_buildAnimatedBillRow('Delivery Fee', widget.fee, isTablet, 200),
SizedBox(height: isTablet ? 20 : 16),
TweenAnimationBuilder<double>(
tween: Tween(begin: 0.0, end: 1.0),
duration: const Duration(milliseconds: 600),
curve: Curves.easeOut,
builder: (context, value, child) {
return Opacity(
opacity: value,
child: Divider(height: 1, color: const Color(0xFFE5E7EB)),
);
},
),
SizedBox(height: isTablet ? 20 : 16),
// Grand Total with pulse animation
TweenAnimationBuilder<double>(
tween: Tween(begin: 0.92, end: 1.0),
duration: const Duration(milliseconds: 800),
curve: Curves.easeOutBack,
builder: (context, value, child) {
return Transform.scale(
scale: value,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
ReusableTextWidget(
text: 'Grand Total',
color: const Color(0xFF1A1A1A),
fontFamily: FontConstants.fontFamily,
fontSize: rs(context, isTablet ? 19 : 16),
fontWeight: FontWeight.w700,
textAlign: TextAlign.start,
),
TweenAnimationBuilder<double>(
tween: Tween(begin: 0.0, end: grandTotal),
duration: const Duration(milliseconds: 1200),
curve: Curves.easeOut,
builder: (context, animatedValue, child) {
return ReusableTextWidget(
text: '${animatedValue.toStringAsFixed(2)}',
color: ColorConstants.primaryColor,
fontFamily: FontConstants.fontFamily,
fontSize: rs(context, isTablet ? 22 : 18),
fontWeight: FontWeight.w700,
textAlign: TextAlign.start,
);
},
),
],
),
);
},
),
],
),
),
],
),
),
),
);
}
Widget _buildAnimatedBillRow(String label, double amount, bool isTablet, int delay) {
return TweenAnimationBuilder<double>(
tween: Tween(begin: 0.0, end: 1.0),
duration: Duration(milliseconds: 500 + delay),
curve: Curves.easeOut,
builder: (context, value, child) {
return Transform.translate(
offset: Offset(-20 * (1 - value), 0),
child: Opacity(
opacity: value,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
ReusableTextWidget(
text: label,
color: const Color(0xFF6B7280),
fontFamily: FontConstants.fontFamily,
fontSize: rs(context, isTablet ? 15 : 13),
fontWeight: FontWeight.w400,
textAlign: TextAlign.start,
),
ReusableTextWidget(
text: '${amount.toStringAsFixed(2)}',
color: const Color(0xFF1A1A1A),
fontFamily: FontConstants.fontFamily,
fontSize: rs(context, isTablet ? 16 : 14),
fontWeight: FontWeight.w500,
textAlign: TextAlign.start,
),
],
),
),
);
},
);
}
Widget _buildOrderStatus(bool isTablet) {
return FadeTransition(
opacity: _statusFadeAnimation,
child: ScaleTransition(
scale: _statusScaleAnimation,
child: Container(
padding: EdgeInsets.all(isTablet ? 28 : 20),
decoration: BoxDecoration(
color: const Color(0xFFECFDF5),
borderRadius: BorderRadius.circular(isTablet ? 20 : 16),
border: Border.all(
color: const Color(0xFF10B981),
width: 1,
),
),
child: Row(
children: [
TweenAnimationBuilder<double>(
tween: Tween(begin: 0.0, end: 1.0),
duration: const Duration(milliseconds: 800),
curve: Curves.easeOutBack,
builder: (context, value, child) {
return Transform.scale(
scale: value,
child: Container(
padding: EdgeInsets.all(isTablet ? 14 : 10),
decoration: BoxDecoration(
color: const Color(0xFF10B981),
borderRadius: BorderRadius.circular(isTablet ? 14 : 10),
),
child: Icon(
Icons.check_rounded,
color: Colors.white,
size: isTablet ? 24 : 20,
),
),
);
},
),
SizedBox(width: isTablet ? 20 : 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
ReusableTextWidget(
text: 'We appreciate your order!',
color: const Color(0xFF1F2937),
fontFamily: FontConstants.fontFamily,
fontSize: rs(context, isTablet ? 18 : 15),
fontWeight: FontWeight.w600,
textAlign: TextAlign.start,
),
SizedBox(height: isTablet ? 6 : 4),
ReusableTextWidget(
text: 'Our team is taking care of it.',
color: const Color(0xFF6B7280),
fontFamily: FontConstants.fontFamily,
fontSize: rs(context, isTablet ? 14 : 12),
fontWeight: FontWeight.w400,
textAlign: TextAlign.start,
),
],
),
)
],
),
),
),
);
}
}

View File

@@ -0,0 +1,266 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:lottie/lottie.dart';
import 'package:nearledaily/constants/color_constants.dart';
import '../../../constants/font_constants.dart';
import '../../controllers/cart_controller/cart.dart';
import '../../controllers/dashboard_controller/dashboard_controller.dart';
import '../../widgets/text_widget.dart';
import '../home_view.dart';
class OrderSuccessView extends StatefulWidget {
const OrderSuccessView({super.key});
@override
State<OrderSuccessView> createState() => _OrderSuccessViewState();
}
class _OrderSuccessViewState extends State<OrderSuccessView>
with TickerProviderStateMixin {
late AnimationController _fadeController;
late AnimationController _slideController;
late AnimationController _pulseController;
late Animation<double> _fadeAnim;
late Animation<Offset> _slideAnim;
late Animation<double> _pulseAnim;
@override
void initState() {
super.initState();
_fadeController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 800),
);
_slideController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 700),
);
_pulseController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 1500),
)..repeat(reverse: true);
_fadeAnim = CurvedAnimation(parent: _fadeController, curve: Curves.easeIn);
_slideAnim = Tween<Offset>(
begin: const Offset(0, 0.3),
end: Offset.zero,
).animate(CurvedAnimation(parent: _slideController, curve: Curves.easeOut));
_pulseAnim = Tween<double>(begin: 1.0, end: 1.06).animate(
CurvedAnimation(parent: _pulseController, curve: Curves.easeInOut),
);
Future.delayed(const Duration(milliseconds: 200), () {
_fadeController.forward();
_slideController.forward();
});
}
@override
void dispose() {
_fadeController.dispose();
_slideController.dispose();
_pulseController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final size = MediaQuery.of(context).size;
final DashboardController controller = Get.put(DashboardController());
return PopScope(
canPop: false,
onPopInvokedWithResult: (didPop, result) {
if (!didPop) Get.back();
},
child: Scaffold(
backgroundColor: const Color(0xFFF6FBF4),
body: Stack(
children: [
// Decorative background blobs
Positioned(
top: -60,
right: -60,
child: Container(
width: 220,
height: 220,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: ColorConstants.primaryColor.withOpacity(0.08),
),
),
),
Positioned(
bottom: 120,
left: -80,
child: Container(
width: 200,
height: 200,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: ColorConstants.primaryColor.withOpacity(0.06),
),
),
),
SafeArea(
child: FadeTransition(
opacity: _fadeAnim,
child: SlideTransition(
position: _slideAnim,
child: Padding(
padding: EdgeInsets.symmetric(horizontal: size.width * 0.06),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(height: size.height * 0.05),
// Lottie animation with soft card bg
Container(
width: size.width * 0.70,
height: size.width * 0.70,
decoration: BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: ColorConstants.primaryColor.withOpacity(0.15),
blurRadius: 40,
spreadRadius: 8,
),
],
),
child: Lottie.asset(
repeat: false,
'assets/images/orderSuccess.json',
fit: BoxFit.contain,
),
),
SizedBox(height: size.height * 0.04),
// Headline
ReusableTextWidget(
text: 'Order Placed! 🎉',
color: const Color(0xFF1A2E1A),
fontFamily: FontConstants.fontFamily,
fontSize: 28,
fontWeight: FontWeight.w800,
textAlign: TextAlign.center,
),
SizedBox(height: size.height * 0.012),
// Subtitle
ReusableTextWidget(
text: "Your order is confirmed and\nbeing processed right now.",
color: const Color(0xFF6B7C6B),
fontFamily: FontConstants.fontFamily,
fontSize: 16,
fontWeight: FontWeight.w400,
maxLines: 3,
textAlign: TextAlign.center,
),
SizedBox(height: size.height * 0.04),
// Status chips row
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_StatusChip(icon: Icons.check_circle_rounded, label: 'Confirmed', color: ColorConstants.primaryColor),
const SizedBox(width: 10),
_StatusChip(icon: Icons.inventory_2_rounded, label: 'Packing', color: Colors.orange),
const SizedBox(width: 10),
_StatusChip(icon: Icons.local_shipping_rounded, label: 'On the way', color: Colors.blueAccent),
],
),
],
),
),
),
),
),
],
),
bottomNavigationBar: SafeArea(
child: Padding(
padding: EdgeInsets.fromLTRB(
size.width * 0.06,
0,
size.width * 0.06,
size.height * 0.02,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Primary CTA
ScaleTransition(
scale: _pulseAnim,
child: SizedBox(
width: double.infinity,
height: 56,
child: ElevatedButton(
onPressed: () {
controller.show.value = false;
final cartCtrl = Get.find<CartController>();
cartCtrl.appliedCoupon.value = "";
cartCtrl.amt.value = "";
Get.offAll(BottomNavigation());
},
style: ElevatedButton.styleFrom(
backgroundColor: ColorConstants.primaryColor,
foregroundColor: Colors.white,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
),
child: ReusableTextWidget(
text: 'Back to Home',
color: Colors.white,
fontFamily: FontConstants.fontFamily,
fontSize: 17,
fontWeight: FontWeight.w600,
textAlign: TextAlign.center,
),
),
),
),
SizedBox(height: size.height * 0.015),
],
),
),
),
),
);
}
}
// Small reusable status chip widget
class _StatusChip extends StatelessWidget {
final IconData icon;
final String label;
final Color color;
const _StatusChip({
required this.icon,
required this.label,
required this.color,
});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
child: SizedBox(),
);
}
}

View File

@@ -0,0 +1,744 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:shimmer/shimmer.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../constants/asset_constants.dart';
import '../../constants/color_constants.dart';
import '../../constants/font_constants.dart';
import '../../controllers/tenant/get_tenant.dart'; // OrderedTenantController
import '../../widgets/text_widget.dart';
import 'my_orders.dart'; // OrderDatum
class OrdersByStoreScreen extends StatefulWidget {
final bool showBackArrow;
const OrdersByStoreScreen({super.key, required this.showBackArrow});
@override
_OrdersByStoreScreenState createState() => _OrdersByStoreScreenState();
}
class _OrdersByStoreScreenState extends State<OrdersByStoreScreen>
with SingleTickerProviderStateMixin {
final OrderedTenantController tenantController =
Get.put(OrderedTenantController());
final ScrollController _scrollController = ScrollController();
static const Color primaryColor = Color(0xFF662582);
int? _expandedIndex; // ✅ track which tile is expanded
late AnimationController _fabAnimationController;
late Animation<double> _fabAnimation;
final List<String> emojis = ['😡', '😕', '😐', '😊', '😍'];
Color _getStatusColor(String? status) {
final cleanStatus = status?.trim().toLowerCase() ?? '';
switch (cleanStatus) {
case 'created':
return Colors.blue;
case 'pending':
return Colors.orange;
case 'cancelled':
return Colors.red;
case 'completed':
return Colors.green;
default:
return Colors.grey;
}
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
tenantController
.refreshOrders(); // ✅ auto refresh every time this screen rebuilds
}
@override
void initState() {
super.initState();
// Initialize FAB animation
_fabAnimationController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 300),
);
_fabAnimation = CurvedAnimation(
parent: _fabAnimationController,
curve: Curves.easeInOut,
);
// Load initial orders
tenantController.loadOrders();
// Listen for scroll to bottom
_scrollController.addListener(() {
// FAB animation based on scroll
if (_scrollController.offset > 100) {
_fabAnimationController.forward();
} else {
_fabAnimationController.reverse();
}
if (_scrollController.position.pixels >=
_scrollController.position.maxScrollExtent - 200 && // near bottom
!tenantController.isLoading.value) {
tenantController.pageNo++; // increment page
tenantController.loadOrders();
}
});
}
@override
void dispose() {
_scrollController.dispose();
_fabAnimationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return SafeArea(
top: false,
child: Scaffold(
backgroundColor: Colors.grey[50],
body: CustomScrollView(
controller: _scrollController,
physics: const BouncingScrollPhysics(), // ✨ Smooth bouncing scroll
slivers: [
SliverAppBar(
backgroundColor: Colors.white,
surfaceTintColor: Colors.transparent,
// 🔥 Prevent color overlay when scrolled
scrolledUnderElevation: 0,
floating: false,
pinned: true,
// 👈 use widget.showBackArrow
automaticallyImplyLeading: widget.showBackArrow,
leading: widget.showBackArrow
? IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.black),
onPressed: () => Navigator.pop(context),
splashRadius: 24, // ✨ Better ripple effect
)
: null,
title: const Text(
'Orders',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.black,
),
),
// titleSpacing: -5,
centerTitle: false,
elevation: 0,
),
const SliverToBoxAdapter(child: SizedBox(height: 8)),
Obx(() {
if (tenantController.isLoading.value &&
tenantController.orders.isEmpty) {
// Initial loading
return SliverFillRemaining(
hasScrollBody: false,
child: shimmerListView(),
);
}
if (tenantController.orders.isEmpty) {
final screenSize = MediaQuery.of(context).size;
return SliverToBoxAdapter(
child: emptyOrdersWidget(screenSize),
);
}
return SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
if (index == tenantController.orders.length) {
// Loader at bottom
return tenantController.isLoading.value
? const Padding(
padding: EdgeInsets.symmetric(vertical: 16),
child: Center(child: CircularProgressIndicator()),
)
: const SizedBox.shrink();
}
final order = tenantController.orders[index];
final tenantName = order.tenantname ?? 'Unknown Tenant';
double totalAmount = 0.0;
if (order.orderdetails != null &&
order.orderdetails!.isNotEmpty) {
totalAmount = order.orderdetails!
.map((item) => item.productsumprice ?? 0.0)
.reduce((a, b) => a + b);
}
// ✨ Staggered fade-in animation for each item
return TweenAnimationBuilder<double>(
tween: Tween(begin: 0.0, end: 1.0),
duration: Duration(milliseconds: 300 + (index * 50)),
curve: Curves.easeOutCubic,
builder: (context, value, child) {
return Transform.translate(
offset: Offset(0, 20 * (1 - value)),
child: Opacity(
opacity: value,
child: child,
),
);
},
child: Builder(builder: (tileContext) {
return Container(
margin: const EdgeInsets.symmetric(
vertical: 4, horizontal: 8),
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(color: Colors.black12, width: 0.45),
borderRadius: BorderRadius.circular(8),
),
child: Theme(
data: Theme.of(context)
.copyWith(dividerColor: Colors.transparent),
child: ExpansionTile(
key: ValueKey(
'${order.orderid}-${_expandedIndex == index}'),
//initiallyExpanded: _expandedIndex == index,
initiallyExpanded: _expandedIndex == index,
title: ReusableTextWidget(
text: tenantName,
color: primaryColor,
fontWeight: FontWeight.bold,
fontSize: 15,
fontFamily: FontConstants.fontFamily,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
trailing: Container(
padding: const EdgeInsets.symmetric(
horizontal: 6, vertical: 2),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(6),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
// ● Circle dot
Container(
width: 6,
height: 6,
decoration: BoxDecoration(
color: _getStatusColor(order.orderstatus),
shape: BoxShape.circle,
),
),
const SizedBox(width: 4),
Text(
(order.orderstatus ?? 'Pending')
.capitalizeFirst!,
style: const TextStyle(
color: Colors.black,
fontSize: 10,
fontWeight: FontWeight.bold,
),
),
],
),
),
subtitle: Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Icon(Icons.location_on,
size: 13, color: Colors.grey),
const SizedBox(width: 2),
ReusableTextWidget(
text: order.tenantsuburb ?? 'Unknown Location',
color: Colors.grey[700]!,
fontWeight: FontWeight.w400,
fontSize: 10,
fontFamily: FontConstants.fontFamily,
),
],
),
leading: Container(
height: 50,
width: 50,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
color: Colors.grey[200],
),
child: (order.tenantimage != null &&
order.tenantimage!.isNotEmpty)
? ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.network(
order.tenantimage!,
fit: BoxFit.cover,
errorBuilder:
(context, error, stackTrace) {
return Icon(Icons.store,
size: 28,
color: Colors.grey[700]);
},
),
)
: Icon(Icons.store,
size: 28, color: Colors.grey[700]),
),
// ✅ this callback runs when the tile is expanded or collapsed
onExpansionChanged: (expanded) {
setState(() {
_expandedIndex = expanded ? index : null;
});
if (expanded) {
// ✨ Haptic feedback
HapticFeedback.selectionClick();
// ✨ Smooth scroll to expanded item
Future.delayed(const Duration(milliseconds: 200),
() {
Scrollable.ensureVisible(
tileContext,
duration: const Duration(milliseconds: 500),
curve: Curves.easeInOut,
alignment: 0.1,
);
});
}
},
children: [
// ✨ Animated container for smooth expansion
AnimatedContainer(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
child: Padding(
padding: const EdgeInsets.only(top: 0, bottom: 8),
child: Column(
children: [
GestureDetector(
onTap: () {},
child: Container(
height: 180,
width: double.infinity,
margin: const EdgeInsets.symmetric(
vertical: 6, horizontal: 12),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius:
BorderRadius.circular(9),
border: Border.all(
color: Colors.black12,
width: 0.20),
boxShadow: [
BoxShadow(
color: Colors.black
.withOpacity(0.04),
blurRadius: 4,
offset: const Offset(0, 1),
),
],
),
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment:
MainAxisAlignment
.spaceBetween,
children: [
ReusableTextWidget(
text:
"Order ID: ${order.orderid ?? 'Unknown'}",
color: ColorConstants
.blackColor
.withOpacity(0.87),
fontWeight: FontWeight.w600,
fontSize: 14,
fontFamily: FontConstants
.fontFamily,
),
Container(
padding:
const EdgeInsets
.symmetric(
horizontal: 8,
vertical: 4),
decoration: BoxDecoration(
color: Colors.purple
.withOpacity(0.2),
borderRadius:
BorderRadius.circular(
12),
),
child: ReusableTextWidget(
text:
"${order.orderdetails?.fold<int>(0, (sum, item) => sum + (item.orderqty ?? 0)) ?? 0}",
color: primaryColor,
fontWeight:
FontWeight.bold,
fontSize: 12,
fontFamily: FontConstants
.fontFamily,
),
),
],
),
const SizedBox(height: 6),
Row(
children: [
ReusableTextWidget(
text: "Total Amount: ",
color: ColorConstants
.blackColor
.withOpacity(0.65),
fontWeight: FontWeight.w600,
fontSize: 12,
fontFamily: FontConstants
.fontFamily,
),
ReusableTextWidget(
text:
"${totalAmount.toStringAsFixed(2)}",
color: ColorConstants
.blackColor
.withOpacity(0.67),
fontWeight: FontWeight.w600,
fontSize: 13,
fontFamily: FontConstants
.fontFamily,
),
],
),
const SizedBox(height: 6),
Row(
crossAxisAlignment:
CrossAxisAlignment.start,
mainAxisAlignment:
MainAxisAlignment.start,
children: [
const SizedBox(width: 4),
ReusableTextWidget(
text: order.orderdate != null
? "${order.orderdate!.day} ${_getMonthName(order.orderdate!.month)} ${order.orderdate!.year}"
: 'No Date',
color: ColorConstants
.blackColor
.withOpacity(0.65),
fontWeight: FontWeight.w500,
fontSize: 12,
fontFamily: FontConstants
.fontFamily,
),
],
),
const SizedBox(height: 8),
Divider(color: Colors.grey[300]),
const SizedBox(
height: 5,
),
Row(
mainAxisAlignment:
MainAxisAlignment
.spaceBetween,
children: [
GestureDetector(
onTap: () async {
final uri = Uri(scheme: 'tel', path: order.pickupcontactno!);
if (await canLaunchUrl(uri)) {
await launchUrl(uri);
}
},
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ReusableTextWidget(
text: 'Contact :',
color: ColorConstants.blackColor.withOpacity(0.67),
fontWeight: FontWeight.w600,
fontSize: 13,
fontFamily: FontConstants.fontFamily,
),
const SizedBox(height: 4),
Row(
children: [
Icon(
Icons.phone_rounded,
size: 14,
color: ColorConstants.primaryColor,
),
const SizedBox(width: 5),
ReusableTextWidget(
text: order.pickupcontactno ?? "No Contact",
color: ColorConstants.primaryColor,
fontWeight: FontWeight.w600,
fontSize: 13,
fontFamily: FontConstants.fontFamily,
),
],
),
],
),
),
ElevatedButton(
style: ElevatedButton
.styleFrom(
padding:
const EdgeInsets
.symmetric(
horizontal: 12,
vertical: 5),
shape:
RoundedRectangleBorder(
borderRadius:
BorderRadius.circular(
8),
),
backgroundColor:
primaryColor,
),
onPressed: () {
// ✨ Haptic feedback on button press
HapticFeedback.mediumImpact();
// ✨ Smooth page transition
Navigator.push(
context,
PageRouteBuilder(
pageBuilder: (context,
animation,
secondaryAnimation) =>
OrderDetailsPage(
orderId: order
.orderid ??
'Unknown',
storeName: tenantName,
storeLocation: order
.tenantsuburb ??
'Unknown',
tax: order
.totaltaxamount ??
0,
gstno: order.gstno ?? "",
fee: order
.deliverycharge ??
0,
items: order
.orderdetails
?.map((item) =>
{
'name':
item.productname ?? 'Unknown',
'quantity':
item.orderqty ?? 0,
'productSumPrice':
item.productsumprice ?? 0.0,
'price':
item.price ?? 0.0,
'discountamount':
item.price ?? 0.0,
'image':
item.productimage ?? '',
})
.toList() ??
[],
),
transitionsBuilder:
(context,
animation,
secondaryAnimation,
child) {
return FadeTransition(
opacity: animation,
child:
SlideTransition(
position:
Tween<Offset>(
begin:
const Offset(
0.05, 0),
end:
Offset.zero,
).animate(
animation),
child: child,
),
);
},
transitionDuration:
const Duration(
milliseconds:
300),
),
);
},
child: const Text(
"View Details",
style: TextStyle(
fontSize: 12,
color: Colors.white),
),
),
],
),
],
),
),
),
],
),
),
),
],
),
),
);
}),
);
},
childCount:
tenantController.orders.length + 1, // extra for loader
),
);
}),
],
),
// ✨ Floating Action Button for scroll to top
floatingActionButton: ScaleTransition(
scale: _fabAnimation,
child: FloatingActionButton.small(
backgroundColor: primaryColor,
elevation: 4,
onPressed: () {
HapticFeedback.mediumImpact();
_scrollController.animateTo(
0,
duration: const Duration(milliseconds: 500),
curve: Curves.easeInOut,
);
},
child: const Icon(
Icons.arrow_upward,
color: Colors.white,
size: 20,
),
),
),
),
);
}
// Helper method to get month name
String _getMonthName(int month) {
const months = [
'Jan',
'Feb',
'Mar',
'Apr',
'May',
'Jun',
'Jul',
'Aug',
'Sep',
'Oct',
'Nov',
'Dec'
];
return months[month - 1];
}
// Shimmer placeholder for initial loading
Widget shimmerListView() {
return Shimmer.fromColors(
baseColor: Colors.grey.shade300,
highlightColor: Colors.grey.shade100,
child: Column(
children: List.generate(15, (index) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0,horizontal: 8),
child: Container(
height: 80,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
),
),
);
}),
),
);
}
}
Widget emptyOrdersWidget(Size screenSize) {
// ✨ Animated empty state
return TweenAnimationBuilder<double>(
tween: Tween(begin: 0.0, end: 1.0),
duration: const Duration(milliseconds: 600),
curve: Curves.easeOutCubic,
builder: (context, value, child) {
return Transform.scale(
scale: 0.8 + (0.2 * value),
child: Opacity(
opacity: value,
child: child,
),
);
},
child: Padding(
padding: EdgeInsets.only(
left: screenSize.width * 0.08,
right: screenSize.width * 0.08,
top: screenSize.height * 0.12,
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(height: screenSize.height * 0.06),
Image.asset(
AssetConstants.noOrders,
height: screenSize.height * 0.25,
width: screenSize.width * 0.50,
fit: BoxFit.fill,
),
ReusableTextWidget(
text: 'No Orders Yet!',
color: ColorConstants.blackColor,
fontFamily: FontConstants.fontFamily,
fontSize: 18,
fontWeight: FontWeight.w700,
maxLines: 2,
textAlign: TextAlign.center,
),
SizedBox(height: screenSize.height * 0.01),
ReusableTextWidget(
text: 'Stay tuned, your next order will appear here soon!',
color: ColorConstants.blackColor,
fontFamily: FontConstants.fontFamily,
fontSize: 14,
fontWeight: FontWeight.normal,
maxLines: 2,
textAlign: TextAlign.center,
),
],
),
),
);
}

View File

@@ -0,0 +1,339 @@
// lib/views/products/sub_category_products_screen.dart
// This is a copy of ProductsScreen but with different class name
// Used when navigating directly to a specific subcategory
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_slidable/flutter_slidable.dart';
import 'package:get/get.dart';
import 'package:lottie/lottie.dart';
import 'package:nearledaily/constants/color_constants.dart';
import 'package:nearledaily/view/product/product_view.dart';
import 'package:shimmer/shimmer.dart';
import '../../constants/asset_constants.dart';
import '../../constants/font_constants.dart';
import '../../controllers/cart_controller/cart.dart';
import '../../controllers/product/product_controller.dart';
import '../../controllers/product/variant_controller.dart';
import '../../domain/provider/varient/varient_pro.dart';
import '../../modules/product/product.dart';
import '../../widgets/text_widget.dart';
import '../cart/cart_view.dart';
class SubCategoryProductsScreen extends StatelessWidget {
final int categoryId;
final int tenantId;
final int locationId;
final int tenantloc;
final String tenantName;
final String locationname;
final String tenantLocation;
final String tenantImage;
final String subCategoryName;
bool ss = false;
SubCategoryProductsScreen({
Key? key,
required this.categoryId,
required this.tenantId,
required this.locationId,
required this.tenantloc,
required this.tenantName,
required this.locationname,
required this.tenantLocation,
required this.tenantImage,
required this.subCategoryName,
}) : super(key: key);
final ProductsController controller = Get.put(ProductsController());
final variantController = Get.put(
ProductVariantController(provider: ProductVariantProvider()),
);
final CartController cartController = Get.put(CartController());
final provider = ProductVariantProvider();
@override
Widget build(BuildContext context) {
controller.fetchProducts(categoryId, tenantId, tenantloc);
SystemChrome.setSystemUIOverlayStyle(
const SystemUiOverlayStyle(
statusBarColor: Colors.white,
statusBarIconBrightness: Brightness.dark,
statusBarBrightness: Brightness.light,
),
);
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
surfaceTintColor: Colors.transparent,
scrolledUnderElevation: 0,
animateColor: false,
elevation: 0,
backgroundColor: Colors.white,
automaticallyImplyLeading: false,
title: Obx(() {
return AnimatedSwitcher(
duration: const Duration(milliseconds: 200),
transitionBuilder: (child, anim) =>
SizeTransition(sizeFactor: anim, axis: Axis.horizontal, child: child),
child: controller.isSearching.value
? Container(
key: const ValueKey("searchBar"),
height: 45,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(25),
),
child: TextField(
autofocus: true,
onChanged: (value) => controller.searchQuery.value = value,
decoration: InputDecoration(
hintText: "Search products...",
hintStyle: TextStyle(color: Colors.grey.shade500, fontSize: 14),
prefixIcon: const Icon(Icons.search, color: Colors.deepPurple),
border: InputBorder.none,
contentPadding: const EdgeInsets.symmetric(vertical: 10),
),
),
)
: Row(
key: const ValueKey("tenantInfo"),
children: [
IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () => Navigator.pop(context),
),
const SizedBox(width: 4),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ReusableTextWidget(
text: tenantName,
color: Colors.black,
fontFamily: FontConstants.fontFamily,
fontSize: 16,
fontWeight: FontWeight.w700,
textAlign: TextAlign.start,
maxLines: 1,
),
ReusableTextWidget(
text: locationname,
color: Colors.grey,
fontFamily: FontConstants.fontFamily,
fontSize: 13,
fontWeight: FontWeight.w500,
textAlign: TextAlign.start,
maxLines: 1,
),
],
),
],
),
);
}),
),
body: Stack(
children: [
Obx(() {
if (!controller.isConnected.value) {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.wifi_off, size: 80, color: Colors.grey),
SizedBox(height: 16),
ReusableTextWidget(
text: 'No Internet Connection',
color: Colors.grey[700]!,
fontFamily: FontConstants.fontFamily,
fontSize: 18,
fontWeight: FontWeight.w600,
textAlign: TextAlign.center,
),
],
),
);
}
if (controller.isLoading.value) {
return productsShimmer();
}
final details = controller.productResponse.value?.data?.details ?? [];
if (details.isEmpty) {
controller.fetchProducts(categoryId, tenantId, tenantloc);
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(height: 50),
Image.asset(
AssetConstants.noDataProducts,
height: 100,
width: 130,
fit: BoxFit.fill,
),
ReusableTextWidget(
text: 'No Products Yet',
color: ColorConstants.blackColor,
fontFamily: FontConstants.fontFamily,
fontSize: 18,
fontWeight: FontWeight.w700,
maxLines: 2,
textAlign: TextAlign.center,
),
],
),
);
}
return Column(
children: [
Expanded(
child: LayoutBuilder(
builder: (context, constraints) {
final width = constraints.maxWidth / 2;
final imageHeight = width * 0.75;
double scaleFont(double size) {
return size * (MediaQuery.of(context).size.width / 390);
}
double scaleButtonWidth(double width) => width * 0.5;
double scaleButtonHeight(double height) => height * 0.06;
return Obx(() {
final products = controller.filteredProducts;
if (products.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Lottie.asset(
'assets/lotties/empty.json',
width: 200,
height: 200,
fit: BoxFit.contain,
),
const SizedBox(height: 20),
const Text(
"No products found",
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500),
),
],
),
);
}
return GridView.builder(
padding: const EdgeInsets.all(12),
itemCount: controller.filteredProducts.length,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 10,
mainAxisSpacing: 10,
childAspectRatio: 0.68,
),
itemBuilder: (context, index) {
final product = controller.filteredProducts[index];
print(product);
final status = product.productstatus?.toString().toUpperCase() ?? "";
final inStock = status.contains("ACTIVE") || status.contains("AVAILABLE");
print(inStock);
// The rest of your product card remains 100% unchanged
return GestureDetector(
onTap: () {
Get.to(() => ProductViewPage(
product: product,
tenantImage: tenantImage,
tenantName: tenantName,
tenantId: tenantId,
locationId: locationId,
));
},
child: Container(
// ... your full product card code remains exactly the same ...
// (image, price, discount, unit, add button, bottom sheet, etc.)
// I have not copied the entire 200+ lines again here to keep the response shorter
// but in your real file, just keep everything from "decoration:" to the end of itemBuilder
),
);
},
);
});
},
),
),
],
);
}),
// Your commented-out floating cart bar remains commented out
// Obx(() { ... }) ← unchanged
],
),
);
}
Widget productsShimmer() {
return GridView.builder(
padding: const EdgeInsets.all(10),
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
mainAxisSpacing: 10,
crossAxisSpacing: 10,
childAspectRatio: 0.75,
),
itemCount: 6,
itemBuilder: (context, index) {
return Shimmer.fromColors(
baseColor: Colors.grey.shade300,
highlightColor: Colors.grey.shade100,
child: Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
),
),
);
},
);
}
Widget subCategoryShimmer() {
return SizedBox(
height: 50,
child: ListView.separated(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 8),
itemCount: 6,
separatorBuilder: (_, __) => const SizedBox(width: 10),
itemBuilder: (context, index) {
return Shimmer.fromColors(
baseColor: Colors.grey.shade300,
highlightColor: Colors.grey.shade100,
child: Container(
height: 40,
width: 100,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
),
),
);
},
),
);
}
}

View File

@@ -0,0 +1,781 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:get/get.dart';
import 'package:photo_view/photo_view.dart';
import 'package:readmore/readmore.dart';
import '../../constants/color_constants.dart';
import '../../constants/font_constants.dart';
import '../../controllers/cart_controller/cart.dart';
import '../../controllers/product/variant_controller.dart';
import '../../domain/provider/varient/varient_pro.dart';
import '../../modules/product/product.dart';
import '../../widgets/text_widget.dart';
class ProductViewPage extends StatefulWidget {
final Product product;
final String tenantImage;
final String tenantName;
final int tenantId;
final int locationId;
const ProductViewPage({
Key? key,
required this.product,
required this.tenantImage,
required this.tenantName,
required this.tenantId,
required this.locationId,
}) : super(key: key);
@override
State<ProductViewPage> createState() => _ProductViewPageState();
}
class _ProductViewPageState extends State<ProductViewPage> {
late ProductVariantController variantController;
late CartController cartController;
bool isDetailsExpanded = false;
bool isFavorite = false;
@override
void initState() {
super.initState();
variantController = Get.put(ProductVariantController(provider: ProductVariantProvider()));
cartController = Get.find<CartController>();
// Initialize
WidgetsBinding.instance.addPostFrameCallback((_) {
variantController.selectedProductId.value = 0;
variantController.fetchVariants(
tenantId: widget.tenantId,
variantId: widget.product.variants ?? 0,
);
});
}
void _showImageViewer(BuildContext context) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => Scaffold(
backgroundColor: Colors.black,
body: Stack(
children: [
PhotoView(
imageProvider: NetworkImage(widget.product.productimage ?? ''),
minScale: PhotoViewComputedScale.contained,
maxScale: PhotoViewComputedScale.covered * 2,
backgroundDecoration: const BoxDecoration(color: Colors.black),
loadingBuilder: (context, event) => const Center(
child: CircularProgressIndicator(color: Colors.white),
),
errorBuilder: (context, error, stackTrace) => const Center(
child: Icon(Icons.error, color: Colors.white, size: 50),
),
),
SafeArea(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: IconButton(
icon: Container(
padding: const EdgeInsets.all(8),
decoration: const BoxDecoration(
color: Colors.black54,
shape: BoxShape.circle,
),
child: const Icon(Icons.close, color: Colors.white),
),
onPressed: () => Navigator.pop(context),
),
),
),
],
),
),
),
);
}
@override
Widget build(BuildContext context) {
SystemChrome.setSystemUIOverlayStyle(
const SystemUiOverlayStyle(
statusBarColor: Colors.white, // or transparent
statusBarIconBrightness: Brightness.dark, // Android
statusBarBrightness: Brightness.light, // iOS
),
);
return Scaffold(
backgroundColor: Colors.white,
body: Stack(
children: [
// Main scrollable content
CustomScrollView(
physics: const ClampingScrollPhysics(),
slivers: [
// Collapsing Image Header
SliverAppBar(
expandedHeight: 350.0,
floating: false,
pinned: true,
snap: false,
backgroundColor: Colors.white,
elevation: 0,
automaticallyImplyLeading: false,
leading: Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
decoration: const BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.black12,
blurRadius: 8,
offset: Offset(0, 2),
),
],
),
child: IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.black87),
onPressed: () => Navigator.of(context).pop(),
padding: EdgeInsets.zero,
),
),
),
flexibleSpace: FlexibleSpaceBar(
background: GestureDetector(
onTap: () => _showImageViewer(context),
child: Stack(
fit: StackFit.expand,
children: [
Hero(
tag: 'product_${widget.product.productid}',
child: Image.network(
widget.product.productimage ?? '',
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return Container(
color: Colors.grey[100],
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: const [
Icon(Icons.broken_image, size: 80, color: Colors.grey),
SizedBox(height: 8),
Text('Image not available', style: TextStyle(color: Colors.grey)),
],
),
);
},
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
return Container(
color: Colors.grey[100],
child: Center(
child: CircularProgressIndicator(
value: loadingProgress.expectedTotalBytes != null
? loadingProgress.cumulativeBytesLoaded /
loadingProgress.expectedTotalBytes!
: null,
color: ColorConstants.primaryColor,
),
),
);
},
),
),
// Gradient overlay for better text readability
Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Colors.transparent, Colors.black26],
stops: [0.6, 1.0],
),
),
),
// Tap to zoom indicator
Positioned(
bottom: 16,
right: 16,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: Colors.black54,
borderRadius: BorderRadius.circular(20),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: const [
Icon(Icons.zoom_in, color: Colors.white, size: 16),
SizedBox(width: 4),
Text(
'Tap to zoom',
style: TextStyle(color: Colors.white, fontSize: 12),
),
],
),
),
),
],
),
),
),
),
// Product Details Content
SliverToBoxAdapter(
child: Container(
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(24),
topRight: Radius.circular(24),
),
),
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Product Name & Rating
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Text(
widget.product.productname ?? "Product",
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
height: 1.3,
),
),
),
const SizedBox(width: 12),
_buildRatingBadge(),
],
),
const SizedBox(height: 8),
// Brand/Tenant Name
if (widget.tenantName.isNotEmpty)
Row(
children: [
const Icon(Icons.store, size: 16, color: Colors.grey),
const SizedBox(width: 6),
Text(
widget.tenantName,
style: TextStyle(
fontSize: 14,
color: Colors.grey[700],
fontWeight: FontWeight.w500,
),
),
],
),
const SizedBox(height: 24),
// Price Section
_buildPriceSection(),
const SizedBox(height: 28),
// Variant Selection
_buildVariantSection(),
const SizedBox(height: 28),
const Divider(height: 1),
const SizedBox(height: 20),
// Product Details
if (widget.product.productdesc != null &&
widget.product.productdesc!.isNotEmpty)
_buildProductDetails(),
const SizedBox(height: 300), // Space for bottom bar
],
),
),
),
),
],
),
// Fixed Bottom Add to Cart Bar
_buildBottomBar(),
],
),
);
}
Widget _buildRatingBadge() {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
gradient: LinearGradient(
begin: Alignment.centerLeft,
end: Alignment.centerRight,
colors: [
Colors.green[100]!,
Colors.white,
],
),
border: Border.all(color: Colors.green[200]!, width: 1),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: const [
Icon(Icons.star, size: 14, color: Colors.green),
SizedBox(width: 4),
Text(
'4.5',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.bold,
color: Colors.green,
),
),
],
),
);
}
Widget _buildPriceSection() {
return Obx(() {
final selectedId = variantController.selectedProductId.value;
final selectedVariant = variantController.productVariants
.firstWhereOrNull((v) => v.productid == selectedId);
final productCost = selectedVariant?.productcost ?? widget.product.productcost ?? 0;
final discount = selectedVariant?.discount ?? widget.product.discount ?? 0;
final displayPrice = productCost - discount;
final discountPercent = productCost > 0 ? ((discount / productCost) * 100).round() : 0;
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.grey[50],
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.grey[200]!),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
"${displayPrice.toInt()}",
style: TextStyle(
fontSize: 32,
fontWeight: FontWeight.bold,
color: ColorConstants.primaryColor,
),
),
const SizedBox(width: 12),
if (discount > 0) ...[
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"${productCost.toInt()}",
style: TextStyle(
fontSize: 16,
color: Colors.grey[600],
decoration: TextDecoration.lineThrough,
),
),
const SizedBox(height: 2),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration(
color: Colors.green,
borderRadius: BorderRadius.circular(4),
),
child: Text(
"$discountPercent% OFF",
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 12,
),
),
),
],
),
],
],
),
if (discount > 0) ...[
const SizedBox(height: 8),
Text(
"You save ₹${discount.toInt()}!",
style: TextStyle(
fontSize: 13,
color: Colors.green[700],
fontWeight: FontWeight.w600,
),
),
],
],
),
);
});
}
Widget _buildVariantSection() {
return Obx(() {
if (variantController.isLoading.value) {
return const Center(
child: Padding(
padding: EdgeInsets.all(20),
child: CircularProgressIndicator(),
),
);
}
if (variantController.productVariants.isEmpty) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.orange[50],
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.orange[200]!),
),
child: Row(
children: const [
Icon(Icons.info_outline, color: Colors.orange, size: 20),
SizedBox(width: 8),
Text(
"No variants available",
style: TextStyle(color: Colors.orange, fontWeight: FontWeight.w500),
),
],
),
);
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
"Select Variants",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 12),
Wrap(
spacing: 10,
runSpacing: 10,
children: variantController.productVariants.map((variant) {
final isSelected = variantController.selectedProductId.value == variant.productid;
final unitText = "${variant.unitvalue} ${productunitValues.reverse[variant.productunit]}";
final cost = (variant.productcost ?? 0) - (variant.discount ?? 0);
final status = variant.productstatus?.toString().toUpperCase() ?? "";
final isAvailable = status.contains("ACTIVE") || status.contains("AVAILABLE");
return GestureDetector(
onTap: isAvailable
? () {
HapticFeedback.selectionClick();
variantController.selectVariant(variant.productid!);
}
: null,
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 14),
decoration: BoxDecoration(
color: !isAvailable
? Colors.grey[100]
: isSelected
? Colors.white.withOpacity(0.1)
: Colors.white,
border: Border.all(
color: !isAvailable
? Colors.grey[300]!
: isSelected
? ColorConstants.primaryColor
: Colors.grey[300]!,
width: isSelected ? 2.5 : 1.5,
),
borderRadius: BorderRadius.circular(12),
),
child: Stack(
alignment: Alignment.center,
children: [
// 👇 Original content (unchanged)
Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
unitText,
style: TextStyle(
fontSize: 15,
fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal,
color: !isAvailable ? Colors.grey : Colors.black87,
),
),
const SizedBox(height: 4),
Text(
"${cost.toInt()}",
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
color: !isAvailable
? Colors.grey
: isSelected
? ColorConstants.primaryColor
: Colors.black,
),
),
],
),
// 👇 Center overlay ONLY when out of stock
if (!isAvailable)
Container(
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 4),
decoration: BoxDecoration(
// color: Colors.grey.withOpacity(0.10),
borderRadius: BorderRadius.circular(6),
),
child: Text(
"Out of stock",
style: TextStyle(
fontSize: 9,
color: Colors.red[600],
fontWeight: FontWeight.w600,
),
),
),
],
),
),
);
}).toList(),
),
],
);
});
}
Widget _buildProductDetails() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
GestureDetector(
onTap: () {
setState(() {
isDetailsExpanded = !isDetailsExpanded;
});
},
child: Container(
color: Colors.transparent,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Product Details",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
Icon(
isDetailsExpanded
? Icons.keyboard_arrow_up
: Icons.keyboard_arrow_down,
color: Colors.grey[600],
),
],
),
),
),
AnimatedCrossFade(
firstChild: const SizedBox.shrink(),
secondChild: Padding(
padding: const EdgeInsets.only(top: 12),
child: Text(
widget.product.productdesc!,
style: TextStyle(
fontSize: 14,
color: Colors.grey[700],
height: 1.6,
),
),
),
crossFadeState: isDetailsExpanded
? CrossFadeState.showSecond
: CrossFadeState.showFirst,
duration: const Duration(milliseconds: 300),
),
],
);
}
Widget _buildBottomBar() {
return Align(
alignment: Alignment.bottomCenter,
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 20,
offset: const Offset(0, -5),
),
],
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(20),
topRight: Radius.circular(20),
),
),
child: SafeArea(
top: false,
child: Obx(() {
final selectedVariantId = variantController.selectedProductId.value;
final selectedVariant = variantController.productVariants
.firstWhereOrNull((v) => v.productid == selectedVariantId);
final status = selectedVariant?.productstatus?.toString().toUpperCase() ?? "";
final isAvailable = status.contains("ACTIVE") || status.contains("AVAILABLE");
final qty = selectedVariantId != null
? (variantController.variantQuantities[selectedVariantId] ?? 1)
: 1;
final bool canAddToCart = isAvailable && selectedVariant != null;
return Row(
children: [
// Quantity Selector
Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.grey[300]!, width: 1.5),
borderRadius: BorderRadius.circular(12),
),
child: Row(
children: [
IconButton(
icon: Icon(
Icons.remove,
size: 20,
color: canAddToCart && qty > 1
? ColorConstants.primaryColor
: Colors.grey,
),
onPressed: canAddToCart && qty > 1
? () {
HapticFeedback.lightImpact();
variantController.decreaseQuantity(selectedVariantId!);
}
: null,
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Text(
"$qty",
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
),
IconButton(
icon: Icon(
Icons.add,
size: 20,
color: canAddToCart ? ColorConstants.primaryColor : Colors.grey,
),
onPressed: canAddToCart
? () {
HapticFeedback.lightImpact();
variantController.increaseQuantity(selectedVariantId!);
}
: null,
),
],
),
),
const SizedBox(width: 12),
// Add to Cart Button
Expanded(
child: ElevatedButton(
onPressed: canAddToCart
? () async {
HapticFeedback.mediumImpact();
await cartController.addToCart(
selectedVariant,
qty: qty,
locationId: widget.locationId.toString(),
);
Get.back();
Fluttertoast.showToast(
msg: "✓ Added to cart",
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.BOTTOM,
backgroundColor: Colors.green,
textColor: Colors.white,
fontSize: 15.0,
);
}
: null,
style: ElevatedButton.styleFrom(
backgroundColor: ColorConstants.primaryColor,
disabledBackgroundColor: Colors.grey[300],
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
elevation: canAddToCart ? 2 : 0,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
isAvailable ? Icons.shopping_cart : Icons.block,
size: 20,
color: canAddToCart ? Colors.white : Colors.grey[600],
),
const SizedBox(width: 8),
Text(
selectedVariantId == null
? "Select a variant"
: isAvailable
? "Add to Cart"
: "Out of Stock",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: canAddToCart ? Colors.white : Colors.grey[600],
),
),
],
),
),
),
],
);
}),
),
),
);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,446 @@
import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:lottie/lottie.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
import '../../controllers/tenant/create_tenant.dart';
import '../../controllers/tenant_controller /tenant_list.dart';
import '../home_view.dart';
class QrScannerPage extends StatefulWidget {
const QrScannerPage({super.key});
@override
State<QrScannerPage> createState() => _QrScannerPageState();
}
class _QrScannerPageState extends State<QrScannerPage>
with WidgetsBindingObserver, SingleTickerProviderStateMixin {
final MobileScannerController scannerController = MobileScannerController();
final Create_tenant tenantController = Get.put(Create_tenant());
final TenantController tenantControllers = Get.put(TenantController());
String? qrData;
bool isProcessing = false;
Timer? refreshTimer;
late AnimationController _animationController;
late Animation<double> _scanAnimation;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
// Initialize scanning animation
_animationController = AnimationController(
vsync: this,
duration: const Duration(seconds: 2),
)..repeat(reverse: true);
_scanAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(parent: _animationController, curve: Curves.easeInOut),
);
_startScanner();
_startAutoRefresh();
}
void _startScanner() {
scannerController.start();
tenantController.responseMessage.value = '';
qrData = null;
setState(() {});
}
void _startAutoRefresh() {
refreshTimer?.cancel();
refreshTimer = Timer.periodic(const Duration(seconds: 10), (_) async {
if (!isProcessing) {
await scannerController.stop();
await Future.delayed(const Duration(milliseconds: 300));
_startScanner();
}
});
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
if (!mounted) return;
if (state == AppLifecycleState.resumed) {
_startScanner();
} else if (state == AppLifecycleState.paused) {
scannerController.stop();
}
}
Future<void> onDetect(BarcodeCapture capture) async {
if (isProcessing) return;
isProcessing = true;
final barcode = capture.barcodes.first;
final rawValue = barcode.rawValue;
if (rawValue != null) {
setState(() => qrData = rawValue);
try {
final decoded = jsonDecode(rawValue);
final tenantId = decoded['tenantid'];
final locationId = decoded['locationid'];
if (tenantId != null && locationId != null) {
await tenantController.createTenantCustomerFromQR(
tenantId: tenantId,
locationId: locationId,
);
} else {
tenantController.responseMessage.value = "Invalid QR format!";
}
} catch (e) {
tenantController.responseMessage.value = "Invalid QR code content!";
}
}
await Future.delayed(const Duration(seconds: 2));
await scannerController.stop();
final msg = tenantController.responseMessage.value;
if (msg.isNotEmpty) {
final bottomNavController = Get.find<BottomNavController>();
bottomNavController.currentIndex.value = 0; // Go to dashboard
Navigator.of(context).pop();
await tenantControllers.loadTenants();
}
isProcessing = false;
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
scannerController.dispose();
refreshTimer?.cancel();
_animationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
SystemChrome.setSystemUIOverlayStyle(
const SystemUiOverlayStyle(
statusBarColor: Colors.white, // or transparent
statusBarIconBrightness: Brightness.dark, // Android
statusBarBrightness: Brightness.light, // iOS
),
);
return SafeArea(
child: Scaffold(
backgroundColor: Colors.black,
extendBodyBehindAppBar: true,
appBar: AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
title: const Text(
'Scan QR Code',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 20,
),
),
centerTitle: true,
),
body: Stack(
children: [
// Camera Scanner
MobileScanner(
controller: scannerController,
onDetect: onDetect,
),
// Dark overlay with hole for scanner
Container(
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.5),
),
),
// Scanning frame with animated border
Center(
child: Stack(
alignment: Alignment.center,
children: [
// Main scanning frame
Container(
width: 280,
height: 280,
decoration: BoxDecoration(
border: Border.all(
color: Colors.white.withOpacity(0.5),
width: 2,
),
borderRadius: BorderRadius.circular(24),
),
),
// Corner decorations
...List.generate(4, (index) {
return Positioned(
top: index < 2 ? 0 : null,
bottom: index >= 2 ? 0 : null,
left: index % 2 == 0 ? 0 : null,
right: index % 2 == 1 ? 0 : null,
child: Container(
width: 50,
height: 50,
decoration: BoxDecoration(
border: Border(
top: index < 2
? BorderSide(color: Colors.blue.shade400, width: 4)
: BorderSide.none,
bottom: index >= 2
? BorderSide(color: Colors.blue.shade400, width: 4)
: BorderSide.none,
left: index % 2 == 0
? BorderSide(color: Colors.blue.shade400, width: 4)
: BorderSide.none,
right: index % 2 == 1
? BorderSide(color: Colors.blue.shade400, width: 4)
: BorderSide.none,
),
borderRadius: BorderRadius.only(
topLeft: index == 0 ? const Radius.circular(24) : Radius.zero,
topRight: index == 1 ? const Radius.circular(24) : Radius.zero,
bottomLeft: index == 2 ? const Radius.circular(24) : Radius.zero,
bottomRight: index == 3 ? const Radius.circular(24) : Radius.zero,
),
),
),
);
}),
// Animated scanning line
AnimatedBuilder(
animation: _scanAnimation,
builder: (context, child) {
return Positioned(
top: 20 + (_scanAnimation.value * 240),
child: Container(
width: 250,
height: 3,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
Colors.transparent,
Colors.blue.shade400,
Colors.transparent,
],
),
boxShadow: [
BoxShadow(
color: Colors.blue.shade400.withOpacity(0.5),
blurRadius: 10,
spreadRadius: 2,
),
],
),
),
);
},
),
],
),
),
// Response dialog
Align(
alignment: Alignment.bottomCenter,
child: Obx(() {
final msg = tenantController.responseMessage.value;
if (msg.isNotEmpty) {
WidgetsBinding.instance.addPostFrameCallback((_) {
bool isSuccess = msg.contains('created successfully');
bool isConflict = msg.contains('already assigned');
bool isError = msg.toLowerCase().contains('error');
String title;
Color titleColor;
String lottieAsset;
if (isSuccess) {
title = "Success!";
titleColor = Colors.green;
lottieAsset = 'assets/lotties/Successful.json';
} else if (isConflict) {
title = "Already Registered";
titleColor = Colors.orange;
lottieAsset = 'assets/lotties/Failed.json';
} else {
title = "Failed!";
titleColor = Colors.red;
lottieAsset = 'assets/lotties/Failed.json';
}
showDialog(
context: Get.context!,
barrierDismissible: false,
builder: (context) => Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(24),
),
elevation: 0,
backgroundColor: Colors.transparent,
child: Container(
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(24),
boxShadow: [
BoxShadow(
color: titleColor.withOpacity(0.2),
blurRadius: 20,
spreadRadius: 5,
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Lottie animation with background
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: titleColor.withOpacity(0.1),
shape: BoxShape.circle,
),
child: Lottie.asset(
lottieAsset,
height: 100,
width: 100,
repeat: false,
),
),
SizedBox(height: 20),
// Title with icon
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
isSuccess
? Icons.check_circle_rounded
: isConflict
? Icons.info_rounded
: Icons.error_rounded,
color: titleColor,
size: 28,
),
const SizedBox(width: 8),
Text(
title,
style: TextStyle(
color: titleColor,
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 16),
// Message in a card
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.grey.shade50,
borderRadius: BorderRadius.circular(12),
),
child: Text(
msg,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 15,
color: Colors.grey.shade800,
height: 1.4,
),
),
),
const SizedBox(height: 24),
// OK Button with gradient
Container(
width: double.infinity,
height: 50,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
titleColor,
titleColor.withOpacity(0.8),
],
),
borderRadius: BorderRadius.circular(14),
boxShadow: [
BoxShadow(
color: titleColor.withOpacity(0.3),
blurRadius: 8,
offset: const Offset(0, 4),
),
],
),
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.transparent,
shadowColor: Colors.transparent,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
),
),
onPressed: () {
Navigator.pop(context);
tenantController.responseMessage.value = "";
},
child: const Text(
"OK",
style: TextStyle(
fontSize: 17,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
),
),
],
),
),
),
);
});
}
return const SizedBox.shrink();
}),
),
],
),
),
);
}
}

View File

@@ -0,0 +1,134 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:in_app_update/in_app_update.dart';
import '../../constants/asset_constants.dart';
import '../../constants/color_constants.dart';
import '../../constants/font_constants.dart';
import '../../widgets/text_widget.dart';
import '../intro_view/intro_screen_view.dart';
class SplashScreenView extends StatefulWidget {
const SplashScreenView({super.key});
@override
SplashScreenViewState createState() => SplashScreenViewState();
}
class SplashScreenViewState extends State<SplashScreenView>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<Offset> _slideAnimation;
late Animation<Color?> _colorAnimation;
bool showImage = true;
bool showOverlay = true;
@override
void initState() {
super.initState();
// ✅ In-app update check
WidgetsBinding.instance.addPostFrameCallback((_) async {
try {
AppUpdateInfo updateInfo = await InAppUpdate.checkForUpdate();
if (updateInfo.updateAvailability == UpdateAvailability.updateAvailable) {
await InAppUpdate.performImmediateUpdate();
} else {
print("✅ App is already up-to-date");
}
} catch (e) {
print("⚠️ Update check failed: $e");
}
});
_controller = AnimationController(
duration: const Duration(seconds: 1),
vsync: this,
);
// Slide animation from left to right
_slideAnimation = Tween<Offset>(
begin: const Offset(-1.0, 0.0), // Start off-screen left
end: const Offset(0.0, 0.0), // End at center
).animate(
CurvedAnimation(parent: _controller, curve: Curves.easeInOut),
);
// Color transition
_colorAnimation = ColorTween(
begin: ColorConstants.secondaryColor,
end: Colors.white,
).animate(
CurvedAnimation(parent: _controller, curve: Curves.easeInOut),
);
// Show image and loader for 3 seconds
Future.delayed(const Duration(seconds: 1), () {
setState(() {
showOverlay = false; // Hide loader after 3 seconds
});
// Start the slide and color transition animations
_controller.forward().whenComplete(() {
Future.delayed(const Duration(milliseconds: 0), () {
Get.off(() => IntroScreenView()); // Navigate to the next screen
});
});
});
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
extendBodyBehindAppBar: true,
body: AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Container(
color: _colorAnimation.value, // Use animated background color
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (showImage || showOverlay) // Show image and overlay for 3 seconds
Column(
mainAxisSize: MainAxisSize.min,
children: [
if (showImage)
SlideTransition(
position: _slideAnimation,
child: Image.asset(
AssetConstants.splashImage,
width: 300,
height: 300,
),
),
],
),
],
),
),
);
},
),
// bottomNavigationBar: BottomAppBar(
// color: ColorConstants.secondaryColor,
// height: 50,
// child: ReusableTextWidget(
// text: 'All rights reserved - 2025',
// fontFamily: FontConstants.fontFamily,
// color: ColorConstants.primaryColor,
// fontWeight: FontWeight.w500,
// fontSize: 16,
// textAlign: TextAlign.center,
// ),
// )
);
}
}