first commit
This commit is contained in:
854
lib/view/account/account_view.dart
Normal file
854
lib/view/account/account_view.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
0
lib/view/account/demo.dart
Normal file
0
lib/view/account/demo.dart
Normal file
805
lib/view/account/edit_profile_view.dart
Normal file
805
lib/view/account/edit_profile_view.dart
Normal 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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
43
lib/view/account/faq_view.dart
Normal file
43
lib/view/account/faq_view.dart
Normal 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()),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
328
lib/view/account/help/create_request.dart
Normal file
328
lib/view/account/help/create_request.dart
Normal 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),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
230
lib/view/account/help/request_page.dart
Normal file
230
lib/view/account/help/request_page.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
265
lib/view/account/notification_settings_view.dart
Normal file
265
lib/view/account/notification_settings_view.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
162
lib/view/account/product.dart
Normal file
162
lib/view/account/product.dart
Normal 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 ?? [];
|
||||
}
|
||||
}
|
||||
379
lib/view/account/share_app.dart
Normal file
379
lib/view/account/share_app.dart
Normal 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,
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
25
lib/view/account/test.dart
Normal file
25
lib/view/account/test.dart
Normal 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(
|
||||
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
152
lib/view/authentication/app_update_view.dart
Normal file
152
lib/view/authentication/app_update_view.dart
Normal 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(
|
||||
// "We’ve 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,
|
||||
// ),
|
||||
// ],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
591
lib/view/authentication/costomer_create_view.dart
Normal file
591
lib/view/authentication/costomer_create_view.dart
Normal 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),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
443
lib/view/authentication/login_view.dart
Normal file
443
lib/view/authentication/login_view.dart
Normal 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),
|
||||
);
|
||||
}
|
||||
}
|
||||
69
lib/view/authentication/map_view.dart
Normal file
69
lib/view/authentication/map_view.dart
Normal 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;
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
331
lib/view/authentication/verification_view.dart
Normal file
331
lib/view/authentication/verification_view.dart
Normal 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(
|
||||
"Didn’t 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
1870
lib/view/cart/cart_view.dart
Normal file
File diff suppressed because it is too large
Load Diff
282
lib/view/cart/order_countdown_page.dart
Normal file
282
lib/view/cart/order_countdown_page.dart
Normal 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),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
1837
lib/view/dashboard_view/dashboard_view.dart
Normal file
1837
lib/view/dashboard_view/dashboard_view.dart
Normal file
File diff suppressed because it is too large
Load Diff
611
lib/view/dashboard_view/searchScreen.dart
Normal file
611
lib/view/dashboard_view/searchScreen.dart
Normal 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),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
596
lib/view/dashboard_view/tenant_profile.dart
Normal file
596
lib/view/dashboard_view/tenant_profile.dart
Normal 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
443
lib/view/home_view.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
272
lib/view/intro_view/intro_screen_view.dart
Normal file
272
lib/view/intro_view/intro_screen_view.dart
Normal 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;
|
||||
}
|
||||
873
lib/view/map_view/location.dart
Normal file
873
lib/view/map_view/location.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
882
lib/view/orders/my_orders.dart
Normal file
882
lib/view/orders/my_orders.dart
Normal 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,
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
266
lib/view/orders/order_succes.dart
Normal file
266
lib/view/orders/order_succes.dart
Normal 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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
744
lib/view/orders/orders_by_tenant.dart
Normal file
744
lib/view/orders/orders_by_tenant.dart
Normal 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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
339
lib/view/product/category_products.dart
Normal file
339
lib/view/product/category_products.dart
Normal 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),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
781
lib/view/product/product_view.dart
Normal file
781
lib/view/product/product_view.dart
Normal 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],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
1267
lib/view/product/tenant_products.dart
Normal file
1267
lib/view/product/tenant_products.dart
Normal file
File diff suppressed because it is too large
Load Diff
446
lib/view/qr_scaner/qr_scaner.dart
Normal file
446
lib/view/qr_scaner/qr_scaner.dart
Normal 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();
|
||||
}),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
134
lib/view/splash_view/splash_view.dart
Normal file
134
lib/view/splash_view/splash_view.dart
Normal 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,
|
||||
// ),
|
||||
// )
|
||||
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user