first commit
This commit is contained in:
163
lib/constants/api_constants.dart
Normal file
163
lib/constants/api_constants.dart
Normal file
@@ -0,0 +1,163 @@
|
||||
class ApiConstants {
|
||||
|
||||
///Flavours Route
|
||||
static String mainDev = "dev";
|
||||
static String mainRoute = "live";
|
||||
|
||||
///Base Url
|
||||
static String baseUrl = 'https://jupiter.nearle.app';
|
||||
static String baseUrl1 = 'https://fiesta.nearle.app';
|
||||
|
||||
|
||||
static String workolikBase = "https://api.workolik.com/api/rest";
|
||||
|
||||
|
||||
///Authentication
|
||||
static String login='';
|
||||
static String createCustomer='';
|
||||
|
||||
///Products
|
||||
|
||||
|
||||
///Get Product Category
|
||||
static String getProductCategory='';
|
||||
static String getAllProductByCategory='';
|
||||
static String getProductsBySubcategory='';
|
||||
static String getProductVarient = '';
|
||||
|
||||
///Customers
|
||||
static String getCustomerInfo = '';
|
||||
static String customerUpdate = '';
|
||||
// static String getCustomerLocations = '';
|
||||
static String createCustomerLocations = '';
|
||||
|
||||
/// tenants
|
||||
static String tenantCustomers = '';
|
||||
static String orderedtenantCustomers = '';
|
||||
|
||||
///Admin
|
||||
static String getAppLocationDetails='';
|
||||
static String notifyAdmin='' ;
|
||||
|
||||
/// Tenants
|
||||
static String createCustomerTenant ='';
|
||||
|
||||
///Orders
|
||||
static String createOrder = '';
|
||||
static String getOrder = '';
|
||||
|
||||
/// Locations
|
||||
static String getServiceLocations = '';
|
||||
|
||||
static String getSlotTiming = '';
|
||||
|
||||
/// Help and support
|
||||
static String createSupportAndSupport ='';
|
||||
|
||||
static String supportTypes = '';
|
||||
|
||||
static String getRequestedSupport = '';
|
||||
|
||||
|
||||
|
||||
/// Customer locations
|
||||
|
||||
static String getCustomerLocations = "$workolikBase/getcustomerlocations";
|
||||
|
||||
/// getproducts
|
||||
|
||||
static String getCustomerOrders = "$workolikBase/getcustomerorders";
|
||||
|
||||
|
||||
/// fetchprofile
|
||||
|
||||
static String fetchProfile = "$workolikBase/getbyid?";
|
||||
|
||||
static String updateCustomer = "$baseUrl1/live/api/v1/mob/customers/update";
|
||||
|
||||
|
||||
|
||||
/// Appcategorys
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
///Authentication
|
||||
// static String loginDev="$baseUrl/$mainDev/api/v1";
|
||||
// static String loginLive="$baseUrl/$mainRoute/api/v1";
|
||||
|
||||
static String loginDev = "$baseUrl1/$mainDev/api/v1/mob/customers/login";
|
||||
static String loginLive = "$baseUrl1/$mainRoute/api/v1/mob/customers/login";
|
||||
|
||||
|
||||
///Create Customer
|
||||
static String createCustomerLive="https://fiesta.nearle.app/live/api/v1/mob/customers/create";
|
||||
|
||||
/// Products Varient
|
||||
static String getProductVarientLive = "$baseUrl/$mainRoute/api/v1/products/getproductbyvariant";
|
||||
|
||||
///Get Product Category
|
||||
static String getProductCategoryDev="$baseUrl/$mainDev/api/v1/products/getproductsubcategories";
|
||||
static String getProductCategoryLive="$baseUrl/$mainRoute/api/v1/products/getproductsubcategories";
|
||||
|
||||
///Get All Product By Category
|
||||
static String getAllProductByCategoryDev="$baseUrl/$mainDev/api/v1/products/getallproducts";
|
||||
static String getAllProductByCategoryLive="$baseUrl/$mainRoute/api/v1/products/getallproducts";
|
||||
|
||||
///GetProducts By Subcategory
|
||||
static String getProductsBySubcategoryDev="$baseUrl/$mainDev/api/v1/products/getproductsbysubcategory";
|
||||
static String getProductsBySubcategoryLive="$baseUrl/$mainRoute/api/v1/products/getproductsbysubcategory";
|
||||
|
||||
///Customers
|
||||
static String getCustomerInfoDev = "$baseUrl/$mainDev/api/v1/customers/getbyid";
|
||||
static String getCustomerInfoLive = "$baseUrl/$mainRoute/api/v1/customers/getbyid";
|
||||
|
||||
static String customerUpdateDev = '$baseUrl/$mainDev/api/v1/customers/update';
|
||||
static String customerUpdateLive = '$baseUrl/$mainRoute/api/v1/customers/update';
|
||||
|
||||
///Customer Locations
|
||||
// static String getCustomerLocationsDev = "$baseUrl/$mainDev/api/v1/customers/getcustomerlocation";
|
||||
// static String getCustomerLocationsLive = "$baseUrl/$mainRoute/api/v1/customers/getcustomerlocation";
|
||||
|
||||
static String createCustomerLocationsDev = "$baseUrl/$mainRoute/api/v1/customers/createlocations";
|
||||
static String createCustomerLocationsLive = "$baseUrl/$mainRoute/api/v1/customers/createlocations";
|
||||
|
||||
|
||||
///Orders
|
||||
static String createOrderDev = "$baseUrl/$mainDev/api/v1/mob/orders/createorder";
|
||||
static String createOrderLive = "$baseUrl/$mainRoute/api/v1/mob/orders/createorder";
|
||||
|
||||
static String getOrderDev = "$baseUrl/$mainDev/api/v3/orders/getcustomerorders";
|
||||
static String getOrderLive = "$baseUrl/$mainRoute/api/v3/orders/getcustomerorders";
|
||||
|
||||
|
||||
///Admin
|
||||
static String getAppLocationDetailsDev="$baseUrl/$mainDev/api/v1/utils/getapplocationconfig";
|
||||
static String getAppLocationDetailsLive="$baseUrl/$mainRoute/api/v1/utils/getapplocationconfig";
|
||||
|
||||
|
||||
static String notifyAdminLive="https://jupiter.nearle.app/live/api/v1/utils/notifytenant";
|
||||
|
||||
|
||||
static String serviceLocationLive = '${baseUrl}/$mainRoute/api/v1/utils/getapplocations';
|
||||
|
||||
static String getSlotTimingLive = "$baseUrl/$mainRoute/api/v1/tenants/gettenantslots";
|
||||
|
||||
static String createTenantLive = "$baseUrl/$mainRoute/api/v1/tenants/createtenantcustomer";
|
||||
|
||||
static String tenantCustomerLive = "$baseUrl1/$mainRoute/api/v1/mob/tenants/getcustomertenants";
|
||||
static String orderedtenantCustomerLive = "$baseUrl1/$mainRoute/api/v1/mob/orders/getcustomerorders";
|
||||
|
||||
static String createSupportAndSupportLive = "$baseUrl/$mainRoute/api/v1/customers/createcustomerrequest";
|
||||
|
||||
static String supportTypesLive = "$baseUrl/$mainRoute/api/v1/utils/getapptypes/?tag=customersupport";
|
||||
|
||||
static String getRequestedSupportLive = "$baseUrl/$mainRoute/api/v1/customers/getcustomerrequests";
|
||||
|
||||
//?customerid=1001&pageno=1&pagesize=1
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
84
lib/constants/asset_constants.dart
Normal file
84
lib/constants/asset_constants.dart
Normal file
@@ -0,0 +1,84 @@
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class AssetConstants {
|
||||
static const String splashImage = "assets/images/nearleDailyLogo.png";
|
||||
static const String dailyLogo = "assets/images/dailyLogo.png";
|
||||
static const String intro1 = "assets/images/intro1.png";
|
||||
static const String intro2 = "assets/images/intro2.png";
|
||||
static const String intro3 = "assets/images/intro3.png";
|
||||
static const String loginImage = "assets/images/loginImage.png";
|
||||
static const String locationRequestImage = "assets/images/location.png";
|
||||
static const String notificationImage = "assets/images/notification.png";
|
||||
static const String recentWatchIcon = "assets/images/recentIcon.png";
|
||||
|
||||
static const String banner = "assets/images/banner.png";
|
||||
|
||||
|
||||
static const String logo = "assets/images/nearledash3.png";
|
||||
|
||||
|
||||
|
||||
///Account
|
||||
static const String yourOrderImage = "assets/images/yourOrderImage.png";
|
||||
static const String helpSupport = "assets/images/helpSupport.png";
|
||||
static const String walletImage = "assets/images/walletImage.png";
|
||||
|
||||
|
||||
static const String profileIcon = "assets/images/profileIcon.png";
|
||||
static const String favouriteIcon = "assets/images/favouriteIcon.png";
|
||||
static const String addressIcon = "assets/images/addressIcon.png";
|
||||
static const String notificationIcon = "assets/images/notificationIcon.png";
|
||||
static const String faqIcon = "assets/images/faqIcon.png";
|
||||
static const String playStoreRatingIcon = "assets/images/playStoreRatingIcon.png";
|
||||
|
||||
static const String payOnDelivery = "assets/images/payOnDelivery.png";
|
||||
|
||||
///No data
|
||||
static const String noDataProducts = "assets/images/noDataProducts.png";
|
||||
static const String noDataCart = "assets/images/noDataCart.png";
|
||||
static const String noRecords = "assets/images/noRecords.png";
|
||||
static const String noOrders = "assets/images/noOrders.png";
|
||||
|
||||
|
||||
///Orders
|
||||
static const String personIcon = "assets/images/personIcon.png";
|
||||
static const String callIcon = "assets/images/callIcon.png";
|
||||
static const String orderCreateIcon = "assets/images/orderCreateIcon.png";
|
||||
static const String orderDeliveryIcon = "assets/images/orderDeliveryIcon.png";
|
||||
static const String orderOnTheWayIcon = "assets/images/orderOnTheWayIcon.png";
|
||||
static const String orderPrepareIcon = "assets/images/orderPrepareIcon.png";
|
||||
|
||||
|
||||
/// Intro
|
||||
static const String introNew_1 = "assets/images/intro_1.png";
|
||||
static const String introNew_2 = "assets/images/intro_2.png";
|
||||
|
||||
|
||||
static const String productActive = "assets/images/fruit.png";
|
||||
static const String productInactive = "assets/images/fruits_inactive.png";
|
||||
|
||||
|
||||
static const String whiteBackground = "assets/images/white_backrgoudn.png";
|
||||
|
||||
static const String scanIcon = "assets/images/Scan_icon.png";
|
||||
|
||||
static const String banner_1 = "assets/images/Banner_1.png";
|
||||
|
||||
static const String banner_2 = "assets/images/Banner_2.png";
|
||||
|
||||
static const String nearToYou = "assets/images/near_to_you.png";
|
||||
|
||||
static const String nearleCopyright = "assets/images/nearle_copyrights.png";
|
||||
|
||||
static const String storyline1 = "assets/images/storyline_1.png";
|
||||
|
||||
static const String storyline2 = "assets/images/storyline_2.png";
|
||||
|
||||
static const String storyline3 = "assets/images/storyline_3.png";
|
||||
|
||||
static const String fssaiLogo = "assets/images/Fssai-Logo-Vector.png";
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
40
lib/constants/color_constants.dart
Normal file
40
lib/constants/color_constants.dart
Normal file
@@ -0,0 +1,40 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
|
||||
class ColorConstants{
|
||||
static const primaryColor = Color(0xFF662582);
|
||||
static const primaryColor1 = Color(0xFFE7D3EF);
|
||||
static const darkGreyColor = Color(0xFF575756);
|
||||
static const lightGrey = Color(0xFFb2b2b2);
|
||||
static const greyBottom = Color(0xFFD9D9D9);
|
||||
static const greenColor = Color(0xFF00b894);
|
||||
static const secondaryColor = Colors.white;
|
||||
static const blackColor = Colors.black;
|
||||
static const lightBlackColor = Colors.black54;
|
||||
static const grey = Colors.grey;
|
||||
static const lightGreyBg= Color(0xF5F5F5FF);
|
||||
static const ratingColor= Color(0xFF2D781E);
|
||||
|
||||
static final List<Color> bgColors = [
|
||||
Colors.white,
|
||||
Colors.blue.shade50,
|
||||
Colors.green.shade50,
|
||||
Colors.orange.shade50,
|
||||
Colors.purple.shade50,
|
||||
Colors.red.shade50,
|
||||
Colors.yellow.shade50,
|
||||
Colors.cyan.shade50,
|
||||
Colors.pink.shade50,
|
||||
Colors.teal.shade50,
|
||||
Colors.lime.shade50,
|
||||
Colors.indigo.shade50,
|
||||
Colors.amber.shade50,
|
||||
Colors.deepOrange.shade50,
|
||||
Colors.lightBlue.shade50,
|
||||
];
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
7
lib/constants/error_constants.dart
Normal file
7
lib/constants/error_constants.dart
Normal file
@@ -0,0 +1,7 @@
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class ErrorConstants{
|
||||
|
||||
static RxBool apiError = false.obs;
|
||||
|
||||
}
|
||||
67
lib/constants/font_constants.dart
Normal file
67
lib/constants/font_constants.dart
Normal file
@@ -0,0 +1,67 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
|
||||
class FontConstants {
|
||||
static const String fontFamily = 'Proxima Nova';
|
||||
}
|
||||
|
||||
|
||||
class ZoomIconButton extends StatefulWidget {
|
||||
final VoidCallback onTap;
|
||||
final IconData icon;
|
||||
final double size;
|
||||
final Color color;
|
||||
|
||||
const ZoomIconButton({
|
||||
Key? key,
|
||||
required this.onTap,
|
||||
this.icon = Icons.more_vert,
|
||||
this.size = 24,
|
||||
this.color = Colors.black,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<ZoomIconButton> createState() => _ZoomIconButtonState();
|
||||
}
|
||||
|
||||
class _ZoomIconButtonState extends State<ZoomIconButton> {
|
||||
double _scale = 1.0;
|
||||
|
||||
void _onTapDown(TapDownDetails details) {
|
||||
setState(() {
|
||||
_scale = 1.2; // zoom in
|
||||
});
|
||||
}
|
||||
|
||||
void _onTapUp(TapUpDetails details) {
|
||||
setState(() {
|
||||
_scale = 1.0; // zoom back
|
||||
});
|
||||
widget.onTap(); // trigger your action
|
||||
}
|
||||
|
||||
void _onTapCancel() {
|
||||
setState(() {
|
||||
_scale = 1.0; // reset if cancelled
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTapDown: _onTapDown,
|
||||
onTapUp: _onTapUp,
|
||||
onTapCancel: _onTapCancel,
|
||||
child: AnimatedScale(
|
||||
scale: _scale,
|
||||
duration: const Duration(milliseconds: 100),
|
||||
curve: Curves.easeInCirc,
|
||||
child: Icon(
|
||||
widget.icon,
|
||||
size: widget.size,
|
||||
color: widget.color,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
47
lib/controllers/account_controller/faq_controller.dart
Normal file
47
lib/controllers/account_controller/faq_controller.dart
Normal file
@@ -0,0 +1,47 @@
|
||||
import 'dart:ui';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:webview_flutter/webview_flutter.dart';
|
||||
|
||||
class FaqController extends GetxController {
|
||||
WebViewController? webViewController;
|
||||
var isLoading = true.obs;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
initializeWebView();
|
||||
}
|
||||
|
||||
void initializeWebView() {
|
||||
webViewController = WebViewController()
|
||||
..setJavaScriptMode(JavaScriptMode.unrestricted)
|
||||
..setBackgroundColor(const Color(0x00000000))
|
||||
..setNavigationDelegate(
|
||||
NavigationDelegate(
|
||||
onPageStarted: (url) {
|
||||
isLoading.value = true;
|
||||
print('Started loading: $url');
|
||||
},
|
||||
onPageFinished: (url) {
|
||||
isLoading.value = false;
|
||||
print('Finished loading: $url');
|
||||
},
|
||||
onWebResourceError: (error) {
|
||||
isLoading.value = false;
|
||||
print('WebView error: ${error.description}');
|
||||
},
|
||||
),
|
||||
);
|
||||
loadFaqUrl();
|
||||
}
|
||||
|
||||
Future<void> loadFaqUrl() async {
|
||||
if (webViewController != null) {
|
||||
try {
|
||||
await webViewController!.loadRequest(Uri.parse('https://nearle.in/faq'));
|
||||
} catch (e) {
|
||||
print('Error loading URL: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
135
lib/controllers/account_controller/profile.dart
Normal file
135
lib/controllers/account_controller/profile.dart
Normal file
@@ -0,0 +1,135 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:in_app_review/in_app_review.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import '../../domain/repository/authentication/auth_repository.dart';
|
||||
|
||||
class AccountController extends GetxController {
|
||||
var isLoading = true.obs;
|
||||
var profileName = ''.obs;
|
||||
var profileImage = ''.obs;
|
||||
var profilePhone = ''.obs;
|
||||
var appVersion = ''.obs;
|
||||
|
||||
final Dio _dio = Dio();
|
||||
var Name = ''.obs;
|
||||
var Adress = ''.obs;
|
||||
var Profile = ''.obs;
|
||||
var Number = ''.obs;
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
fetchProfileFromAPI();
|
||||
loadAppVersion();
|
||||
loadUserDetails();
|
||||
_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;
|
||||
}
|
||||
|
||||
final repo = LoginRepository();
|
||||
final fetchedProfile = await repo.fetchProfile(id.toString());
|
||||
|
||||
if (fetchedProfile != null) {
|
||||
|
||||
Name.value = fetchedProfile.firstname ?? '';
|
||||
Profile.value = fetchedProfile.profileimage ?? '';
|
||||
Number.value = fetchedProfile.contactno ?? '';
|
||||
|
||||
Adress.value = fetchedProfile.suburb ?? '';
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// Load from SharedPreferences (e.g., on app start)
|
||||
Future<void> loadUserDetails() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
|
||||
profileName.value = prefs.getString('customerFirstname') ?? '';
|
||||
profileImage.value = prefs.getString('customerProfile') ?? '';
|
||||
profilePhone.value = prefs.getString('contactno') ?? '';
|
||||
|
||||
}
|
||||
|
||||
/// 🔹 Fetch user details from API
|
||||
Future<void> fetchProfileFromAPI() async {
|
||||
try {
|
||||
isLoading(true);
|
||||
|
||||
// If you store the customerId in SharedPreferences, load it like this:
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
int? customerId = prefs.getInt('customerId') ?? 0; // fallback
|
||||
|
||||
final String url = "https://fiesta.nearle.app/live/api/v1/mob/customers/getbyid/?customerid=$customerId";
|
||||
|
||||
final response = await _dio.get(url);
|
||||
|
||||
if (response.statusCode == 200 && response.data['status'] == true) {
|
||||
final data = response.data['details'];
|
||||
print(customerId);
|
||||
print(data);
|
||||
print('rrr');
|
||||
|
||||
// Update observables
|
||||
// profileName.value = data['firstname'] ?? 'Guest';
|
||||
// profileImage.value = data['profileimage'] ??
|
||||
// 'https://i.pravatar.cc/150?img=12';
|
||||
// profilePhone.value =
|
||||
// "${data['dialcode'] ?? ''} ${data['contactno'] ?? ''}";
|
||||
|
||||
// Optionally, save to SharedPreferences for later offline use
|
||||
prefs.setString('customerFirstname', data['firstname'] ?? '');
|
||||
prefs.setString('customerProfile', data['profileimage'] ?? '');
|
||||
prefs.setString('contactno', data['contactno'] ?? '');
|
||||
} else {
|
||||
Get.snackbar('Error', 'Failed to fetch profile data');
|
||||
}
|
||||
} catch (e) {
|
||||
Get.snackbar('Error', 'Something went wrong: $e');
|
||||
} finally {
|
||||
isLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// 🔹 Get App Version
|
||||
Future<void> loadAppVersion() async {
|
||||
PackageInfo info = await PackageInfo.fromPlatform();
|
||||
appVersion.value = '${info.version}+${info.buildNumber}';
|
||||
}
|
||||
|
||||
Future<void> rateApp() async {
|
||||
final inAppReview = InAppReview.instance;
|
||||
|
||||
try {
|
||||
if (await inAppReview.isAvailable()) {
|
||||
await inAppReview.requestReview();
|
||||
// Popup MAY or MAY NOT show — Google's decision
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
// ALWAYS open Play Store page (recommended for testing)
|
||||
_openStorePage();
|
||||
}
|
||||
|
||||
void _openStorePage() {
|
||||
const packageName = "com.nearle.gear";
|
||||
final url = Uri.parse(
|
||||
"https://play.google.com/store/apps/details?id=$packageName");
|
||||
|
||||
launchUrl(url, mode: LaunchMode.externalApplication);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
297
lib/controllers/authentication/auth_controller.dart
Normal file
297
lib/controllers/authentication/auth_controller.dart
Normal file
@@ -0,0 +1,297 @@
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:sms_autofill/sms_autofill.dart';
|
||||
|
||||
import '../../Helper/Logger.dart';
|
||||
import '../../constants/error_constants.dart';
|
||||
import '../../data/authentication/auth_request.dart';
|
||||
import '../../data/authentication/auth_response.dart';
|
||||
import '../../domain/repository/authentication/auth_repository.dart';
|
||||
import '../../view/authentication/costomer_create_view.dart';
|
||||
import '../../view/authentication/verification_view.dart';
|
||||
import '../../view/dashboard_view/dashboard_view.dart';
|
||||
import '../../view/home_view.dart';
|
||||
import '../tenant_controller /tenant_list.dart';
|
||||
|
||||
class AuthController extends GetxController {
|
||||
LoginRepository loginRepository = LoginRepository();
|
||||
final TenantController tenantControllers = Get.put(TenantController());
|
||||
|
||||
var isLoading = false.obs;
|
||||
|
||||
int? activeStatus;
|
||||
int authMode = 0;
|
||||
int a = 1;
|
||||
String? customerToken;
|
||||
String? customerContactNo;
|
||||
String? contactLength;
|
||||
int? customerId;
|
||||
int? auth;
|
||||
bool? logInStatus;
|
||||
bool? isNewUser;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// Dio instance for SMS API requests
|
||||
final dio1 = Dio(BaseOptions(
|
||||
connectTimeout: const Duration(seconds: 30),
|
||||
receiveTimeout: const Duration(seconds: 60),
|
||||
));
|
||||
|
||||
// Sign-in method to initiate login
|
||||
signIn(BuildContext context, String phone) async {
|
||||
customerContactNo =phone;
|
||||
if (phone.isEmpty) {
|
||||
Get.snackbar("Error", "Enter a valid phone number");
|
||||
return;
|
||||
}
|
||||
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
|
||||
// Retrieve FCM token saved in main()
|
||||
String? fcmToken = prefs.getString('fcmToken') ?? '';
|
||||
String deviceId = prefs.getString('currentDeviceId') ?? '';
|
||||
String deviceType = Platform.isAndroid ? "android" : "ios";
|
||||
isLoading.value = true; // Start loading
|
||||
print("=== Login API Payload ===");
|
||||
print("Phone: $phone");
|
||||
print("FCM Token: $fcmToken");
|
||||
print("Device ID: $deviceId");
|
||||
print("Device Type: $deviceType");
|
||||
print("=========================");
|
||||
|
||||
await loginApi(
|
||||
LoginRequest(
|
||||
contactno: phone, // Pass entered number
|
||||
configid: 2,
|
||||
devicetype: deviceType,
|
||||
customertoken: fcmToken,
|
||||
deviceid: deviceId,
|
||||
),
|
||||
context,
|
||||
);
|
||||
|
||||
isLoading.value = false; // Stop loading
|
||||
}
|
||||
|
||||
// Login API call to authenticate user
|
||||
loginApi(LoginRequest data, BuildContext context) async {
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
String? deviceId = prefs.getString('currentDeviceId');
|
||||
LoginResponse? result = await loginRepository.signIn(data);
|
||||
if (result?.status == true) {
|
||||
activeStatus = result?.details?.status;
|
||||
logger.i('activeStatusLoginApi $activeStatus');
|
||||
customerId = int.parse(result?.details?.customerid ?? '');
|
||||
logInStatus = result?.status;
|
||||
authMode = result?.details?.authmode ?? 0;
|
||||
customerToken = result?.details?.customertoken ?? '';
|
||||
logger.i('CustomerId login: $customerId');
|
||||
|
||||
// Store user details in SharedPreferences
|
||||
prefs.setInt('customerId', int.parse(result?.details?.customerid ?? ''));
|
||||
prefs.setString('customerFirstname', result?.details?.firstname ?? '');
|
||||
prefs.setString('customerProfile', result?.details?.profileimage ?? '');
|
||||
prefs.setString('customerLastname', result?.details?.lastname ?? '');
|
||||
prefs.setString('dialCode', result?.details?.dialcode ?? '');
|
||||
prefs.setString('customerEmail', result?.details?.email ?? '');
|
||||
prefs.setString('watchedIntro', result?.details?.intro ?? '');
|
||||
prefs.setString('deviceId', result?.details?.deviceid ?? '');
|
||||
prefs.setString('deviceType', result?.details?.devicetype ?? '');
|
||||
prefs.setString('contactno', result?.details?.contactno ?? '');
|
||||
prefs.setInt('authmode', result?.details?.authmode ?? 0);
|
||||
prefs.setInt('configId', result?.details?.configid ?? 0);
|
||||
prefs.setString('customerAddress', result?.details?.address ?? '');
|
||||
prefs.setString('customerSuburb', result?.details?.suburb ?? '');
|
||||
prefs.setString('customerState', result?.details?.state ?? '');
|
||||
prefs.setString('customerCity', result?.details?.city ?? '');
|
||||
prefs.setString('customerLandmark', result?.details?.landmark ?? '');
|
||||
prefs.setString('customerDoorNo', result?.details?.doorno ?? '');
|
||||
prefs.setString('customerPostcode', result?.details?.postcode ?? '');
|
||||
prefs.setString('customerLatitude', result?.details?.latitude ?? '');
|
||||
prefs.setString('customerLongitude', result?.details?.longitude ?? '');
|
||||
prefs.setInt('appLocationId', result?.details?.applocationid ?? 0);
|
||||
prefs.setInt('tenantid', result?.details?.tenantid ?? 0);
|
||||
prefs.setBool('skipUserLogIn', false);
|
||||
prefs.setInt('locationId', result?.details?.locationid ?? 0);
|
||||
|
||||
logger.i('Get locationID: ${prefs.getInt('locationId')}');
|
||||
logger.i('Tenant id from login: ${result?.details?.tenantid}');
|
||||
|
||||
ErrorConstants.apiError.value = false;
|
||||
|
||||
isNewUser=false;
|
||||
|
||||
validateDevice(deviceId ?? '');
|
||||
} else {
|
||||
if (customerContactNo == '7397177923') {
|
||||
Get.to(() => DashboardPage());
|
||||
}
|
||||
|
||||
if (result?.status == false) {
|
||||
// ErrorConstants.apiError.value = true;
|
||||
update();
|
||||
isNewUser = true;
|
||||
Get.to(() => VerificationUiPage(
|
||||
phoneNumber: customerContactNo!, // actual number
|
||||
isNewUser: true, // false = existing user, true = new user
|
||||
));
|
||||
await receiveSmsOtp();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate device ID to determine navigation
|
||||
void validateDevice(String currentDeviceId) async {
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
final storedDeviceId = prefs.getString('deviceId');
|
||||
logger.i('Comparing device IDs: current=$currentDeviceId, stored=$storedDeviceId');
|
||||
|
||||
if(authMode ==1){
|
||||
|
||||
logger.i('got it');
|
||||
Get.to(() => VerificationUiPage(
|
||||
phoneNumber: customerContactNo!, // actual number
|
||||
isNewUser: false, // false = existing user, true = new user
|
||||
));
|
||||
}else{
|
||||
if (currentDeviceId.isNotEmpty && currentDeviceId == storedDeviceId) {
|
||||
|
||||
Get.offAll(() => BottomNavigation());
|
||||
await tenantControllers.loadTenants();
|
||||
} else {
|
||||
Get.to(() => VerificationUiPage(
|
||||
phoneNumber: customerContactNo!, // actual number
|
||||
isNewUser: false, // false = existing user, true = new user
|
||||
));
|
||||
await receiveSmsOtp();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// Validate the entered OTP
|
||||
// auth_controller.dart
|
||||
Future<void> validateOtp(String enteredOtp, BuildContext context, [bool? forceIsNewUser]) async {
|
||||
// Use passed value as override if controller state is unreliable
|
||||
final bool effectiveIsNewUser = forceIsNewUser ?? isNewUser ?? false;
|
||||
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
final savedOtp = prefs.getString('otp');
|
||||
|
||||
if (authMode == 1 && enteredOtp.trim() == '123456') {
|
||||
Get.offAll(() => BottomNavigation());
|
||||
await tenantControllers.loadTenants();
|
||||
return;
|
||||
}
|
||||
|
||||
if (savedOtp == null) {
|
||||
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,
|
||||
);
|
||||
Get.snackbar('Error', 'No OTP found. Please request a new one.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (enteredOtp.trim() == savedOtp.trim()) {
|
||||
await prefs.setBool('isOtpVerified', true);
|
||||
|
||||
if (effectiveIsNewUser) { // ✅ Uses reliable value
|
||||
Get.to(() => CustomerCreateView(mobileNumber: customerContactNo!));
|
||||
} else {
|
||||
Get.offAll(() => BottomNavigation());
|
||||
await tenantControllers.loadTenants();
|
||||
}
|
||||
} else {
|
||||
Fluttertoast.showToast(
|
||||
msg: "Please enter the correct OTP and try again.",
|
||||
toastLength: Toast.LENGTH_SHORT,
|
||||
gravity: ToastGravity.TOP,
|
||||
backgroundColor: Colors.red.withOpacity(0.8),
|
||||
textColor: Colors.white,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Send OTP via SMS
|
||||
Future<void> receiveSmsOtp() async {
|
||||
final appSignature = await SmsAutoFill().getAppSignature;
|
||||
final otp = (100000 + (Random().nextInt(900000))).toString(); // Generate random 6-digit OTP
|
||||
logger.i('Generated OTP: $otp');
|
||||
logger.i('app sign : $appSignature');
|
||||
|
||||
// Initialize SmsAutoFill to listen for incoming SMS
|
||||
await SmsAutoFill().listenForCode();
|
||||
|
||||
final message = "<#> Dear customer, use OTP $otp to sign in to Nearle App.\n$appSignature";
|
||||
final encodedMessage = Uri.encodeComponent(message);
|
||||
|
||||
// Use environment variable or secure storage for API key
|
||||
const smsApiKey = 'e57f5c9679af26077be1a7eadabb1b2a'; // Consider moving to secure config
|
||||
final url = 'https://msg.lionsms.com/api/smsapi?'
|
||||
'key=$smsApiKey'
|
||||
'&route=7'
|
||||
'&sender=NEARLE'
|
||||
'&number=$customerContactNo' // Dynamic phone number
|
||||
'&sms=$encodedMessage' // Full message including OTP
|
||||
'&templateid=1107174712357438611';
|
||||
|
||||
logger.i('urlsendOtp: $url');
|
||||
logger.i('appSignature: $appSignature');
|
||||
|
||||
try {
|
||||
final response = await dio1.get(url);
|
||||
logger.i('SMS API response: ${response.data}');
|
||||
if (response.statusCode == 200) {
|
||||
logger.i('SMS sent successfully');
|
||||
Fluttertoast.showToast(
|
||||
msg: "SMS sent successfully",
|
||||
toastLength: Toast.LENGTH_SHORT,
|
||||
gravity: ToastGravity.TOP,
|
||||
backgroundColor: Colors.green.withOpacity(0.8),
|
||||
textColor: Colors.white,
|
||||
fontSize: 15,
|
||||
);
|
||||
// Store OTP for verification
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString('otp', otp);
|
||||
} else {
|
||||
logger.i('Failed to send SMS: ${response.data}');
|
||||
Fluttertoast.showToast(
|
||||
msg: "Failed to send OTP. Please try again.",
|
||||
toastLength: Toast.LENGTH_SHORT,
|
||||
gravity: ToastGravity.TOP,
|
||||
backgroundColor: Colors.red.withOpacity(0.8),
|
||||
textColor: Colors.white,
|
||||
fontSize: 15,
|
||||
);
|
||||
|
||||
}
|
||||
} catch (e) {
|
||||
logger.i('Error sending SMS: $e');
|
||||
Fluttertoast.showToast(
|
||||
msg: "something went wrong",
|
||||
toastLength: Toast.LENGTH_SHORT,
|
||||
gravity: ToastGravity.TOP,
|
||||
backgroundColor: Colors.black.withOpacity(0.8),
|
||||
textColor: Colors.white,
|
||||
fontSize: 15,
|
||||
);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
0
lib/controllers/authentication/location.dart
Normal file
0
lib/controllers/authentication/location.dart
Normal file
0
lib/controllers/authentication/otp_controller.dart
Normal file
0
lib/controllers/authentication/otp_controller.dart
Normal file
628
lib/controllers/cart_controller/cart.dart
Normal file
628
lib/controllers/cart_controller/cart.dart
Normal file
@@ -0,0 +1,628 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:http/http.dart' as _dio;
|
||||
import 'package:lottie/lottie.dart';
|
||||
import 'package:nearledaily/constants/color_constants.dart';
|
||||
import '../../modules/authentication/auth.dart';
|
||||
import '../../modules/product/product.dart';
|
||||
import '../../modules/tenant/get_tenant.dart' hide Customer;
|
||||
import '../../service/dio.dart';
|
||||
import '../tenant_controller /tenant_list.dart'; // New Product modules
|
||||
|
||||
class CartController extends GetxController {
|
||||
var cartItems = <CartItem>[].obs;
|
||||
var pastTenantId;
|
||||
var pastLocationId;
|
||||
|
||||
var currentTenant = Rxn<Tenant>();
|
||||
final CustomDio _customDio = CustomDio(); // assuming your postData() is here
|
||||
RxBool showCouponAnimation = false.obs;
|
||||
final shake = ValueNotifier<bool>(false);
|
||||
|
||||
void triggerCouponAnimation() {
|
||||
showCouponAnimation.value = true;
|
||||
|
||||
Future.delayed(Duration(seconds: 2), () {
|
||||
showCouponAnimation.value = false;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
appliedCoupon.value = "";
|
||||
amt.value = "";
|
||||
}
|
||||
|
||||
RxList<Map<String, dynamic>> coupons = <Map<String, dynamic>>[].obs;
|
||||
RxString appliedCoupon = ''.obs;
|
||||
RxString amt = ''.obs;
|
||||
|
||||
void loadCoupons() async {
|
||||
try {
|
||||
isLoading.value = true;
|
||||
|
||||
final tenant = currentTenant.value;
|
||||
|
||||
final tenantid = tenant?.tenantid ?? '';
|
||||
final locationid = tenant?.locationid ?? 'Unknown Store';
|
||||
|
||||
final url =
|
||||
"https://jupiter.nearle.app/live/api/v1/tenants/gettenantpromotions?tenantid=$tenantid&locationid=$locationid";
|
||||
|
||||
final response = await http.get(Uri.parse(url));
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final data = jsonDecode(response.body);
|
||||
|
||||
print(url);
|
||||
final List details = data["details"] ?? [];
|
||||
|
||||
coupons.value = details.map((promo) {
|
||||
return {
|
||||
"code": promo["promocode"] ?? "",
|
||||
"desc": promo["description"] ?? "",
|
||||
"amount": promo["promoamount"]?.toString() ?? "0",
|
||||
"start": promo["startdate"] ?? "",
|
||||
"end": promo["enddate"] ?? "",
|
||||
};
|
||||
}).toList();
|
||||
}
|
||||
} catch (e) {
|
||||
print("loadCoupons Error: $e");
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
void applyCoupon(String code) {
|
||||
appliedCoupon.value = code;
|
||||
|
||||
|
||||
triggerCouponAnimation();
|
||||
}
|
||||
|
||||
void showCouponBottomSheet(BuildContext context) {
|
||||
final cartCtrl = Get.find<CartController>();
|
||||
|
||||
cartCtrl.appliedCoupon.value = "";
|
||||
cartCtrl.amt.value = "";
|
||||
cartCtrl.coupons.clear();
|
||||
|
||||
cartCtrl.loadCoupons(); // fetch coupons again
|
||||
|
||||
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (context) {
|
||||
return DraggableScrollableSheet(
|
||||
initialChildSize: 0.7,
|
||||
minChildSize: 0.4,
|
||||
maxChildSize: 0.95,
|
||||
builder: (_, controller) {
|
||||
return Container(
|
||||
padding: EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(12)),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
|
||||
|
||||
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
"Available Coupons",
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
IconButton(onPressed: (){
|
||||
Navigator.of(context).pop();
|
||||
// triggerCouponAnimation();
|
||||
}, icon: Icon(Icons.close))
|
||||
],
|
||||
),
|
||||
SizedBox(height: 15),
|
||||
|
||||
Expanded(
|
||||
child: Obx(() {
|
||||
if (cartCtrl.isLoading.value) {
|
||||
return Center(
|
||||
child: Lottie.asset(
|
||||
'assets/lotties/loading.json', // path to your Lottie JSON file
|
||||
width: 500,
|
||||
height: 500,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Filter only valid (non-expired) coupons
|
||||
final validCoupons = cartCtrl.coupons.where((item) {
|
||||
try {
|
||||
if (item["end"] == null || item["end"].toString().isEmpty) return true;
|
||||
DateTime endDate = DateTime.parse(item["end"]);
|
||||
DateTime today = DateTime.now();
|
||||
DateTime onlyToday = DateTime(today.year, today.month, today.day);
|
||||
DateTime onlyEnd = DateTime(endDate.year, endDate.month, endDate.day);
|
||||
return !onlyEnd.isBefore(onlyToday); // keep only if not expired
|
||||
} catch (e) {
|
||||
return false; // invalid date → skip
|
||||
}
|
||||
}).toList();
|
||||
|
||||
if (validCoupons.isEmpty) {
|
||||
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Lottie animation
|
||||
Lottie.asset(
|
||||
'assets/lotties/nodata.json', // Replace with your Lottie file path
|
||||
width: 150,
|
||||
height: 150,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
"No coupons available",
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return ListView.builder(
|
||||
controller: controller,
|
||||
itemCount: validCoupons.length,
|
||||
itemBuilder: (_, index) {
|
||||
final item = validCoupons[index];
|
||||
return couponTile(
|
||||
item["code"],
|
||||
item["desc"],
|
||||
item["amount"],
|
||||
start: item["start"],
|
||||
end: item["end"],
|
||||
);
|
||||
},
|
||||
);
|
||||
}),
|
||||
),
|
||||
|
||||
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget couponTile(String code, String desc, String amount, {String? start, String? end}) {
|
||||
final cartCtrl = Get.find<CartController>();
|
||||
|
||||
String formatDate(String iso) {
|
||||
try {
|
||||
DateTime dt = DateTime.parse(iso);
|
||||
return "${dt.day}-${dt.month}-${dt.year}";
|
||||
} catch (e) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
return Container(
|
||||
margin: EdgeInsets.only(bottom: 12),
|
||||
padding: EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Color(0xFF662582).withOpacity(0.06),
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(color: Color(0xFF662582).withOpacity(0.3)),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.03),
|
||||
blurRadius: 4,
|
||||
offset: Offset(0, 2),
|
||||
)
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Icon left
|
||||
Container(
|
||||
padding: EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: Color(0xFF662582).withOpacity(0.2),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(Icons.discount, size: 18, color: Color(0xFF662582)),
|
||||
),
|
||||
|
||||
SizedBox(width: 12),
|
||||
|
||||
// Text section
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
code,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Color(0xFF662582),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 4),
|
||||
|
||||
Text(
|
||||
desc,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.black87,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
|
||||
SizedBox(height: 6),
|
||||
|
||||
// Expiry date
|
||||
|
||||
Padding(
|
||||
padding: EdgeInsets.only(top: 4),
|
||||
child: Text(
|
||||
"Add items worth ₹100 to use this coupon",
|
||||
style: TextStyle(
|
||||
color: Colors.red,
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(width: 10),
|
||||
|
||||
// Apply Button
|
||||
Obx(() {
|
||||
double t = cartCtrl.totalCost;
|
||||
|
||||
bool isApplied =
|
||||
cartCtrl.cartItems.isNotEmpty &&
|
||||
t >= 100 &&
|
||||
cartCtrl.appliedCoupon.value == code;
|
||||
|
||||
|
||||
double totalAmount = cartCtrl.totalCost; // your total
|
||||
|
||||
return ValueListenableBuilder<bool>(
|
||||
valueListenable: shake,
|
||||
builder: (context, isShaking, child) {
|
||||
return AnimatedContainer(
|
||||
duration: Duration(milliseconds: 80),
|
||||
margin: EdgeInsets.only(left: isShaking ? 4 : 0, right: isShaking ? 4 : 0),
|
||||
child: Column(
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
if (totalAmount < 100) {
|
||||
// 🔥 Trigger shake animation only
|
||||
shake.value = true;
|
||||
Future.delayed(Duration(milliseconds: 300), () {
|
||||
shake.value = false;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Normal Apply / Remove logic
|
||||
if (isApplied) {
|
||||
cartCtrl.appliedCoupon.value = "";
|
||||
cartCtrl.amt.value = "";
|
||||
} else {
|
||||
cartCtrl.amt.value = amount;
|
||||
cartCtrl.appliedCoupon.value = code;
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: isApplied ? Colors.green : Color(0xFF662582),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
isApplied ? "Remove" : "Apply",
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// 🔥 Show small red error text (only when below ₹100)
|
||||
// if (totalAmount < 100)
|
||||
// Padding(
|
||||
// padding: EdgeInsets.only(top: 4),
|
||||
// child: Text(
|
||||
// "Min ₹100 required",
|
||||
// style: TextStyle(
|
||||
// color: Colors.red,
|
||||
// fontSize: 11,
|
||||
// fontWeight: FontWeight.w600,
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}),
|
||||
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
var isLoading = true.obs;
|
||||
var customer = Rxn<Customer>();
|
||||
final TenantController tenantController = Get.find<TenantController>();
|
||||
|
||||
Future<void> fetchCustomer(int customerId) async {
|
||||
isLoading.value = true;
|
||||
try {
|
||||
final url = Uri.parse(
|
||||
'https://fiesta.nearle.app/live/api/v1/mob/customers/getbyid/?customerid=$customerId');
|
||||
final response = await http.get(url);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final data = json.decode(response.body);
|
||||
if (data['status'] == true) {
|
||||
customer.value = Customer.fromJson(data['details']);
|
||||
}
|
||||
} else {
|
||||
print('Error: ${response.statusCode}');
|
||||
}
|
||||
} catch (e) {
|
||||
print('Exception: $e');
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 🔹 Notify Admin Function using your postData helper
|
||||
/// Notify Admin via API
|
||||
Future<void> notifyAdmin({
|
||||
String title = "Nearle deals",
|
||||
String body = "Test -------------------------------------------------",
|
||||
}) async {
|
||||
const String endpoint = "https://jupiter.nearle.app/live/api/v1/utils/notifyadmin";
|
||||
final token = currentTenant.value?.tenanttoken?.toString() ?? "";
|
||||
final Map<String, dynamic> payload = {
|
||||
"token": [
|
||||
token
|
||||
],
|
||||
"notification": {
|
||||
"title": title,
|
||||
"body": body,
|
||||
"sound": "ring",
|
||||
"type": "tojoin"
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
print("📡 Sending admin notification...");
|
||||
|
||||
final response = await _customDio.postData(endpoint, payload);
|
||||
print("📌 Tenant token: $token");
|
||||
print("📌 Notification title: $title");
|
||||
print("📌 Notification body: $body");
|
||||
print("✅ Admin notified successfully: $response");
|
||||
print("📌 Tenant token from tenantController: ${currentTenant.value?.tenanttoken}");
|
||||
} catch (e) {
|
||||
print("❌ Error notifying admin: $e");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
Future<void> addToCart(
|
||||
Product product, {
|
||||
int qty = 1,
|
||||
String? storeName,
|
||||
String? storeImage,
|
||||
String? locationId,
|
||||
}) async {
|
||||
|
||||
final currentTenantId = product.tenantid.toString();
|
||||
final previousTenantId = pastTenantId?.toString();
|
||||
|
||||
final currentLocationId = locationId?.toString();
|
||||
final previousLocationId = pastLocationId?.toString();
|
||||
|
||||
print("Adding product from store: $currentTenantId");
|
||||
print("Past Tenant ID: $previousTenantId");
|
||||
print("Current Location ID: $currentLocationId");
|
||||
print("Past Location ID: $previousLocationId");
|
||||
|
||||
final tenant = tenantController.tenants.firstWhereOrNull(
|
||||
(t) => t.tenantid == int.parse(currentTenantId)
|
||||
);
|
||||
|
||||
// First item → set tenant + location
|
||||
if (cartItems.isEmpty) {
|
||||
cartItems.add(CartItem(product: product, quantity: qty));
|
||||
pastTenantId = currentTenantId;
|
||||
pastLocationId = currentLocationId;
|
||||
currentTenant.value = tenant;
|
||||
|
||||
print("✅ Tenant set for cart: ${tenant?.tenantname}");
|
||||
return;
|
||||
}
|
||||
|
||||
// --------- MAIN CHECK (Tenant + Location Must Match) ----------
|
||||
if (previousTenantId == currentTenantId &&
|
||||
previousLocationId == currentLocationId) {
|
||||
|
||||
// Same tenant and same location → add item normally
|
||||
final index = cartItems.indexWhere(
|
||||
(item) => item.product.productid == product.productid);
|
||||
|
||||
if (index >= 0) {
|
||||
cartItems[index].quantity += qty;
|
||||
cartItems.refresh();
|
||||
} else {
|
||||
cartItems.add(CartItem(product: product, quantity: qty));
|
||||
}
|
||||
|
||||
} else {
|
||||
// -------- DIFFERENT TENANT or DIFFERENT LOCATION -----------
|
||||
// (Your Existing Replace Cart Logic stays SAME)
|
||||
|
||||
if (Get.isBottomSheetOpen!) Get.back();
|
||||
await Future.delayed(Duration(milliseconds: 100));
|
||||
|
||||
bool? replace = await Get.bottomSheet<bool>(
|
||||
SafeArea(
|
||||
child: Container(
|
||||
padding: EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text("Replace Cart?",
|
||||
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
|
||||
IconButton(
|
||||
onPressed: () => Get.back(),
|
||||
icon: Icon(Icons.close),
|
||||
)
|
||||
],
|
||||
),
|
||||
|
||||
SizedBox(height: 16),
|
||||
|
||||
Text(
|
||||
"Looks like your cart has items from another store or location. Replace them?",
|
||||
style: TextStyle(fontSize: 14, color: Colors.grey[700]),
|
||||
),
|
||||
|
||||
SizedBox(height: 24),
|
||||
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () => Get.back(result: false),
|
||||
child: Text("No")),
|
||||
SizedBox(width: 8),
|
||||
ElevatedButton(
|
||||
onPressed: () => Get.back(result: true),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: ColorConstants.primaryColor),
|
||||
child: Text("Yes", style: TextStyle(color: Colors.white)),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
isDismissible: false,
|
||||
enableDrag: false,
|
||||
);
|
||||
|
||||
if (replace == true) {
|
||||
cartItems.clear();
|
||||
cartItems.add(CartItem(product: product, quantity: qty));
|
||||
|
||||
pastTenantId = currentTenantId;
|
||||
pastLocationId = currentLocationId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
double get totalTax => cartItems.fold(
|
||||
0,
|
||||
(sum, item) => sum + ((item.product.taxamount ?? 0) * item.quantity),
|
||||
);
|
||||
|
||||
double get totalCostWithTax => cartItems.fold(
|
||||
0,
|
||||
(sum, item) =>
|
||||
sum + ((item.product.productcost ?? 0) + (item.product.taxamount ?? 0)) * item.quantity,
|
||||
);
|
||||
|
||||
/// Remove product from cart
|
||||
void removeFromCart(Product product) {
|
||||
cartItems.removeWhere((item) => item.product.productid == product.productid);
|
||||
}
|
||||
|
||||
void increaseQty(CartItem item) {
|
||||
item.quantity++;
|
||||
cartItems.refresh();
|
||||
}
|
||||
|
||||
void decreaseQty(CartItem item) {
|
||||
if (item.quantity > 1) {
|
||||
item.quantity--;
|
||||
} else {
|
||||
cartItems.remove(item);
|
||||
}
|
||||
cartItems.refresh();
|
||||
}
|
||||
|
||||
|
||||
/// Clear cart
|
||||
void clearCart() => cartItems.clear();
|
||||
|
||||
/// Total items in cart
|
||||
int get totalItems => cartItems.fold(0, (sum, item) => sum + item.quantity);
|
||||
|
||||
/// Total cost of items in cart
|
||||
double get totalCost =>
|
||||
cartItems.fold(0, (sum, item) => sum + ((item.product.productcost ?? 0) * item.quantity));
|
||||
}
|
||||
|
||||
/// Cart item class
|
||||
class CartItem {
|
||||
final Product product;
|
||||
int quantity;
|
||||
|
||||
CartItem({required this.product, this.quantity = 1});
|
||||
}
|
||||
|
||||
|
||||
52
lib/controllers/dashboard_controller/category.dart
Normal file
52
lib/controllers/dashboard_controller/category.dart
Normal file
@@ -0,0 +1,52 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||
import 'package:get/get_rx/src/rx_types/rx_types.dart';
|
||||
import 'package:get/get_state_manager/src/simple/get_controllers.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import '../../modules/tenant/category.dart';
|
||||
|
||||
class CategoryController extends GetxController {
|
||||
var categories = <Category>[].obs;
|
||||
|
||||
|
||||
var isLoading = false.obs;
|
||||
var selectedIndex = 0.obs;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
fetchCategories();
|
||||
super.onInit();
|
||||
}
|
||||
|
||||
void fetchCategories() async {
|
||||
try {
|
||||
isLoading(true);
|
||||
|
||||
final url = Uri.parse(
|
||||
'https://fiesta.nearle.app/live/api/v1/mob/utils/getappcategories');
|
||||
|
||||
final response = await http.get(url);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final data = jsonDecode(response.body);
|
||||
|
||||
categories.value = (data['details'] as List)
|
||||
.map((e) => Category.fromJson(e))
|
||||
.toList();
|
||||
|
||||
print(response.body);
|
||||
|
||||
}
|
||||
} catch (e) {
|
||||
print("Error: $e");
|
||||
} finally {
|
||||
isLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
void selectCategory(int index) {
|
||||
selectedIndex.value = index;
|
||||
}
|
||||
}
|
||||
748
lib/controllers/dashboard_controller/dashboard_controller.dart
Normal file
748
lib/controllers/dashboard_controller/dashboard_controller.dart
Normal file
@@ -0,0 +1,748 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:geocoding/geocoding.dart';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:lottie/lottie.dart';
|
||||
import 'package:nearledaily/constants/color_constants.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import '../../Helper/Logger.dart';
|
||||
import '../../constants/font_constants.dart';
|
||||
import '../../data/tenant/get_tenant_res.dart';
|
||||
import '../../domain/provider/authentication/location.dart';
|
||||
import '../../domain/provider/tenant/get_tenant_pro.dart';
|
||||
import '../../domain/repository/authentication/auth_repository.dart';
|
||||
import '../../domain/repository/tenant/get_tenant_repo.dart';
|
||||
import '../../modules/authentication/auth.dart';
|
||||
import '../../modules/tenant/category.dart';
|
||||
import '../../modules/tenant/get_tenant.dart';
|
||||
import '../../view/authentication/costomer_create_view.dart';
|
||||
import '../../widgets/text_widget.dart';
|
||||
import '../tenant_controller /tenant_list.dart';
|
||||
|
||||
class DashboardController extends GetxController {
|
||||
// Loading state
|
||||
var isLoading = true.obs;
|
||||
List<Authentication> fetchedLocations = [];
|
||||
var categories = <Category>[].obs;
|
||||
|
||||
|
||||
var selectedIndex = 0.obs;
|
||||
var show = true.obs;
|
||||
|
||||
|
||||
|
||||
void fetchCategories() async {
|
||||
try {
|
||||
isLoading(true);
|
||||
|
||||
final url = Uri.parse(
|
||||
'https://fiesta.nearle.app/live/api/v1/mob/utils/getappcategories');
|
||||
|
||||
final response = await http.get(url);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final data = jsonDecode(response.body);
|
||||
|
||||
categories.value = (data['details'] as List)
|
||||
.map((e) => Category.fromJson(e))
|
||||
.toList();
|
||||
|
||||
print(response.body);
|
||||
print('gtot');
|
||||
|
||||
}
|
||||
} catch (e) {
|
||||
print("Error: $e");
|
||||
} finally {
|
||||
isLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
void selectCategory(int index) {
|
||||
selectedIndex.value = index;
|
||||
}
|
||||
|
||||
|
||||
Future<void> checkMainFlag() async {
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
|
||||
bool firstTime = prefs.getBool("firstTime") ?? true;
|
||||
|
||||
if (firstTime) {
|
||||
show.value = true;
|
||||
|
||||
// Next time this becomes false
|
||||
prefs.setBool("firstTime", false);
|
||||
} else {
|
||||
show.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
void location(){
|
||||
_showLocationBottomSheet();
|
||||
}
|
||||
|
||||
// Carousel images
|
||||
var carouselImages = <String>[].obs;
|
||||
// List<Tenants> getAllTenants = [];
|
||||
// Grid items
|
||||
var gridItems = <Map<String, String>>[].obs;
|
||||
var currentAddress = ''.obs;
|
||||
final CustomerLocationProvider locationProvider = CustomerLocationProvider();
|
||||
final TenantController tenantController = Get.put(TenantController());
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
// loadTenants();
|
||||
_getAndUpdateCurrentLocation();
|
||||
_fetchLocations();
|
||||
checkMainFlag();
|
||||
print("🚀 DashboardController onInit() called");
|
||||
// getTenantCustomers();
|
||||
// Simulate data loading (e.g., from an API)
|
||||
Future.delayed(const Duration(seconds: 2), () {
|
||||
// Load carousel images
|
||||
carouselImages.addAll([
|
||||
'assets/Banner_1.png',
|
||||
'assets/Banner_2.png',
|
||||
]);
|
||||
|
||||
|
||||
|
||||
// Set loading to false after data is loaded
|
||||
isLoading.value = false;
|
||||
});
|
||||
fetchCategories();
|
||||
super.onInit();
|
||||
}
|
||||
Future<void> _fetchLocations() async {
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
final id = prefs.getInt('customerId');
|
||||
|
||||
try {
|
||||
final locations = await locationProvider.fetchCustomerLocations(id!);
|
||||
fetchedLocations = locations;
|
||||
print(locations);
|
||||
|
||||
} catch (e) {
|
||||
print('Error fetching locations: $e');
|
||||
} finally {
|
||||
|
||||
}
|
||||
}
|
||||
void loadTenants() async {
|
||||
isLoading.value = true;
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
int? id = prefs.getInt('customerId');
|
||||
|
||||
try {
|
||||
final response = await CustomerTenantsProvider().getCustomerTenants(id!, 1);
|
||||
|
||||
if (response != null && response.status == true && response.details != null) {
|
||||
populateGridFromTenants(response);
|
||||
} else {
|
||||
gridItems.clear();
|
||||
}
|
||||
} catch (e) {
|
||||
print("⛔ Error fetching tenants: $e");
|
||||
gridItems.clear();
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
void populateGridFromTenants(CustomerTenantsResponse response) {
|
||||
final tenants = response.details ?? [];
|
||||
gridItems.clear();
|
||||
|
||||
for (var tenant in tenants) {
|
||||
gridItems.add({
|
||||
'tenantid': (tenant.tenantid ?? 0).toString(),
|
||||
'title': tenant.tenantname ?? 'No Name',
|
||||
'address': tenant.address ?? '',
|
||||
'licenseno': tenant.licenseno ?? '',
|
||||
'primaryemail': tenant.primaryemail ?? '',
|
||||
'primarycontact': tenant.primarycontact ?? '',
|
||||
'pickuplocationid': (tenant.pickuplocationid ?? 0).toString(),
|
||||
'applocationid': (tenant.applocationid ?? 0).toString(),
|
||||
'suburb': tenant.suburb ?? '',
|
||||
'city': tenant.city ?? '',
|
||||
'latitude': tenant.latitude ?? '',
|
||||
'longitude': tenant.longitude ?? '',
|
||||
'postcode': tenant.postcode ?? '',
|
||||
'tenantimage': tenant.tenantimage != null && tenant.tenantimage!.isNotEmpty
|
||||
? tenant.tenantimage!
|
||||
: 'https://via.placeholder.com/150',
|
||||
'locationid': (tenant.locationid ?? 0).toString(),
|
||||
'locationname': tenant.locationname ?? '',
|
||||
'subcategoryid': (tenant.subcategoryid ?? 0).toString(),
|
||||
'categoryid': (tenant.categoryid ?? 0).toString(),
|
||||
'registrationno': tenant.registrationno ?? '',
|
||||
});
|
||||
}
|
||||
}
|
||||
Future<void> _updateProfile({
|
||||
required String address,
|
||||
required double latitude,
|
||||
required double longitude,
|
||||
required String city,
|
||||
required String state,
|
||||
required String suburb,
|
||||
}) async {
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
int? id = prefs.getInt('customerId');
|
||||
prefs.setDouble('lat', latitude);
|
||||
prefs.setDouble('long', longitude);
|
||||
String? name = prefs.getString('customerFirstname');
|
||||
String? contactNo = prefs.getString('contactno');
|
||||
String? fcm = prefs.getString('fcmToken');
|
||||
|
||||
if (id == null) {
|
||||
// Get.snackbar("Error", "Customer ID not found");
|
||||
return;
|
||||
}
|
||||
|
||||
final repo = LoginRepository();
|
||||
|
||||
Map<String, dynamic> data = {
|
||||
"customerid": id,
|
||||
"configid": 2,
|
||||
"address": address.toString(),
|
||||
"suburb": suburb.toString(),
|
||||
"city": city.toString(),
|
||||
"state": state.toString(),
|
||||
"latitude": latitude.toString(),
|
||||
"longitude": longitude.toString(),
|
||||
'customertoken': fcm
|
||||
};
|
||||
|
||||
print("Request Data: $data");
|
||||
// 🧾 Print all data in readable format
|
||||
print("🚀 Sending Update Profile Request:");
|
||||
print("==================================");
|
||||
print("🆔 Customer ID: $id");
|
||||
print("👤 Name: $name");
|
||||
print("📞 Contact: $contactNo");
|
||||
print("📍 Address: $address");
|
||||
print("🏙️ City: $city");
|
||||
print("🌆 State: $state");
|
||||
print("🏘️ Suburb: $suburb");
|
||||
print("🧭 Latitude: $latitude");
|
||||
print("🧭 Longitude: $longitude");
|
||||
print("🧭 fcm: $fcm");
|
||||
print("==================================");
|
||||
|
||||
|
||||
try {
|
||||
final response = await repo.updateProfile(data);
|
||||
print("Server Response: $response");
|
||||
|
||||
if (response != null && response['status'] == true) {
|
||||
tenantController.loadTenants();
|
||||
// Get.snackbar("Success", "Location updated");
|
||||
} else {
|
||||
// Get.snackbar("Error", "Something went wrong");
|
||||
}
|
||||
} catch (e) {
|
||||
print("Update Profile Error: $e");
|
||||
// Get.snackbar("Error", "Something went wrong");
|
||||
}
|
||||
}
|
||||
Future<void> _getAndUpdateCurrentLocation() async {
|
||||
bool serviceEnabled;
|
||||
LocationPermission permission;
|
||||
|
||||
// Check if location service is enabled
|
||||
serviceEnabled = await Geolocator.isLocationServiceEnabled();
|
||||
if (!serviceEnabled) {
|
||||
// Show bottom sheet to add new address
|
||||
_showLocationBottomSheet();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check location permission
|
||||
permission = await Geolocator.checkPermission();
|
||||
if (permission == LocationPermission.denied) {
|
||||
permission = await Geolocator.requestPermission();
|
||||
if (permission == LocationPermission.denied) {
|
||||
print("❌ Location permissions are denied.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (permission == LocationPermission.deniedForever) {
|
||||
print("❌ Location permissions are permanently denied.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Get current position
|
||||
Position position = await Geolocator.getCurrentPosition(
|
||||
desiredAccuracy: LocationAccuracy.high,
|
||||
);
|
||||
|
||||
// Reverse geocode
|
||||
List<Placemark> placemarks =
|
||||
await placemarkFromCoordinates(position.latitude, position.longitude);
|
||||
|
||||
if (placemarks.isNotEmpty) {
|
||||
Placemark place = placemarks.first;
|
||||
|
||||
String fullAddress =
|
||||
"${place.name}, ${place.subLocality}, ${place.locality}, ${place.administrativeArea}, ${place.country}, ${place.postalCode}";
|
||||
String city = place.locality ?? '';
|
||||
String state = place.administrativeArea ?? '';
|
||||
String suburb = place.subLocality ?? '';
|
||||
|
||||
// Auto update profile
|
||||
await _updateProfile(
|
||||
address: fullAddress,
|
||||
latitude: position.latitude,
|
||||
longitude: position.longitude,
|
||||
city: city,
|
||||
state: state,
|
||||
suburb: suburb,
|
||||
);
|
||||
|
||||
|
||||
} else {
|
||||
print("⚠️ No address found for this location.");
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _showLocationBottomSheet() async {
|
||||
await _fetchLocations();
|
||||
|
||||
if (fetchedLocations.isEmpty) return;
|
||||
|
||||
await Future.delayed(Duration.zero);
|
||||
|
||||
Get.bottomSheet(
|
||||
StatefulBuilder(
|
||||
builder: (context, setState) {
|
||||
String? selectedAddress;
|
||||
|
||||
return SafeArea(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.only(bottom: 16),
|
||||
height: MediaQuery.of(context).size.height * 0.75,
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Drag Handle
|
||||
Center(
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(top: 10, bottom: 16),
|
||||
width: 40,
|
||||
height: 4,
|
||||
decoration: BoxDecoration(
|
||||
color: ColorConstants.primaryColor,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// ── Header Row ──
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
// Title + subtitle
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ReusableTextWidget(
|
||||
text: 'Location & Address',
|
||||
color: Colors.black,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
ReusableTextWidget(
|
||||
text: 'Allow location access for faster delivery',
|
||||
color: Colors.grey.shade600,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Map pin illustration box
|
||||
Container(
|
||||
width: 64,
|
||||
height: 64,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFEDE7F6),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
const Icon(Icons.location_on,
|
||||
color: Color(0xFF662582), size: 32),
|
||||
Positioned(
|
||||
top: 8,
|
||||
left: 8,
|
||||
child: Icon(Icons.auto_awesome,
|
||||
color: Color(0xFF662582), size: 12),
|
||||
),
|
||||
Positioned(
|
||||
top: 12,
|
||||
left: 14,
|
||||
child: Icon(Icons.auto_awesome,
|
||||
color: Color(0xFF662582), size: 8),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// ── Turn on Location Card ──
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16, vertical: 14),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF0EAFB),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// Crosshair icon circle
|
||||
Container(
|
||||
width: 44,
|
||||
height: 44,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
shape: BoxShape.circle,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.08),
|
||||
blurRadius: 6,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: const Icon(Icons.my_location,
|
||||
color: Color(0xFF662582), size: 22),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
// Text
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ReusableTextWidget(
|
||||
text: 'Turn on location',
|
||||
color: Colors.black,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
ReusableTextWidget(
|
||||
text: 'Detect your location automatically',
|
||||
color: Colors.grey.shade600,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
// Enable button
|
||||
ElevatedButton.icon(
|
||||
onPressed: () async {
|
||||
final result =
|
||||
await Get.to(() => const MapPickerPage1());
|
||||
if (result != null &&
|
||||
result is Map<String, dynamic>) {
|
||||
await _updateProfile(
|
||||
address: result['address'] ?? '',
|
||||
latitude: double.tryParse(
|
||||
result['latitude'] ?? '0') ??
|
||||
0.0,
|
||||
longitude: double.tryParse(
|
||||
result['longitude'] ?? '0') ??
|
||||
0.0,
|
||||
city: result['city'] ?? '',
|
||||
state: result['state'] ?? '',
|
||||
suburb: result['suburb'] ?? '',
|
||||
);
|
||||
Navigator.pop(context, result);
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.near_me,
|
||||
color: Colors.white, size: 16),
|
||||
label: ReusableTextWidget(
|
||||
text: 'Enable',
|
||||
color: Colors.white,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: ColorConstants.primaryColor,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 14, vertical: 10),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// ── Saved Addresses Header ──
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
ReusableTextWidget(
|
||||
text: 'Saved Addresses',
|
||||
color: Colors.black,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: () async {
|
||||
final result =
|
||||
await Get.to(() => const MapPickerPage1());
|
||||
if (result != null &&
|
||||
result is Map<String, dynamic>) {
|
||||
await _updateProfile(
|
||||
address: result['address'] ?? '',
|
||||
latitude: double.tryParse(
|
||||
result['latitude'] ?? '0') ??
|
||||
0.0,
|
||||
longitude: double.tryParse(
|
||||
result['longitude'] ?? '0') ??
|
||||
0.0,
|
||||
city: result['city'] ?? '',
|
||||
state: result['state'] ?? '',
|
||||
suburb: result['suburb'] ?? '',
|
||||
);
|
||||
Navigator.pop(context, result);
|
||||
}
|
||||
},
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.add_circle_outline,
|
||||
color: ColorConstants.primaryColor, size: 18),
|
||||
const SizedBox(width: 4),
|
||||
ReusableTextWidget(
|
||||
text: 'Add New',
|
||||
color: ColorConstants.primaryColor,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 10),
|
||||
|
||||
// ── Address List ──
|
||||
Expanded(
|
||||
child: fetchedLocations.isNotEmpty
|
||||
? ListView.builder(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 16),
|
||||
itemCount: fetchedLocations.length,
|
||||
itemBuilder: (context, index) {
|
||||
final loc = fetchedLocations[index];
|
||||
final addr = loc.address ?? '';
|
||||
final isSelected = selectedAddress == addr;
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () async {
|
||||
setState(() => selectedAddress = addr);
|
||||
await _updateProfile(
|
||||
address: loc.address ?? '',
|
||||
latitude: double.tryParse(
|
||||
loc.latitude ?? '0') ??
|
||||
0.0,
|
||||
longitude: double.tryParse(
|
||||
loc.longitude ?? '0') ??
|
||||
0.0,
|
||||
city: loc.city ?? '',
|
||||
state: loc.state ?? '',
|
||||
suburb: loc.suburb ?? '',
|
||||
);
|
||||
Navigator.pop(context, loc);
|
||||
},
|
||||
child: Container(
|
||||
margin:
|
||||
const EdgeInsets.only(bottom: 10),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12, vertical: 14),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(
|
||||
color: isSelected
|
||||
? ColorConstants.primaryColor
|
||||
: Colors.grey.shade200,
|
||||
width: isSelected ? 1.5 : 1,
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color:
|
||||
Colors.black.withOpacity(0.04),
|
||||
blurRadius: 6,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.center,
|
||||
children: [
|
||||
// Purple circle icon
|
||||
Container(
|
||||
width: 42,
|
||||
height: 42,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFEDE7F6),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.location_on,
|
||||
color: Color(0xFF662582),
|
||||
size: 22,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
// Address text
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
ReusableTextWidget(
|
||||
text: loc.suburb?.isNotEmpty ==
|
||||
true
|
||||
? loc.suburb!
|
||||
: "Address ${index + 1}",
|
||||
color: Colors.black87,
|
||||
fontFamily:
|
||||
FontConstants.fontFamily,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
const SizedBox(height: 3),
|
||||
ReusableTextWidget(
|
||||
text: addr,
|
||||
color: Colors.grey.shade600,
|
||||
fontFamily:
|
||||
FontConstants.fontFamily,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
// Current badge or chevron
|
||||
if (isSelected)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 10, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFE8F5E9),
|
||||
borderRadius:
|
||||
BorderRadius.circular(20),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 8,
|
||||
height: 8,
|
||||
decoration:
|
||||
const BoxDecoration(
|
||||
color: Colors.green,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
ReusableTextWidget(
|
||||
text: 'Current',
|
||||
color: Colors.green.shade700,
|
||||
fontFamily:
|
||||
FontConstants.fontFamily,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
else
|
||||
Icon(Icons.chevron_right,
|
||||
color: Colors.grey.shade400,
|
||||
size: 22),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
)
|
||||
: const Center(
|
||||
child: Text(
|
||||
"No saved addresses found.",
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
isScrollControlled: true,
|
||||
);
|
||||
}
|
||||
// Add method to update grid dynamically if needed
|
||||
void addGridItem(Map<String, String> item) {
|
||||
gridItems.add(item);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../../constants/asset_constants.dart';
|
||||
import '../../view/authentication/login_view.dart';
|
||||
|
||||
/// Data modules for each intro slide
|
||||
class IntroSlide {
|
||||
final String title;
|
||||
final String description;
|
||||
final String imageAsset;
|
||||
final String chipLabel;
|
||||
final Color bgColor;
|
||||
final Color accentColor;
|
||||
|
||||
const IntroSlide({
|
||||
required this.title,
|
||||
required this.description,
|
||||
required this.imageAsset,
|
||||
required this.chipLabel,
|
||||
required this.bgColor,
|
||||
required this.accentColor,
|
||||
});
|
||||
}
|
||||
|
||||
class IntroScreenController extends GetxController {
|
||||
late final PageController pageController;
|
||||
late final List<IntroSlide> slides;
|
||||
|
||||
int _currentPage = 0;
|
||||
int get currentPage => _currentPage;
|
||||
bool get isLastPage => _currentPage == slides.length - 1;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
pageController = PageController();
|
||||
|
||||
slides = [
|
||||
const IntroSlide(
|
||||
title: "Fresh Essentials\nEvery Day",
|
||||
description:
|
||||
"Get farm-fresh fruits and vegetables directly from our storage to your home.",
|
||||
imageAsset: AssetConstants.introNew_1,
|
||||
chipLabel: "🌿 FARM TO DOOR",
|
||||
bgColor: Color(0xFFECF8F0),
|
||||
accentColor: Color(0xFF2D9B5A),
|
||||
),
|
||||
const IntroSlide(
|
||||
title: "Fast & Reliable\nDelivery",
|
||||
description:
|
||||
"We store, pack, and deliver from our own facility to ensure quality and speed.",
|
||||
imageAsset: AssetConstants.introNew_2,
|
||||
chipLabel: "⚡ LIGHTNING FAST",
|
||||
bgColor: Color(0xFFFFF7E6),
|
||||
accentColor: Color(0xFFE8931A),
|
||||
),
|
||||
const IntroSlide(
|
||||
title: "Just a Click\nAway",
|
||||
description:
|
||||
"Quick tomato run or a full veggie basket — shopping made easy and convenient.",
|
||||
imageAsset: AssetConstants.intro3,
|
||||
chipLabel: "🛒 SUPER CONVENIENT",
|
||||
bgColor: Color(0xFFEEF4FF),
|
||||
accentColor: Color(0xFF4A7FD4),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
pageController.dispose();
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
void onPageChanged(int index) {
|
||||
_currentPage = index;
|
||||
update();
|
||||
}
|
||||
|
||||
void nextPage() {
|
||||
pageController.nextPage(
|
||||
duration: const Duration(milliseconds: 400),
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
}
|
||||
|
||||
void onDonePress() {
|
||||
Get.off(() => Login_view());
|
||||
}
|
||||
}
|
||||
92
lib/controllers/notifi/notification.dart
Normal file
92
lib/controllers/notifi/notification.dart
Normal file
@@ -0,0 +1,92 @@
|
||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
|
||||
|
||||
|
||||
class NotificationController extends GetxController {
|
||||
var notifications = <Map<String, String>>[].obs; // List of notifications
|
||||
late FlutterLocalNotificationsPlugin localNotifications;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
initNotifications();
|
||||
}
|
||||
|
||||
void initNotifications() async {
|
||||
localNotifications = FlutterLocalNotificationsPlugin();
|
||||
|
||||
const androidSettings = AndroidInitializationSettings('@mipmap/ic_launcher');
|
||||
const iosSettings = DarwinInitializationSettings(); // Updated for iOS
|
||||
await localNotifications.initialize(
|
||||
const InitializationSettings(android: androidSettings, iOS: iosSettings),
|
||||
);
|
||||
|
||||
// Listen for FCM foreground messages
|
||||
FirebaseMessaging.onMessage.listen((message) {
|
||||
showNotification(message);
|
||||
notifications.insert(0, {
|
||||
'title': message.notification?.title ?? '',
|
||||
'body': message.notification?.body ?? '',
|
||||
});
|
||||
update();
|
||||
});
|
||||
}
|
||||
|
||||
void showNotification(RemoteMessage message) async {
|
||||
const androidDetails = AndroidNotificationDetails(
|
||||
'channelId', 'channelName',
|
||||
importance: Importance.max,
|
||||
priority: Priority.high,
|
||||
);
|
||||
|
||||
const iosDetails = DarwinNotificationDetails(); // Updated for iOS
|
||||
const generalNotificationDetails =
|
||||
NotificationDetails(android: androidDetails, iOS: iosDetails);
|
||||
|
||||
await localNotifications.show(
|
||||
0,
|
||||
message.notification?.title ?? '',
|
||||
message.notification?.body ?? '',
|
||||
generalNotificationDetails,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
class NotificationPage extends StatelessWidget {
|
||||
final NotificationController controller = Get.put(NotificationController());
|
||||
|
||||
NotificationPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Notifications'),
|
||||
centerTitle: true,
|
||||
),
|
||||
body: Obx(() {
|
||||
if (controller.notifications.isEmpty) {
|
||||
return const Center(child: Text("No notifications yet"));
|
||||
}
|
||||
return ListView.builder(
|
||||
itemCount: controller.notifications.length,
|
||||
itemBuilder: (context, index) {
|
||||
final notification = controller.notifications[index];
|
||||
return ListTile(
|
||||
leading: const Icon(Icons.notifications),
|
||||
title: Text(notification['title'] ?? ''),
|
||||
subtitle: Text(notification['body'] ?? ''),
|
||||
);
|
||||
},
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
71
lib/controllers/notifi/schedule_notifi.dart
Normal file
71
lib/controllers/notifi/schedule_notifi.dart
Normal file
@@ -0,0 +1,71 @@
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
import 'package:timezone/data/latest_all.dart' as tz;
|
||||
import 'package:timezone/timezone.dart' as tz;
|
||||
|
||||
class NotificationService {
|
||||
static final NotificationService _instance = NotificationService._internal();
|
||||
factory NotificationService() => _instance;
|
||||
NotificationService._internal();
|
||||
|
||||
final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
|
||||
FlutterLocalNotificationsPlugin();
|
||||
|
||||
Future<void> init() async {
|
||||
tz.initializeTimeZones(); // MUST do this
|
||||
final String timeZoneName = await tz.local.name;
|
||||
print("Device Timezone: $timeZoneName");
|
||||
|
||||
const AndroidInitializationSettings androidInit =
|
||||
AndroidInitializationSettings('@mipmap/ic_launcher');
|
||||
|
||||
const InitializationSettings initSettings =
|
||||
InitializationSettings(android: androidInit);
|
||||
|
||||
await flutterLocalNotificationsPlugin.initialize(
|
||||
initSettings,
|
||||
onDidReceiveNotificationResponse: (details) {
|
||||
print("Notification clicked!");
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> scheduleDailyNotification({
|
||||
required int id,
|
||||
required int hour,
|
||||
required int minute,
|
||||
String title = 'Check what’s new 👀',
|
||||
String body = 'Open the app to explore exciting updates!',
|
||||
}) async {
|
||||
final now = tz.TZDateTime.now(tz.local);
|
||||
|
||||
// Correctly calculate the next scheduled time
|
||||
tz.TZDateTime scheduledDate =
|
||||
tz.TZDateTime(tz.local, now.year, now.month, now.day, hour, minute);
|
||||
|
||||
if (scheduledDate.isBefore(now)) {
|
||||
// If time has already passed, schedule for tomorrow
|
||||
scheduledDate = scheduledDate.add(const Duration(days: 1));
|
||||
}
|
||||
|
||||
final androidDetails = AndroidNotificationDetails(
|
||||
'daily_channel',
|
||||
'Daily Notifications',
|
||||
importance: Importance.max,
|
||||
priority: Priority.max,
|
||||
);
|
||||
|
||||
final notificationDetails = NotificationDetails(android: androidDetails);
|
||||
|
||||
await flutterLocalNotificationsPlugin.zonedSchedule(
|
||||
id,
|
||||
title,
|
||||
body,
|
||||
scheduledDate,
|
||||
notificationDetails,
|
||||
androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle,
|
||||
matchDateTimeComponents: DateTimeComponents.time,
|
||||
);
|
||||
|
||||
print("Scheduled notification at $hour:$minute (Next trigger: $scheduledDate)");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
// lib/controllers/order_controller/create_order_controller.dart
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../../domain/provider/order/create_order.dart';
|
||||
import '../../modules/orders/create_order.dart';
|
||||
|
||||
class OrderController extends GetxController {
|
||||
final CreateOrderProvider provider = CreateOrderProvider();
|
||||
var isLoading = false.obs;
|
||||
|
||||
Future<CreateOrderResponse?> createOrder(CreateOrderRequest request) async {
|
||||
try {
|
||||
isLoading.value = true;
|
||||
|
||||
final response = await provider.createOrder(request);
|
||||
|
||||
isLoading.value = false;
|
||||
|
||||
if (response.status == 'accepted') {
|
||||
|
||||
print(response.status);
|
||||
|
||||
print("✅ Order Success");
|
||||
} else {
|
||||
print("❌ Order Failed");
|
||||
}
|
||||
|
||||
return response; // ✅ VERY IMPORTANT
|
||||
} catch (e) {
|
||||
isLoading.value = false;
|
||||
|
||||
print("🔥 ERROR: $e");
|
||||
|
||||
return null; // ✅ return null on error
|
||||
}
|
||||
}}
|
||||
192
lib/controllers/product/product_controller.dart
Normal file
192
lib/controllers/product/product_controller.dart
Normal file
@@ -0,0 +1,192 @@
|
||||
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;
|
||||
|
||||
|
||||
|
||||
List<Product> get allProducts {
|
||||
final response = productResponse.value;
|
||||
if (response == null) return <Product>[];
|
||||
|
||||
final details = response.data?.details ?? <Detail>[];
|
||||
|
||||
return details
|
||||
.expand<Product>((d) => d.products ?? <Product>[])
|
||||
.toList();
|
||||
}
|
||||
|
||||
|
||||
/// In-memory cache: key is "categoryId_tenantId"
|
||||
final Map<String, ProductResponse> _cache = {};
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
|
||||
selectedIndex.value = 0;
|
||||
// Listen for connectivity changes
|
||||
Connectivity().onConnectivityChanged.listen((status) {
|
||||
isConnected.value = (status != ConnectivityResult.none);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
Get.delete<ProductsController>();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
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];
|
||||
selectedIndex.value = 0;
|
||||
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;
|
||||
|
||||
selectedIndex.value = 0;
|
||||
// 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;
|
||||
selectedIndex.value = 0;
|
||||
|
||||
// ✅ 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 ?? [];
|
||||
}
|
||||
}
|
||||
117
lib/controllers/product/variant_controller.dart
Normal file
117
lib/controllers/product/variant_controller.dart
Normal file
@@ -0,0 +1,117 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../../domain/provider/varient/varient_pro.dart';
|
||||
import '../../domain/repository/varient/varient_repo.dart';
|
||||
import '../../modules/product/product.dart';
|
||||
|
||||
class ProductVariantController extends GetxController {
|
||||
final ProductVariantRepository provider;
|
||||
|
||||
ProductVariantController({required this.provider});
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────
|
||||
// Reactive state
|
||||
// ──────────────────────────────────────────────────────────────────────
|
||||
final RxBool isLoading = false.obs;
|
||||
final RxList<Product> productVariants = <Product>[].obs;
|
||||
|
||||
/// Selected variant ID (null = nothing selected)
|
||||
final RxnInt selectedProductId = RxnInt();
|
||||
|
||||
/// Quantity per variant (default = 1)
|
||||
final RxMap<int, int> variantQuantities = <int, int>{}.obs;
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────
|
||||
// Public helpers
|
||||
// ──────────────────────────────────────────────────────────────────────
|
||||
/// **Always call this before opening a new product’s variants**
|
||||
void clearVariantState() {
|
||||
productVariants.clear();
|
||||
selectedProductId.value = null;
|
||||
variantQuantities.clear();
|
||||
isLoading.value = false;
|
||||
}
|
||||
|
||||
void selectVariant(int productId) {
|
||||
if (selectedProductId.value == productId) {
|
||||
selectedProductId.value = null; // allow deselection
|
||||
} else {
|
||||
selectedProductId.value = productId;
|
||||
// Initialise quantity = 1 if not present
|
||||
variantQuantities.putIfAbsent(productId, () => 1);
|
||||
}
|
||||
}
|
||||
|
||||
void increaseQuantity(int productId) {
|
||||
final current = variantQuantities[productId] ?? 0;
|
||||
variantQuantities[productId] = current + 1;
|
||||
}
|
||||
|
||||
void decreaseQuantity(int productId) {
|
||||
final current = variantQuantities[productId] ?? 0;
|
||||
if (current > 1) {
|
||||
variantQuantities[productId] = current - 1;
|
||||
}
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────
|
||||
// FETCH VARIANTS – **reset + safe init**
|
||||
// ──────────────────────────────────────────────────────────────────────
|
||||
Future<void> fetchVariants({
|
||||
required int tenantId,
|
||||
required int variantId,
|
||||
}) async {
|
||||
try {
|
||||
isLoading.value = true;
|
||||
|
||||
// 1. ALWAYS start fresh
|
||||
clearVariantState();
|
||||
|
||||
final variants = await provider.getProductVariant(
|
||||
tenantId: tenantId,
|
||||
variantId: variantId,
|
||||
);
|
||||
|
||||
if (variants != null && variants.isNotEmpty) {
|
||||
productVariants.assignAll(variants);
|
||||
|
||||
// 2. Initialise quantity = 1 for every variant
|
||||
for (final v in variants) {
|
||||
final pid = v.productid ?? 0;
|
||||
if (pid != 0) {
|
||||
variantQuantities[pid] = 1;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Auto-select first variant
|
||||
final first = variants.first;
|
||||
if (first.productid != null) {
|
||||
selectedProductId.value = first.productid;
|
||||
}
|
||||
} else {
|
||||
productVariants.clear();
|
||||
variantQuantities.clear();
|
||||
}
|
||||
} catch (e, s) {
|
||||
debugPrint('fetchVariants error: $e\n$s');
|
||||
// Get.snackbar(
|
||||
// 'Error',
|
||||
// 'Failed to load variants',
|
||||
// snackPosition: SnackPosition.TOP,
|
||||
// backgroundColor: Colors.redAccent,
|
||||
// colorText: Colors.white,
|
||||
// );
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────
|
||||
// Lifecycle
|
||||
// ──────────────────────────────────────────────────────────────────────
|
||||
@override
|
||||
void onClose() {
|
||||
clearVariantState();
|
||||
super.onClose();
|
||||
}
|
||||
}
|
||||
66
lib/controllers/tenant/create_tenant.dart
Normal file
66
lib/controllers/tenant/create_tenant.dart
Normal file
@@ -0,0 +1,66 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import '../../domain/provider/tenant/get_tenant_pro.dart';
|
||||
|
||||
class Create_tenant extends GetxController {
|
||||
final CustomerTenantsProvider provider = CustomerTenantsProvider();
|
||||
|
||||
var isLoading = false.obs;
|
||||
var responseMessage = ''.obs;
|
||||
|
||||
// Get customerId from SharedPreferences
|
||||
Future<int?> _getCustomerId() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return prefs.getInt('customerId');
|
||||
}
|
||||
|
||||
// Call POST API after scanning QR
|
||||
Future<void> createTenantCustomerFromQR({
|
||||
required int tenantId,
|
||||
required int locationId,
|
||||
int status = 1,
|
||||
}) async {
|
||||
try {
|
||||
final customerId = await _getCustomerId();
|
||||
|
||||
if (customerId == null) {
|
||||
responseMessage.value = "Customer ID not found";
|
||||
return;
|
||||
}
|
||||
|
||||
isLoading.value = true;
|
||||
|
||||
final response = await provider.createTenantCustomer(
|
||||
tenantId: tenantId,
|
||||
locationId: locationId,
|
||||
customerId: customerId,
|
||||
status: status,
|
||||
);
|
||||
|
||||
print("🔸 Tenant API Response: $response");
|
||||
|
||||
if (response == null) {
|
||||
responseMessage.value = "No response from server.";
|
||||
return;
|
||||
}
|
||||
|
||||
// ✅ Check API response structure and handle message properly
|
||||
final code = response['code'];
|
||||
final message = response['message'] ?? 'Unknown response';
|
||||
|
||||
if (code == 200 || code == 201) {
|
||||
responseMessage.value = "Tenant customer created successfully";
|
||||
} else if (code == 409) {
|
||||
responseMessage.value = "Customer already assigned to this location";
|
||||
} else {
|
||||
responseMessage.value = "Error: $message";
|
||||
}
|
||||
} catch (e) {
|
||||
responseMessage.value = "Error: $e";
|
||||
print('❌ Exception: $e');
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
92
lib/controllers/tenant/get_tenant.dart
Normal file
92
lib/controllers/tenant/get_tenant.dart
Normal file
@@ -0,0 +1,92 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import '../../domain/provider/tenant/get_tenant_pro.dart';
|
||||
import '../../modules/orders/getcustomerorders.dart';
|
||||
import '../../modules/tenant/get_tenant.dart';
|
||||
|
||||
class OrderedTenantController extends GetxController {
|
||||
final CustomerTenantsProvider provider = CustomerTenantsProvider();
|
||||
|
||||
var isLoading = false.obs;
|
||||
var orders = <Order>[].obs; // ✅ Use Order, not OrderDatum
|
||||
|
||||
int pageNo = 1;
|
||||
int pageSize = 10;
|
||||
bool allLoaded = false;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
loadOrders();
|
||||
}
|
||||
|
||||
/// Reload orders from page 1
|
||||
Future<void> refreshOrders() async {
|
||||
allLoaded = false;
|
||||
pageNo = 1;
|
||||
orders.clear();
|
||||
await loadOrders();
|
||||
}
|
||||
|
||||
/// Load orders with pagination and duplicate prevention
|
||||
Future<void> loadOrders() async {
|
||||
if (allLoaded || isLoading.value) return;
|
||||
|
||||
try {
|
||||
isLoading.value = true;
|
||||
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final customerId = prefs.getInt('customerId');
|
||||
|
||||
if (customerId == null) {
|
||||
Get.snackbar('Error', 'No customer ID found. Please log in.');
|
||||
return;
|
||||
}
|
||||
|
||||
final response = await provider.getCustomerOrderss(
|
||||
customerId,
|
||||
pageNo: pageNo,
|
||||
pageSize: pageSize,
|
||||
);
|
||||
|
||||
print('Requested page: $pageNo, pageSize: $pageSize');
|
||||
|
||||
if (response == null) {
|
||||
print("⚠️ API returned null response");
|
||||
return;
|
||||
}
|
||||
|
||||
// ✅ Use response.orders (not response.data)
|
||||
final fetchedOrders = response.orders;
|
||||
|
||||
if (fetchedOrders.isEmpty) {
|
||||
allLoaded = true;
|
||||
print("✅ All orders loaded.");
|
||||
return;
|
||||
}
|
||||
|
||||
// ✅ Deduplicate by orderheaderid
|
||||
final newOrders = fetchedOrders.where((newOrder) =>
|
||||
!orders.any((existing) =>
|
||||
existing.orderheaderid == newOrder.orderheaderid)).toList();
|
||||
|
||||
if (newOrders.isNotEmpty) {
|
||||
orders.addAll(newOrders);
|
||||
pageNo++;
|
||||
print("✅ Orders loaded: ${orders.length}");
|
||||
} else {
|
||||
allLoaded = true; // All fetched orders already exist locally
|
||||
print("✅ No new unique orders. Marking as all loaded.");
|
||||
}
|
||||
|
||||
// ✅ If fewer items than pageSize were returned, we've reached the end
|
||||
if (fetchedOrders.length < pageSize) {
|
||||
allLoaded = true;
|
||||
}
|
||||
} catch (e) {
|
||||
print("⛔ Error in loadOrders: $e");
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
133
lib/controllers/tenant_controller /tenant_list.dart
Normal file
133
lib/controllers/tenant_controller /tenant_list.dart
Normal file
@@ -0,0 +1,133 @@
|
||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import '../../domain/provider/tenant/get_tenant_pro.dart';
|
||||
import '../../modules/tenant/get_tenant.dart';
|
||||
|
||||
class TenantController extends GetxController {
|
||||
final CustomerTenantsProvider provider = CustomerTenantsProvider();
|
||||
var selectedCategoryId = 0.obs;
|
||||
var isLoading = false.obs;
|
||||
var customerFirstName = ''.obs;
|
||||
var profileImage = ''.obs;
|
||||
var tenants = <Tenant>[].obs;
|
||||
var searchtenants = <Tenant>[].obs;
|
||||
var isConnected = true.obs;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
loadTenants();
|
||||
loadTenants2(categoryId: 0);
|
||||
// Listen for connectivity changes
|
||||
// Listen for connectivity changes
|
||||
Connectivity().onConnectivityChanged.listen((status) {
|
||||
bool connected = (status != ConnectivityResult.none);
|
||||
|
||||
// If we were offline and now online → reload tenants
|
||||
if (!isConnected.value && connected) {
|
||||
loadTenants();
|
||||
}
|
||||
|
||||
isConnected.value = connected;
|
||||
});
|
||||
}
|
||||
|
||||
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> loadTenants({int? categoryId}) async {
|
||||
isLoading.value = true;
|
||||
|
||||
bool connected = await hasInternet();
|
||||
if (!connected) {
|
||||
isLoading.value = false;
|
||||
isConnected.value = false;
|
||||
return;
|
||||
} else {
|
||||
isConnected.value = true;
|
||||
}
|
||||
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final customerId = prefs.getInt('customerId');
|
||||
|
||||
if (customerId == null) {
|
||||
print("⚠️ No customer id found");
|
||||
return;
|
||||
}
|
||||
|
||||
// 🔥 Use selectedCategoryId if passed
|
||||
final response = await provider.getCustomerTenants(
|
||||
customerId,
|
||||
categoryId ?? selectedCategoryId.value,
|
||||
);
|
||||
|
||||
if (response != null && response.status == true && response.details != null) {
|
||||
tenants.value = response.details!;
|
||||
print("✅ Tenants loaded: ${tenants.length}");
|
||||
} else {
|
||||
print("⚠️ Failed: ${response?.message}");
|
||||
}
|
||||
} catch (e) {
|
||||
print("⛔ Error: $e");
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Future<void> loadTenants2({int? categoryId}) async {
|
||||
isLoading.value = true;
|
||||
|
||||
bool connected = await hasInternet();
|
||||
if (!connected) {
|
||||
isLoading.value = false;
|
||||
isConnected.value = false;
|
||||
return;
|
||||
} else {
|
||||
isConnected.value = true;
|
||||
}
|
||||
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final customerId = prefs.getInt('customerId');
|
||||
|
||||
if (customerId == null) {
|
||||
print("⚠️ No customer id found");
|
||||
return;
|
||||
}
|
||||
|
||||
// 🔥 Use selectedCategoryId if passed
|
||||
final response = await provider.getCustomerTenants(
|
||||
customerId,
|
||||
categoryId ?? 0,
|
||||
);
|
||||
|
||||
if (response != null && response.status == true && response.details != null) {
|
||||
searchtenants.value = response.details!;
|
||||
print("✅ Tenants loaded: ${searchtenants.length}");
|
||||
} else {
|
||||
print("⚠️ Failed: ${response?.message}");
|
||||
}
|
||||
} catch (e) {
|
||||
print("⛔ Error: $e");
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
34
lib/data/authentication/auth_request.dart
Normal file
34
lib/data/authentication/auth_request.dart
Normal file
@@ -0,0 +1,34 @@
|
||||
class LoginRequest {
|
||||
String? contactno;
|
||||
int? configid;
|
||||
String? customertoken;
|
||||
String? devicetype;
|
||||
String? deviceid;
|
||||
|
||||
LoginRequest(
|
||||
{this.contactno,
|
||||
this.configid,
|
||||
this.customertoken,
|
||||
this.devicetype,
|
||||
this.deviceid}
|
||||
|
||||
);
|
||||
|
||||
LoginRequest.fromJson(Map<String, dynamic> json) {
|
||||
contactno = json['contactno'];
|
||||
configid = json['configid'];
|
||||
customertoken = json['customertoken'];
|
||||
devicetype = json['devicetype'];
|
||||
deviceid = json['deviceid'];
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
data['contactno'] = contactno;
|
||||
data['configid'] = configid;
|
||||
data['customertoken'] = customertoken;
|
||||
data['devicetype'] = devicetype;
|
||||
data['deviceid'] = deviceid;
|
||||
return data;
|
||||
}
|
||||
}
|
||||
29
lib/data/authentication/auth_response.dart
Normal file
29
lib/data/authentication/auth_response.dart
Normal file
@@ -0,0 +1,29 @@
|
||||
import '../../modules/authentication/auth.dart';
|
||||
|
||||
class LoginResponse {
|
||||
int? code;
|
||||
Authentication? details;
|
||||
String? message;
|
||||
bool? status;
|
||||
|
||||
LoginResponse({this.code, this.details, this.message, this.status});
|
||||
|
||||
LoginResponse.fromJson(Map<String, dynamic> json) {
|
||||
code = json['code'];
|
||||
details =
|
||||
json['details'] != null ? new Authentication.fromJson(json['details']) : null;
|
||||
message = json['message'];
|
||||
status = json['status'];
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = new Map<String, dynamic>();
|
||||
data['code'] = this.code;
|
||||
if (this.details != null) {
|
||||
data['details'] = this.details!.toJson();
|
||||
}
|
||||
data['message'] = this.message;
|
||||
data['status'] = this.status;
|
||||
return data;
|
||||
}
|
||||
}
|
||||
19
lib/data/tenant/get_tenant_res.dart
Normal file
19
lib/data/tenant/get_tenant_res.dart
Normal file
@@ -0,0 +1,19 @@
|
||||
|
||||
|
||||
import '../../modules/tenant/get_tenant.dart';
|
||||
|
||||
class CustomerTenantResponse {
|
||||
final Customer customer;
|
||||
final List<Tenant> tenants;
|
||||
|
||||
CustomerTenantResponse({required this.customer, required this.tenants});
|
||||
|
||||
factory CustomerTenantResponse.fromJson(Map<String, dynamic> json) {
|
||||
return CustomerTenantResponse(
|
||||
customer: Customer.fromJson(json['data']['customer']),
|
||||
tenants: (json['data']['tenants'] as List)
|
||||
.map((e) => Tenant.fromJson(e))
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
121
lib/domain/provider/authentication/auth_provider.dart
Normal file
121
lib/domain/provider/authentication/auth_provider.dart
Normal file
@@ -0,0 +1,121 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:nearledaily/constants/api_constants.dart';
|
||||
|
||||
import '../../../Helper/Logger.dart';
|
||||
import '../../../data/authentication/auth_request.dart';
|
||||
import '../../../data/authentication/auth_response.dart';
|
||||
import '../../../modules/authentication/auth.dart';
|
||||
import '../../../modules/authentication/getbyid.dart';
|
||||
import '../../../service/dio.dart';
|
||||
|
||||
|
||||
class LoginProvider {
|
||||
final CustomDio _customDio = CustomDio(); // ✅ Declare the CustomDio instance
|
||||
|
||||
|
||||
Future<LoginResponse?> signIn(String urlData, LoginRequest data) async {
|
||||
logger.i('signInUrlData $urlData');
|
||||
LoginResponse? loginResponse;
|
||||
final customDio = CustomDio();
|
||||
|
||||
try {
|
||||
final response = await customDio.postData(
|
||||
urlData,
|
||||
data.toJson(),
|
||||
);
|
||||
|
||||
logger.i("Raw response type: ${response.runtimeType}");
|
||||
logger.i("Raw response: $response");
|
||||
|
||||
if (response != null) {
|
||||
if (response is Map<String, dynamic>) {
|
||||
loginResponse = LoginResponse.fromJson(response);
|
||||
} else if (response is String) {
|
||||
try {
|
||||
final Map<String, dynamic> jsonMap = jsonDecode(response);
|
||||
loginResponse = LoginResponse.fromJson(jsonMap);
|
||||
} catch (e) {
|
||||
loginResponse = LoginResponse(
|
||||
status: false,
|
||||
message: "Something went wrong",
|
||||
);
|
||||
}
|
||||
} else {
|
||||
loginResponse = LoginResponse(
|
||||
status: false,
|
||||
message: "something went wrong",
|
||||
);
|
||||
}
|
||||
|
||||
logger.i('loginResponse: ${loginResponse.toJson()}');
|
||||
}
|
||||
} catch (e, stacktrace) {
|
||||
logger.e("Error occurred in signIn: $e");
|
||||
logger.e(stacktrace);
|
||||
loginResponse = LoginResponse(
|
||||
status: false,
|
||||
message: "Something went wrong",
|
||||
);
|
||||
}
|
||||
|
||||
return loginResponse;
|
||||
}
|
||||
|
||||
Future<CustomerFullView?> getProfile(String customerId) async {
|
||||
logger.i('🔹 GetProfile API customerId: $customerId');
|
||||
|
||||
CustomerFullView? profile;
|
||||
final customDio = CustomDio();
|
||||
final url = "${ApiConstants.fetchProfile}customerid=$customerId&contactno=''";
|
||||
|
||||
try {
|
||||
final response = await customDio.getData(url,
|
||||
headers: {
|
||||
'x-hasura-admin-secret': 'nearle-admin-secret',
|
||||
},
|
||||
);
|
||||
|
||||
logger.i("Raw response type: ${response.runtimeType}");
|
||||
logger.i("Raw response: $response");
|
||||
|
||||
if (response != null) {
|
||||
Map<String, dynamic> jsonMap;
|
||||
if (response is Map<String, dynamic>) {
|
||||
jsonMap = response;
|
||||
} else if (response is String) {
|
||||
jsonMap = jsonDecode(response);
|
||||
} else {
|
||||
throw Exception("Something went wrong");
|
||||
}
|
||||
|
||||
// Parse the list and take the first item
|
||||
final list = jsonMap['customer_full_view'] as List?;
|
||||
|
||||
logger.i('🔹 Full jsonMap keys: ${jsonMap.keys.toList()}'); // ← see all keys
|
||||
logger.i('🔹 customerFullView list: $list'); // ← see the list
|
||||
logger.i('🔹 list length: ${list?.length}');
|
||||
if (list != null && list.isNotEmpty) {
|
||||
profile = CustomerFullView.fromJson(list[0] as Map<String, dynamic>);
|
||||
logger.i('Profile parsed successfully');
|
||||
}
|
||||
}
|
||||
|
||||
} catch (e, stacktrace) {
|
||||
logger.e("Error occurred in ProfileProvider.getProfile: $e");
|
||||
logger.e(stacktrace);
|
||||
}
|
||||
|
||||
return profile;
|
||||
}
|
||||
|
||||
|
||||
Future<dynamic> putData(String endpoint, Map<String, dynamic> data) async {
|
||||
try {
|
||||
final response = await _customDio.putData(endpoint, data);
|
||||
return response;
|
||||
} catch (e) {
|
||||
return {"status": false, "message": e.toString()};
|
||||
}
|
||||
}
|
||||
}
|
||||
98
lib/domain/provider/authentication/location.dart
Normal file
98
lib/domain/provider/authentication/location.dart
Normal file
@@ -0,0 +1,98 @@
|
||||
import 'dart:convert';
|
||||
import 'package:nearledaily/service/dio.dart';
|
||||
import 'package:nearledaily/helper/logger.dart';
|
||||
import '../../../constants/api_constants.dart';
|
||||
import '../../../modules/authentication/auth.dart';
|
||||
import '../../repository/authentication/location_repo.dart';
|
||||
|
||||
class CustomerLocationProvider implements CustomerLocationRepository {
|
||||
final CustomDio customDio = CustomDio();
|
||||
|
||||
@override
|
||||
Future<List<Authentication>> fetchCustomerLocations(int customerId) async {
|
||||
final url = "${ApiConstants.getCustomerLocations}?customerid=$customerId";
|
||||
logger.i("GET CustomerLocation URL: $url");
|
||||
|
||||
try {
|
||||
final response = await customDio.getData(
|
||||
url,
|
||||
headers: {
|
||||
"x-hasura-admin-secret": "nearle-admin-secret",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
);
|
||||
|
||||
if (response != null &&
|
||||
response['customerlocations'] != null &&
|
||||
(response['customerlocations'] as List).isNotEmpty) {
|
||||
|
||||
final locations = (response['customerlocations'] as List)
|
||||
.map((e) => Authentication.fromLocationJson(e))
|
||||
.toList();
|
||||
|
||||
logger.i("Locations count: ${locations.length}");
|
||||
return locations;
|
||||
|
||||
} else {
|
||||
logger.w("No customer locations found for customerId $customerId");
|
||||
return [];
|
||||
}
|
||||
} catch (e) {
|
||||
logger.e("Error in fetchCustomerLocations: $e");
|
||||
return [];
|
||||
}
|
||||
}
|
||||
// Create new customer location
|
||||
@override
|
||||
Future<bool> createCustomerLocation({
|
||||
required int customerId,
|
||||
required String address,
|
||||
required String doorNo,
|
||||
required String landmark,
|
||||
String suburb = "",
|
||||
String city = "",
|
||||
String state = "",
|
||||
String postcode = "",
|
||||
String latitude = "",
|
||||
String longitude = "",
|
||||
String defaultAddress = "Yes",
|
||||
int primaryAddress = 1,
|
||||
int status = 1,
|
||||
}) async {
|
||||
final url = "https://fiesta.nearle.app/live/api/v1/mob/customers/createlocations";
|
||||
final body = {
|
||||
"customerid": customerId,
|
||||
"address": address,
|
||||
"suburb": suburb,
|
||||
"city": city,
|
||||
"state": state,
|
||||
"landmark": landmark,
|
||||
"doorno": doorNo,
|
||||
"postcode": postcode,
|
||||
"latitude": latitude,
|
||||
"longitude": longitude,
|
||||
"defaultaddress": defaultAddress,
|
||||
"primaryaddress": primaryAddress,
|
||||
"status": status
|
||||
};
|
||||
|
||||
logger.i("POST CustomerLocation URL: $url");
|
||||
logger.i("Request Body: ${jsonEncode(body)}");
|
||||
|
||||
try {
|
||||
final response = await customDio.postData(url, body);
|
||||
|
||||
if (response != null && (response['code'] == 200 || response['code'] == 201)) {
|
||||
logger.i("CustomerLocation created successfully: ${jsonEncode(response)}");
|
||||
return true;
|
||||
} else {
|
||||
logger.w("Failed to create CustomerLocation: ${jsonEncode(response)}");
|
||||
return false;
|
||||
}
|
||||
} catch (e) {
|
||||
logger.e("Error in createCustomerLocation: $e");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
26
lib/domain/provider/order/create_order.dart
Normal file
26
lib/domain/provider/order/create_order.dart
Normal file
@@ -0,0 +1,26 @@
|
||||
// lib/domain/provider/order/create_order_provider.dart
|
||||
import 'package:dio/dio.dart';
|
||||
import '../../../modules/orders/create_order.dart';
|
||||
import '../../../service/dio.dart';
|
||||
import '../../repository/order/create_order_repo.dart';
|
||||
|
||||
class CreateOrderProvider implements CreateOrderRepository {
|
||||
final CustomDio customDio = CustomDio();
|
||||
|
||||
@override
|
||||
Future<CreateOrderResponse> createOrder(CreateOrderRequest request) async {
|
||||
try {
|
||||
final response = await customDio.postData(
|
||||
'https://queue.workolik.com/live/api/v1/mob/orders/createorder',
|
||||
request.toJson(),
|
||||
|
||||
);
|
||||
|
||||
print(response);
|
||||
return CreateOrderResponse.fromJson(response);
|
||||
} catch (e) {
|
||||
return CreateOrderResponse(
|
||||
status: "error", message_id: 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
35
lib/domain/provider/product/all_products.dart
Normal file
35
lib/domain/provider/product/all_products.dart
Normal file
@@ -0,0 +1,35 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import '../../../Helper/Logger.dart';
|
||||
import '../../../modules/product/product.dart';
|
||||
import '../../../service/dio.dart';
|
||||
|
||||
|
||||
class ProductsProvider {
|
||||
final CustomDio customDio = CustomDio();
|
||||
|
||||
Future<ProductResponse?> getProductsBySubCategory({
|
||||
required int categoryId,
|
||||
required int tenantId, required int locationId,
|
||||
}) async {
|
||||
final url =
|
||||
"https://fiesta.nearle.app/live/api/v1/mob/products/getproductsbysubcategory?categoryid=2&tenantid=$tenantId&locationid=$locationId";
|
||||
logger.i("GET ProductsBySubCategory URL: $url");
|
||||
|
||||
ProductResponse? responseModel;
|
||||
|
||||
try {
|
||||
final response = await customDio.getData(url);
|
||||
|
||||
if (response != null) {
|
||||
// logger.i(response);
|
||||
responseModel = ProductResponse.fromJson(response);
|
||||
// logger.i("GET Products Response: ${jsonEncode(responseModel.toJson())}");
|
||||
}
|
||||
} catch (e) {
|
||||
logger.e("Error in getProductsBySubCategory: $e");
|
||||
}
|
||||
|
||||
return responseModel;
|
||||
}
|
||||
}
|
||||
78
lib/domain/provider/profile/create_request.dart
Normal file
78
lib/domain/provider/profile/create_request.dart
Normal file
@@ -0,0 +1,78 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import '../../../modules/profile/customer_request.dart';
|
||||
import '../../repository/profile/request_repo.dart';
|
||||
|
||||
class CustomerRequestProvider with ChangeNotifier {
|
||||
final CustomerRequestRepository _repository = CustomerRequestRepository();
|
||||
bool isLoading = false;
|
||||
|
||||
// List of fetched customer requests
|
||||
List<CustomerRequestStatusModel> requests = [];
|
||||
|
||||
/// Create a new customer request
|
||||
Future<bool> sendRequest(String subject, String remarks) async {
|
||||
isLoading = true;
|
||||
notifyListeners();
|
||||
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
int? customerId = prefs.getInt('customerId');
|
||||
|
||||
if (customerId == null) {
|
||||
isLoading = false;
|
||||
notifyListeners();
|
||||
throw Exception("Something went wrong");
|
||||
}
|
||||
|
||||
final model = CustomerRequestModel(
|
||||
referencedate: DateTime.now().toUtc().toIso8601String().split('.').first + 'Z',
|
||||
referencetype: "general", // always set default value
|
||||
customerid: customerId,
|
||||
tenantid: 0,
|
||||
locationid: 0,
|
||||
subject: subject,
|
||||
remarks: remarks,
|
||||
status: 0,
|
||||
apptypeid: 98,
|
||||
);
|
||||
|
||||
final result = await _repository.createCustomerRequest(model);
|
||||
|
||||
isLoading = false;
|
||||
notifyListeners();
|
||||
|
||||
if (result != null && result is Map<String, dynamic> && result["status"] != false) {
|
||||
debugPrint("✅ API Success: $result");
|
||||
// Optionally refresh the list after creating a request
|
||||
await fetchCustomerRequests();
|
||||
return true;
|
||||
} else {
|
||||
debugPrint("❌ API Failed: $result");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch customer requests (status)
|
||||
Future<void> fetchCustomerRequests({int pageNo = 1, int pageSize = 10}) async {
|
||||
isLoading = true;
|
||||
notifyListeners();
|
||||
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
int? customerId = prefs.getInt('customerId');
|
||||
|
||||
if (customerId == null) {
|
||||
isLoading = false;
|
||||
notifyListeners();
|
||||
return;
|
||||
}
|
||||
|
||||
requests = await _repository.fetchCustomerRequests(
|
||||
customerId: customerId,
|
||||
pageNo: pageNo,
|
||||
pageSize: pageSize,
|
||||
);
|
||||
|
||||
isLoading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
222
lib/domain/provider/tenant/get_tenant_pro.dart
Normal file
222
lib/domain/provider/tenant/get_tenant_pro.dart
Normal file
@@ -0,0 +1,222 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import '../../../Helper/Logger.dart';
|
||||
import '../../../constants/api_constants.dart';
|
||||
import '../../../modules/orders/getcustomerorders.dart';
|
||||
import '../../../modules/tenant/get_tenant.dart';
|
||||
import '../../../service/dio.dart';
|
||||
|
||||
class CustomerTenantsProvider {
|
||||
final CustomDio customDio = CustomDio();
|
||||
|
||||
Future<CustomerTenantsResponse?> getCustomerTenants(int customerId, int i) async {
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
|
||||
double? la = prefs.getDouble('lat');
|
||||
double? lo = prefs.getDouble('long');
|
||||
|
||||
final url = "${ApiConstants.tenantCustomers}?customerid=$customerId&tenant=0&latitude=$la&longitude=$lo&categoryid=$i";
|
||||
logger.i("GET CustomerTenants URL: $url");
|
||||
|
||||
CustomerTenantsResponse? responseModel;
|
||||
|
||||
try {
|
||||
final response = await customDio.getData(url);
|
||||
|
||||
if (response != null) {
|
||||
logger.i(response);
|
||||
responseModel = CustomerTenantsResponse.fromJson(response);
|
||||
logger.i("GET CustomerTenants Response: ${jsonEncode(responseModel.toJson())}");
|
||||
}
|
||||
} catch (e) {
|
||||
logger.e("Error in getCustomerTenants: $e");
|
||||
}
|
||||
|
||||
return responseModel;
|
||||
}
|
||||
|
||||
// Future<OrdersResponse?> getCustomerOrders(int customerId, {required int pageNo, required int pageSize}) async {
|
||||
//
|
||||
// print(pageNo);
|
||||
// print(pageSize);
|
||||
// print('ee');
|
||||
// final url = "https://fiesta.nearle.app/live/api/v1/mob/orders/getcustomerorders/?customerid=$customerId&pageno=$pageNo&pagesize=$pageSize";
|
||||
// logger.i("GET CustomerOrders URL: $url"); // Should now show pagesize=8
|
||||
//
|
||||
// OrdersResponse? responseModel;
|
||||
//
|
||||
// try {
|
||||
// final response = await customDio.getData(url);
|
||||
//
|
||||
// if (response != null) {
|
||||
// responseModel = OrdersResponse.fromJson(response);
|
||||
// logger.i("GET CustomerOrders Response: ${jsonEncode(responseModel.toJson())}");
|
||||
// }
|
||||
// } catch (e) {
|
||||
// logger.e("Error in getCustomerOrders: $e");
|
||||
// }
|
||||
//
|
||||
// return responseModel;
|
||||
// }
|
||||
|
||||
Future<OrdersResponse?> getCustomerOrders(
|
||||
int customerId, {
|
||||
required int pageNo,
|
||||
required int pageSize,
|
||||
}) async {
|
||||
print(pageNo);
|
||||
print(pageSize);
|
||||
print('ee');
|
||||
|
||||
final url =
|
||||
"https://api.workolik.com/api/rest/getcustomerorders/?customerid=$customerId&pageno=$pageNo&pagesize=$pageSize";
|
||||
|
||||
|
||||
// final url =
|
||||
// "${ApiConstants.getCustomerOrders}"
|
||||
// "?customerid=$customerId"
|
||||
// "&limit=$pageSize"
|
||||
// "&offset=$pageNo";
|
||||
|
||||
logger.i("GET CustomerOrders URL: $url");
|
||||
|
||||
OrdersResponse? responseModel;
|
||||
|
||||
try {
|
||||
final response = await customDio.getData(url);
|
||||
|
||||
// 👇 Add this to see what actually came back
|
||||
logger.i("🔍 Raw response type: ${response.runtimeType}");
|
||||
logger.i("🔍 Raw response: $response");
|
||||
|
||||
dynamic jsonResponse;
|
||||
|
||||
if (response is String) {
|
||||
try {
|
||||
jsonResponse = jsonDecode(response);
|
||||
} catch (e) {
|
||||
logger.e("❌ JSON decode failed: $e");
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
jsonResponse = response;
|
||||
}
|
||||
|
||||
if (jsonResponse != null && jsonResponse is Map<String, dynamic>) {
|
||||
responseModel = OrdersResponse.fromJson(jsonResponse);
|
||||
logger.i("✅ Parsed Orders Response: ${jsonEncode(responseModel.toJson())}");
|
||||
} else {
|
||||
logger.w("⚠️ Unexpected response format: $jsonResponse");
|
||||
}
|
||||
} catch (e) {
|
||||
logger.e("⛔ Error in getCustomerOrders: $e");
|
||||
}
|
||||
|
||||
return responseModel;
|
||||
}
|
||||
|
||||
|
||||
Future<OrderResponse?> getCustomerOrderss(
|
||||
int customerId, {
|
||||
required int pageNo,
|
||||
required int pageSize,
|
||||
}) async {
|
||||
final offset = (pageNo - 1) * pageSize;
|
||||
|
||||
final url =
|
||||
"https://api.workolik.com/api/rest/getcustomerorders"
|
||||
"?customerid=$customerId"
|
||||
"&tenantid=1087"
|
||||
"&moduleid=2"
|
||||
"&fromdate=2025-08-01T00:00:00"
|
||||
"&todate=2026-12-31T23:59:59"
|
||||
"&orderstatus=delivered"
|
||||
"&keyword=%%"
|
||||
"&limit=$pageSize"
|
||||
"&offset=$offset";
|
||||
|
||||
try {
|
||||
logger.i("➡️ API Request: $url");
|
||||
|
||||
final response = await customDio.getData(url, headers: {
|
||||
"x-hasura-admin-secret": "nearle-admin-secret",
|
||||
// OR
|
||||
// "x-hasura-access-key": "YOUR_ACCESS_KEY",
|
||||
},);
|
||||
|
||||
logger.d("✅ API Response: $response");
|
||||
|
||||
if (response != null) {
|
||||
return OrderResponse.fromJson(response);
|
||||
} else {
|
||||
logger.w("⚠️ API returned null");
|
||||
return null;
|
||||
}
|
||||
} catch (e, stackTrace) {
|
||||
logger.e("❌ API Error", error: e, stackTrace: stackTrace);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>?> createTenantCustomer({
|
||||
required int tenantId,
|
||||
required int locationId,
|
||||
required int customerId,
|
||||
required int status,
|
||||
}) async {
|
||||
final url = "https://fiesta.nearle.app/live/api/v1/mob/tenants/createtenantcustomer";
|
||||
final body = {
|
||||
"tenantid": tenantId,
|
||||
"locationid": locationId,
|
||||
"customerid": customerId,
|
||||
"status": status,
|
||||
};
|
||||
logger.i("POST CreateTenantCustomer URL: $url, Body: $body");
|
||||
|
||||
try {
|
||||
final response = await customDio.postData(url, body);
|
||||
|
||||
logger.i("POST CreateTenantCustomer Response: $response");
|
||||
|
||||
// Ensure response is a Map<String, dynamic>
|
||||
if (response is Map<String, dynamic>) {
|
||||
return response;
|
||||
} else {
|
||||
// If API returned string or other type, wrap it
|
||||
return {'message': 'Something went wrong'};
|
||||
}
|
||||
} catch (e) {
|
||||
logger.e("Error in createTenantCustomer: $e");
|
||||
// Wrap exception in map
|
||||
return {'error':'Something went wrong'};
|
||||
}
|
||||
}
|
||||
|
||||
Future<TenantLocationsResponse?> getTenantLocations(int tenantId) async {
|
||||
final url = "https://fiesta.nearle.app/live/api/v1/mob/tenants/gettenantlocations/?tenantid=$tenantId";
|
||||
logger.i("GET TenantLocations URL: $url");
|
||||
|
||||
try {
|
||||
final response = await customDio.getData(url);
|
||||
|
||||
if (response != null) {
|
||||
final responseModel = TenantLocationsResponse.fromJson(response);
|
||||
logger.i("GET TenantLocations Response: ${jsonEncode(responseModel.toJson())}");
|
||||
return responseModel; // <--- return the parsed modules
|
||||
}
|
||||
} catch (e) {
|
||||
logger.e("Error in getTenantLocations: $e");
|
||||
}
|
||||
|
||||
return null; // <--- return null if request fails
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
42
lib/domain/provider/varient/varient_pro.dart
Normal file
42
lib/domain/provider/varient/varient_pro.dart
Normal file
@@ -0,0 +1,42 @@
|
||||
import 'dart:convert';
|
||||
import 'package:nearledaily/service/dio.dart';
|
||||
import 'package:nearledaily/helper/logger.dart';
|
||||
import '../../../modules/product/product.dart';
|
||||
import '../../repository/varient/varient_repo.dart';
|
||||
|
||||
class ProductVariantProvider implements ProductVariantRepository {
|
||||
final CustomDio customDio = CustomDio();
|
||||
|
||||
@override
|
||||
Future<List<Product>?> getProductVariant({
|
||||
required int tenantId,
|
||||
required int variantId,
|
||||
}) async {
|
||||
final url =
|
||||
"https://fiesta.nearle.app/live/api/v1/mob/products/getproductbyvariant?tenantid=$tenantId&variantid=$variantId";
|
||||
logger.i("GET ProductVariant URL: $url");
|
||||
|
||||
try {
|
||||
final response = await customDio.getData(url);
|
||||
|
||||
if (response != null &&
|
||||
response['code'] == 200 &&
|
||||
response['details'] != null &&
|
||||
response['details'].isNotEmpty) {
|
||||
// Map JSON using new Product.fromJson factory
|
||||
final products = (response['details'] as List)
|
||||
.map((e) => Product.fromJson(e as Map<String, dynamic>))
|
||||
.toList();
|
||||
|
||||
logger.i("GET ProductVariant Response: ${jsonEncode(products)}");
|
||||
return products;
|
||||
} else {
|
||||
logger.w("No product variants found for variantId $variantId");
|
||||
return [];
|
||||
}
|
||||
} catch (e) {
|
||||
logger.e("Error in getProductVariant: $e");
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
42
lib/domain/repository/authentication/auth_repository.dart
Normal file
42
lib/domain/repository/authentication/auth_repository.dart
Normal file
@@ -0,0 +1,42 @@
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
|
||||
import '../../../constants/api_constants.dart';
|
||||
import '../../../data/authentication/auth_request.dart';
|
||||
import '../../../data/authentication/auth_response.dart';
|
||||
import '../../../modules/authentication/auth.dart';
|
||||
import '../../../modules/authentication/getbyid.dart';
|
||||
import '../../provider/authentication/auth_provider.dart';
|
||||
|
||||
|
||||
class LoginRepository{
|
||||
|
||||
LoginProvider loginProvider = LoginProvider();
|
||||
final LoginProvider _profileProvider = LoginProvider();
|
||||
|
||||
Future<LoginResponse?> signIn(LoginRequest data) async {
|
||||
|
||||
return await loginProvider.signIn('${ApiConstants.login}',data);
|
||||
|
||||
}
|
||||
Future<CustomerFullView?> fetchProfile(String customerId) async {
|
||||
return await _profileProvider.getProfile(customerId);
|
||||
}
|
||||
|
||||
|
||||
Future<dynamic> updateProfile(dynamic data) async {
|
||||
final String url = ApiConstants.updateCustomer;
|
||||
try {
|
||||
final dio = Dio();
|
||||
// print(url);
|
||||
final response = await dio.put(url, data: data);
|
||||
|
||||
return response.data;
|
||||
|
||||
} catch (e) {
|
||||
print("Error updating profile: $e");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
21
lib/domain/repository/authentication/location_repo.dart
Normal file
21
lib/domain/repository/authentication/location_repo.dart
Normal file
@@ -0,0 +1,21 @@
|
||||
import '../../../modules/authentication/auth.dart';
|
||||
|
||||
abstract class CustomerLocationRepository {
|
||||
Future<List<Authentication>> fetchCustomerLocations(int customerId);
|
||||
|
||||
Future<bool> createCustomerLocation({
|
||||
required int customerId,
|
||||
required String address,
|
||||
required String doorNo,
|
||||
required String landmark,
|
||||
String suburb,
|
||||
String city,
|
||||
String state,
|
||||
String postcode,
|
||||
String latitude,
|
||||
String longitude,
|
||||
String defaultAddress,
|
||||
int primaryAddress,
|
||||
int status,
|
||||
});
|
||||
}
|
||||
7
lib/domain/repository/order/create_order_repo.dart
Normal file
7
lib/domain/repository/order/create_order_repo.dart
Normal file
@@ -0,0 +1,7 @@
|
||||
// lib/domain/repository/order/create_order_repo.dart
|
||||
|
||||
import '../../../modules/orders/create_order.dart';
|
||||
|
||||
abstract class CreateOrderRepository {
|
||||
Future<CreateOrderResponse> createOrder(CreateOrderRequest request);
|
||||
}
|
||||
8
lib/domain/repository/product/all_products_repo.dart
Normal file
8
lib/domain/repository/product/all_products_repo.dart
Normal file
@@ -0,0 +1,8 @@
|
||||
import '../../../modules/product/product.dart';
|
||||
|
||||
abstract class ProductsRepository {
|
||||
Future<ProductResponse?> getProductsBySubCategory({
|
||||
required int categoryId,
|
||||
required int tenantId,
|
||||
});
|
||||
}
|
||||
45
lib/domain/repository/profile/request_repo.dart
Normal file
45
lib/domain/repository/profile/request_repo.dart
Normal file
@@ -0,0 +1,45 @@
|
||||
import '../../../modules/profile/customer_request.dart';
|
||||
import '../../../service/dio.dart';
|
||||
|
||||
class CustomerRequestRepository {
|
||||
final CustomDio _dio = CustomDio();
|
||||
|
||||
/// Create a new customer request
|
||||
Future<dynamic> createCustomerRequest(CustomerRequestModel model) async {
|
||||
const String url = "https://fiesta.nearle.app/live/api/v1/mob/customers/createcustomerrequest";
|
||||
try {
|
||||
final response = await _dio.postData(url, model.toJson());
|
||||
print("POST URL: $url");
|
||||
print("Payload: ${model.toJson()}");
|
||||
print("Response: $response");
|
||||
return response;
|
||||
} catch (e) {
|
||||
return {"status": false, "message": e.toString()};
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch customer requests (status)
|
||||
Future<List<CustomerRequestStatusModel>> fetchCustomerRequests({
|
||||
required int customerId,
|
||||
int pageNo = 1,
|
||||
int pageSize = 10,
|
||||
}) async {
|
||||
final String url = "https://fiesta.nearle.app/live/api/v1/mob/customers/getcustomerrequests"
|
||||
"?customerid=$customerId&pageno=$pageNo&pagesize=$pageSize";
|
||||
|
||||
try {
|
||||
final response = await _dio.getData(url); // assuming getData exists
|
||||
print("GET URL: $url");
|
||||
print("Response: $response");
|
||||
|
||||
if (response != null && response['status'] == true) {
|
||||
final List data = response['data'] ?? [];
|
||||
return data.map((e) => CustomerRequestStatusModel.fromJson(e)).toList();
|
||||
}
|
||||
return [];
|
||||
} catch (e) {
|
||||
print("❌ Fetch error: $e");
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
24
lib/domain/repository/tenant/get_tenant_repo.dart
Normal file
24
lib/domain/repository/tenant/get_tenant_repo.dart
Normal file
@@ -0,0 +1,24 @@
|
||||
|
||||
import '../../../modules/tenant/get_tenant.dart';
|
||||
import '../../provider/tenant/get_tenant_pro.dart';
|
||||
|
||||
abstract class CustomerTenantsRepository {
|
||||
Future<CustomerTenantsResponse?> getCustomerTenants(int customerId);
|
||||
Future<OrdersResponse?> getCustomerOrders(int customerId);
|
||||
Future<Map<String, dynamic>?> createTenantCustomer({
|
||||
required int tenantId,
|
||||
required int locationId,
|
||||
required int customerId,
|
||||
required int status,
|
||||
});
|
||||
|
||||
|
||||
final CustomerTenantsProvider provider;
|
||||
|
||||
CustomerTenantsRepository({required this.provider});
|
||||
|
||||
Future<TenantLocationsResponse?> getTenantLocations(int tenantId) async {
|
||||
final response = await provider.getTenantLocations(tenantId);
|
||||
return response;
|
||||
}
|
||||
}
|
||||
19
lib/domain/repository/varient/varient_repo.dart
Normal file
19
lib/domain/repository/varient/varient_repo.dart
Normal file
@@ -0,0 +1,19 @@
|
||||
import '../../../modules/product/product.dart';
|
||||
|
||||
// abstract class ProductVariantRepository {
|
||||
// Future<ProductVariant?> getProductVariant({
|
||||
// required int tenantId,
|
||||
// required int variantId,
|
||||
// });
|
||||
// }
|
||||
|
||||
|
||||
|
||||
|
||||
abstract class ProductVariantRepository {
|
||||
Future<List<Product>?> getProductVariant({
|
||||
required int tenantId,
|
||||
required int variantId,
|
||||
});
|
||||
}
|
||||
|
||||
25
lib/helper/distance.dart
Normal file
25
lib/helper/distance.dart
Normal file
@@ -0,0 +1,25 @@
|
||||
import 'dart:math';
|
||||
|
||||
/// Calculate distance between two coordinates in km
|
||||
double calculateDistance(double lat1, double lon1, double lat2, double lon2) {
|
||||
const double earthRadius = 6371; // km
|
||||
final dLat = _degreesToRadians(lat2 - lat1);
|
||||
final dLon = _degreesToRadians(lon2 - lon1);
|
||||
|
||||
final a = sin(dLat / 2) * sin(dLat / 2) +
|
||||
cos(_degreesToRadians(lat1)) *
|
||||
cos(_degreesToRadians(lat2)) *
|
||||
sin(dLon / 2) *
|
||||
sin(dLon / 2);
|
||||
|
||||
final c = 2 * atan2(sqrt(a), sqrt(1 - a));
|
||||
final distance = earthRadius * c;
|
||||
|
||||
return distance;
|
||||
}
|
||||
|
||||
double _degreesToRadians(double degrees) {
|
||||
return degrees * pi / 180;
|
||||
}
|
||||
|
||||
|
||||
22
lib/helper/firebase_options.dart
Normal file
22
lib/helper/firebase_options.dart
Normal file
@@ -0,0 +1,22 @@
|
||||
import 'package:firebase_core/firebase_core.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
class DefaultFirebaseOptions {
|
||||
static FirebaseOptions get currentPlatform {
|
||||
if (defaultTargetPlatform == TargetPlatform.iOS ||
|
||||
defaultTargetPlatform == TargetPlatform.macOS) {
|
||||
return const FirebaseOptions(
|
||||
apiKey: 'AIzaSyBkzz2Yua74Q9YpzGmUPFP94fmJQqNMIiU',
|
||||
appId: '1:140444764229:ios:d66c3707aaf5c1ed283b2c',
|
||||
messagingSenderId: '140444764229',
|
||||
projectId: 'nearle-gear',
|
||||
storageBucket: 'nearle-gear.appspot.com',
|
||||
iosBundleId: 'com.nearle.gear',
|
||||
);
|
||||
}
|
||||
|
||||
throw UnsupportedError(
|
||||
'DefaultFirebaseOptions are not supported for this platform.',
|
||||
);
|
||||
}
|
||||
}
|
||||
11
lib/helper/logger.dart
Normal file
11
lib/helper/logger.dart
Normal file
@@ -0,0 +1,11 @@
|
||||
import 'package:logger/logger.dart';
|
||||
|
||||
var logger = Logger(
|
||||
printer: PrettyPrinter(
|
||||
methodCount: 2, // Number of method calls to be displayed
|
||||
errorMethodCount: 8, // Number of method calls if stacktrace is provided
|
||||
lineLength: 300, // Width of the output
|
||||
colors: true, // Colorful log messages
|
||||
printEmojis: true, // Print an emoji for each log message
|
||||
),
|
||||
);
|
||||
0
lib/helper/store_banner_painter.dart
Normal file
0
lib/helper/store_banner_painter.dart
Normal file
16
lib/helper/toaster.dart
Normal file
16
lib/helper/toaster.dart
Normal file
@@ -0,0 +1,16 @@
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
import '../constants/color_constants.dart';
|
||||
|
||||
class Toast{
|
||||
static void showToast(String message){
|
||||
Fluttertoast.showToast(
|
||||
msg: message,
|
||||
textColor: ColorConstants.secondaryColor,
|
||||
timeInSecForIosWeb: 1,
|
||||
webShowClose: true,
|
||||
backgroundColor: ColorConstants.grey,
|
||||
gravity: ToastGravity.TOP_RIGHT,
|
||||
fontSize: 15.0,
|
||||
);
|
||||
}
|
||||
}
|
||||
341
lib/main.dart
Normal file
341
lib/main.dart
Normal file
@@ -0,0 +1,341 @@
|
||||
import 'dart:async';
|
||||
import 'dart:typed_data';
|
||||
import 'package:alp_animated_splashscreen/alp_animated_splashscreen.dart';
|
||||
import 'package:firebase_core/firebase_core.dart';
|
||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:in_app_update/in_app_update.dart';
|
||||
import 'package:nearledaily/constants/color_constants.dart';
|
||||
import 'package:nearledaily/view/authentication/app_update_view.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:webview_flutter/webview_flutter.dart';
|
||||
import 'package:webview_flutter_android/webview_flutter_android.dart';
|
||||
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:new_version_plus/new_version_plus.dart';
|
||||
|
||||
import 'constants/api_constants.dart';
|
||||
import 'controllers/account_controller/faq_controller.dart';
|
||||
import 'controllers/authentication/auth_controller.dart';
|
||||
import 'controllers/cart_controller/cart.dart';
|
||||
import 'controllers/dashboard_controller/dashboard_controller.dart';
|
||||
import 'controllers/intro_controller/intro_screen_controller.dart';
|
||||
import 'controllers/order_controller/create_order_controller.dart';
|
||||
import 'controllers/tenant/get_tenant.dart';
|
||||
import 'controllers/tenant_controller /tenant_list.dart';
|
||||
import 'helper/firebase_options.dart';
|
||||
import 'service/bindings.dart';
|
||||
import 'service/device_info/device_info.dart';
|
||||
import 'view/home_view.dart';
|
||||
import 'view/splash_view/splash_view.dart';
|
||||
|
||||
@pragma('vm:entry-point')
|
||||
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
|
||||
await Firebase.initializeApp();
|
||||
const AndroidInitializationSettings androidSettings =
|
||||
AndroidInitializationSettings('@mipmap/ic_launcher');
|
||||
await flutterLocalNotificationsPlugin.initialize(
|
||||
const InitializationSettings(android: androidSettings),
|
||||
);
|
||||
|
||||
final androidPlugin = flutterLocalNotificationsPlugin
|
||||
.resolvePlatformSpecificImplementation<
|
||||
AndroidFlutterLocalNotificationsPlugin>();
|
||||
await androidPlugin?.createNotificationChannel(
|
||||
const AndroidNotificationChannel(
|
||||
'nearle_channel',
|
||||
'Nearle Notifications',
|
||||
importance: Importance.max,
|
||||
),
|
||||
);
|
||||
|
||||
final data = message.data;
|
||||
final title = data['title'] as String?;
|
||||
final body = data['body'] as String?;
|
||||
|
||||
if (title == null || title.isEmpty) return;
|
||||
|
||||
await flutterLocalNotificationsPlugin.show(
|
||||
999,
|
||||
title,
|
||||
body?.isNotEmpty == true ? body : null,
|
||||
NotificationDetails(
|
||||
android: AndroidNotificationDetails(
|
||||
'nearle_channel',
|
||||
'Nearle Notifications',
|
||||
importance: Importance.max,
|
||||
priority: Priority.high,
|
||||
fullScreenIntent: true,
|
||||
playSound: true,
|
||||
enableVibration: true,
|
||||
largeIcon: const DrawableResourceAndroidBitmap('nearle_logo.jpeg'),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
|
||||
FlutterLocalNotificationsPlugin();
|
||||
|
||||
Future<void> _setupLocalNotifications() async {
|
||||
const AndroidInitializationSettings initializationSettingsAndroid =
|
||||
AndroidInitializationSettings('@mipmap/ic_launcher');
|
||||
const InitializationSettings initializationSettings =
|
||||
InitializationSettings(android: initializationSettingsAndroid);
|
||||
await flutterLocalNotificationsPlugin.initialize(initializationSettings);
|
||||
|
||||
const AndroidNotificationChannel channel = AndroidNotificationChannel(
|
||||
'nearle_channel',
|
||||
'Nearle Notifications',
|
||||
description: 'High priority notifications',
|
||||
importance: Importance.max,
|
||||
playSound: true,
|
||||
enableVibration: true,
|
||||
);
|
||||
|
||||
await flutterLocalNotificationsPlugin
|
||||
.resolvePlatformSpecificImplementation<
|
||||
AndroidFlutterLocalNotificationsPlugin>()
|
||||
?.createNotificationChannel(channel);
|
||||
}
|
||||
|
||||
final RouteObserver<ModalRoute<void>> routeObserver =
|
||||
RouteObserver<ModalRoute<void>>();
|
||||
|
||||
// PRINT CURRENT + STORE VERSION (unchanged)
|
||||
Future<void> _printAndCheckAppVersions() async {
|
||||
try {
|
||||
final PackageInfo packageInfo = await PackageInfo.fromPlatform();
|
||||
final String currentVersion = packageInfo.version;
|
||||
final String buildNumber = packageInfo.buildNumber;
|
||||
final String packageName = packageInfo.packageName;
|
||||
|
||||
print("==================================================");
|
||||
print("CURRENT APP VERSION: $currentVersion");
|
||||
print("BUILD NUMBER: $buildNumber");
|
||||
print("PACKAGE NAME: $packageName");
|
||||
print("==================================================");
|
||||
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString('currentAppVersion', currentVersion);
|
||||
|
||||
if (defaultTargetPlatform == TargetPlatform.android) {
|
||||
final newVersion = NewVersionPlus(
|
||||
androidId: "com.nearle.gear",
|
||||
);
|
||||
|
||||
final status = await newVersion.getVersionStatus();
|
||||
if (status != null) {
|
||||
print("PLAY STORE VERSION: ${status.storeVersion}");
|
||||
print("CAN UPDATE: ${status.canUpdate}");
|
||||
print("APP STORE LINK: ${status.appStoreLink}");
|
||||
print("==================================================");
|
||||
|
||||
await prefs.setString('storeAppVersion', status.storeVersion);
|
||||
await prefs.setBool('updateAvailable', status.canUpdate);
|
||||
} else {
|
||||
print("Could not fetch Play Store version");
|
||||
await prefs.setString('storeAppVersion', 'Unknown');
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
print("Version check error: $e");
|
||||
}
|
||||
}
|
||||
|
||||
// FIXED: Now correctly detects real update from Play Store
|
||||
Future<bool> _checkForAppUpdate() async {
|
||||
if (defaultTargetPlatform != TargetPlatform.android) return false;
|
||||
|
||||
try {
|
||||
final newVersion = NewVersionPlus(androidId: "com.nearle.gear");
|
||||
final status = await newVersion.getVersionStatus();
|
||||
|
||||
if (status != null && status.canUpdate) {
|
||||
print("UPDATE DETECTED! Redirecting to AppUpdateView");
|
||||
print("Current: ${status.localVersion} → Store: ${status.storeVersion}");
|
||||
return true; // This will now go to AppUpdateView
|
||||
}
|
||||
} catch (e) {
|
||||
print("Version check failed (continuing anyway): $e");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void _handleMessage(RemoteMessage message) {
|
||||
print("Notification tapped: ${message.data}");
|
||||
}
|
||||
|
||||
class AnimatedSplashWithNavigation extends StatefulWidget {
|
||||
final Widget nextScreen;
|
||||
const AnimatedSplashWithNavigation({super.key, required this.nextScreen});
|
||||
|
||||
@override
|
||||
State<AnimatedSplashWithNavigation> createState() =>
|
||||
_AnimatedSplashWithNavigationState();
|
||||
}
|
||||
|
||||
class _AnimatedSplashWithNavigationState
|
||||
extends State<AnimatedSplashWithNavigation> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
Future.delayed(const Duration(milliseconds: 5200), () {
|
||||
if (mounted) {
|
||||
Get.offAll(() => widget.nextScreen);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedSplashScreen(
|
||||
logo: 'assets/images/splashimg.png',
|
||||
brandname: 'Nearle Daily',
|
||||
backgroundcolor: Colors.white,
|
||||
foregroundcolor: ColorConstants.primaryColor,
|
||||
brandnamecolor: ColorConstants.primaryColor,
|
||||
// companyname: 'Nearle Daily',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
FlutterError.onError = (details) => FlutterError.dumpErrorToConsole(details);
|
||||
|
||||
await runZonedGuarded(() async {
|
||||
|
||||
await Firebase.initializeApp();
|
||||
|
||||
await _printAndCheckAppVersions();
|
||||
|
||||
SystemChrome.setSystemUIOverlayStyle(
|
||||
SystemUiOverlayStyle(statusBarColor: Colors.grey[200]),
|
||||
);
|
||||
|
||||
await _setupLocalNotifications();
|
||||
FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
|
||||
|
||||
RemoteMessage? initialMessage =
|
||||
await FirebaseMessaging.instance.getInitialMessage();
|
||||
if (initialMessage != null) _handleMessage(initialMessage);
|
||||
|
||||
FirebaseMessaging.onMessageOpenedApp.listen(_handleMessage);
|
||||
|
||||
if (!kIsWeb) {
|
||||
await FirebaseMessaging.instance.requestPermission(
|
||||
alert: true,
|
||||
badge: true,
|
||||
sound: true,
|
||||
);
|
||||
}
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
prefs.setBool("firstTime", true);
|
||||
|
||||
FirebaseMessaging.onMessage.listen((RemoteMessage message) async {
|
||||
final data = message.data;
|
||||
final title = data['title'] ?? message.notification?.title ?? 'Nearle';
|
||||
final body = data['body'] ?? message.notification?.body ?? '';
|
||||
|
||||
final ByteData jpegData =
|
||||
await rootBundle.load('assets/images/nearledaily.png');
|
||||
final Uint8List jpegBytes = jpegData.buffer.asUint8List();
|
||||
|
||||
await flutterLocalNotificationsPlugin.show(
|
||||
DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
||||
title,
|
||||
body,
|
||||
NotificationDetails(
|
||||
android: AndroidNotificationDetails(
|
||||
'nearle_channel',
|
||||
'Nearle Notifications',
|
||||
importance: Importance.max,
|
||||
priority: Priority.high,
|
||||
fullScreenIntent: true,
|
||||
playSound: true,
|
||||
enableVibration: true,
|
||||
largeIcon: ByteArrayAndroidBitmap(jpegBytes),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
String? fcmToken = await FirebaseMessaging.instance.getToken();
|
||||
await prefs.setString('fcmToken', fcmToken ?? '');
|
||||
|
||||
if (defaultTargetPlatform == TargetPlatform.android) {
|
||||
final params = PlatformWebViewControllerCreationParams();
|
||||
AndroidWebViewController(params);
|
||||
}
|
||||
|
||||
ApiConstants.tenantCustomers = ApiConstants.tenantCustomerLive;
|
||||
ApiConstants.orderedtenantCustomers = ApiConstants.orderedtenantCustomerLive;
|
||||
ApiConstants.login = ApiConstants.loginLive;
|
||||
|
||||
Get.put(TenantController(), permanent: true);
|
||||
Get.lazyPut(() => AuthController(), fenix: true);
|
||||
Get.lazyPut(() => DashboardController(), fenix: true);
|
||||
Get.lazyPut(() => CartController(), fenix: true);
|
||||
Get.lazyPut(() => BottomNavController(), fenix: true);
|
||||
Get.lazyPut(() => OrderedTenantController(), fenix: true);
|
||||
Get.lazyPut(() => OrderController(), fenix: true);
|
||||
Get.lazyPut(() => FaqController(), fenix: true);
|
||||
Get.lazyPut(() => IntroScreenController(), fenix: true);
|
||||
|
||||
DeviceInfo deviceInfo = DeviceInfo();
|
||||
await deviceInfo.getDeviceInfo();
|
||||
|
||||
await SystemChrome.setPreferredOrientations([
|
||||
DeviceOrientation.portraitUp,
|
||||
DeviceOrientation.portraitDown,
|
||||
]);
|
||||
|
||||
final int? customerId = prefs.getInt('customerId');
|
||||
final String? contactNo = prefs.getString('contactno');
|
||||
bool updateAvailable = await _checkForAppUpdate(); // Now works correctly!
|
||||
|
||||
Widget nextScreen;
|
||||
if (updateAvailable) {
|
||||
nextScreen = const AppUpdateView();
|
||||
} else if (customerId != null && contactNo != null && contactNo.isNotEmpty) {
|
||||
nextScreen = BottomNavigation();
|
||||
} else {
|
||||
nextScreen = const SplashScreenView();
|
||||
}
|
||||
SystemChrome.setSystemUIOverlayStyle(
|
||||
const SystemUiOverlayStyle(
|
||||
statusBarColor: Colors.white, // or transparent
|
||||
statusBarIconBrightness: Brightness.dark, // Android
|
||||
statusBarBrightness: Brightness.light, // iOS
|
||||
),
|
||||
);
|
||||
|
||||
runApp(MyApp(startScreen: nextScreen));
|
||||
}, (error, stack) => print('Error: $error'));
|
||||
}
|
||||
|
||||
class MyApp extends StatelessWidget {
|
||||
final Widget startScreen;
|
||||
const MyApp({super.key, required this.startScreen});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GetMaterialApp(
|
||||
debugShowCheckedModeBanner: false,
|
||||
title: 'Nearle Daily',
|
||||
navigatorObservers: [routeObserver],
|
||||
theme: ThemeData(
|
||||
fontFamily: 'Proxima Nova',
|
||||
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
|
||||
),
|
||||
initialBinding: GlobalBinding(),
|
||||
home: AnimatedSplashWithNavigation(nextScreen: startScreen),
|
||||
);
|
||||
}
|
||||
}
|
||||
336
lib/modules/authentication/auth.dart
Normal file
336
lib/modules/authentication/auth.dart
Normal file
@@ -0,0 +1,336 @@
|
||||
class Authentication {
|
||||
final String? customerid;
|
||||
final String? firstname;
|
||||
final String? lastname;
|
||||
final String? profileimage;
|
||||
final String? gender;
|
||||
final String? dob;
|
||||
final String? dialcode;
|
||||
final String? contactno;
|
||||
final String? email;
|
||||
final String? deviceid;
|
||||
final String? devicetype;
|
||||
final int? authmode;
|
||||
final int? configid;
|
||||
final String? customertoken;
|
||||
final String? address;
|
||||
final String? suburb;
|
||||
final String? city;
|
||||
final String? state;
|
||||
final String? landmark;
|
||||
final String? doorno;
|
||||
final String? postcode;
|
||||
final String? latitude;
|
||||
final String? longitude;
|
||||
final int? applocationid;
|
||||
final int? locationid;
|
||||
final String? defaultaddress;
|
||||
final int? primaryaddress;
|
||||
final int? tenantid;
|
||||
final int? status;
|
||||
final String? intro;
|
||||
|
||||
// 🆕 Additional fields specific to Profile API
|
||||
final int? deliverylocationid;
|
||||
final int? allocationid;
|
||||
final int? tenantlocationid;
|
||||
final String? selectedlatitude;
|
||||
final String? selectedlongitude;
|
||||
final String? radius;
|
||||
final int? qrmode;
|
||||
|
||||
const Authentication({
|
||||
this.customerid,
|
||||
this.firstname,
|
||||
this.lastname,
|
||||
this.profileimage,
|
||||
this.gender,
|
||||
this.dob,
|
||||
this.dialcode,
|
||||
this.contactno,
|
||||
this.email,
|
||||
this.deviceid,
|
||||
this.devicetype,
|
||||
this.authmode,
|
||||
this.configid,
|
||||
this.customertoken,
|
||||
this.address,
|
||||
this.suburb,
|
||||
this.city,
|
||||
this.state,
|
||||
this.landmark,
|
||||
this.doorno,
|
||||
this.postcode,
|
||||
this.latitude,
|
||||
this.longitude,
|
||||
this.applocationid,
|
||||
this.locationid,
|
||||
this.defaultaddress,
|
||||
this.primaryaddress,
|
||||
this.tenantid,
|
||||
this.status,
|
||||
this.intro,
|
||||
this.deliverylocationid,
|
||||
this.allocationid,
|
||||
this.tenantlocationid,
|
||||
this.selectedlatitude,
|
||||
this.selectedlongitude,
|
||||
this.radius,
|
||||
this.qrmode,
|
||||
});
|
||||
|
||||
factory Authentication.fromJson(Map<String, dynamic> json) {
|
||||
return Authentication(
|
||||
customerid: json['customerid']?.toString(),
|
||||
firstname: json['firstname'],
|
||||
lastname: json['lastname'],
|
||||
profileimage: json['profileimage'],
|
||||
gender: json['gender'],
|
||||
dob: json['dob'],
|
||||
dialcode: json['dialcode'],
|
||||
contactno: json['contactno'],
|
||||
email: json['email'],
|
||||
deviceid: json['deviceid'],
|
||||
devicetype: json['devicetype'],
|
||||
authmode: json['authmode'],
|
||||
configid: json['configid'],
|
||||
customertoken: json['customertoken'],
|
||||
address: json['address'],
|
||||
suburb: json['suburb'],
|
||||
city: json['city'],
|
||||
state: json['state'],
|
||||
landmark: json['landmark'],
|
||||
doorno: json['doorno'],
|
||||
postcode: json['postcode'],
|
||||
latitude: json['latitude'],
|
||||
longitude: json['longitude'],
|
||||
applocationid: json['applocationid'],
|
||||
locationid: json['locationid'],
|
||||
defaultaddress: json['defaultaddress'],
|
||||
primaryaddress: json['primaryaddress'],
|
||||
tenantid: json['tenantid'],
|
||||
status: json['status'],
|
||||
intro: json['intro'],
|
||||
);
|
||||
}
|
||||
|
||||
/// 🏠 Factory for location API
|
||||
factory Authentication.fromLocationJson(Map<String, dynamic> json) {
|
||||
return Authentication(
|
||||
customerid: json['customerid']?.toString(),
|
||||
address: json['address'],
|
||||
suburb: json['suburb'],
|
||||
city: json['city'],
|
||||
state: json['state'],
|
||||
landmark: json['landmark'],
|
||||
doorno: json['doorno'],
|
||||
postcode: json['postcode'],
|
||||
latitude: json['latitude'],
|
||||
longitude: json['longitude'],
|
||||
locationid: json['locationid'],
|
||||
primaryaddress: json['primaryaddress'],
|
||||
status: json['status'],
|
||||
);
|
||||
}
|
||||
|
||||
/// 🆕 Factory for Profile API
|
||||
factory Authentication.fromProfileJson(Map<String, dynamic> json) {
|
||||
final details = json['details'] ?? {};
|
||||
|
||||
return Authentication(
|
||||
customerid: details['customerid']?.toString(),
|
||||
firstname: details['firstname'],
|
||||
lastname: details['lastname'],
|
||||
profileimage: details['profileimage'],
|
||||
gender: details['gender'],
|
||||
dob: details['dob'],
|
||||
dialcode: details['dialcode'],
|
||||
contactno: details['contactno'],
|
||||
email: details['email'],
|
||||
deviceid: details['deviceid'],
|
||||
devicetype: details['devicetype'],
|
||||
authmode: details['authmode'],
|
||||
configid: details['configid'],
|
||||
customertoken: details['customertoken'],
|
||||
address: details['address'],
|
||||
suburb: details['suburb'],
|
||||
city: details['city'],
|
||||
state: details['state'],
|
||||
landmark: details['landmark'],
|
||||
doorno: details['doorno'],
|
||||
postcode: details['postcode'],
|
||||
latitude: details['latitude'],
|
||||
longitude: details['longitude'],
|
||||
applocationid: details['applocationid'],
|
||||
primaryaddress: details['primaryaddress'],
|
||||
tenantid: details['tenantid'],
|
||||
status: details['status'],
|
||||
intro: details['intro'],
|
||||
deliverylocationid: details['deliverylocationid'],
|
||||
allocationid: details['allocationid'],
|
||||
tenantlocationid: details['tenantlocationid'],
|
||||
selectedlatitude: details['selectedlatitude'],
|
||||
selectedlongitude: details['selectedlongitude'],
|
||||
radius: details['radius'],
|
||||
qrmode: details['qrmode'],
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'customerid': customerid,
|
||||
'firstname': firstname,
|
||||
'lastname': lastname,
|
||||
'profileimage': profileimage,
|
||||
'gender': gender,
|
||||
'dob': dob,
|
||||
'dialcode': dialcode,
|
||||
'contactno': contactno,
|
||||
'email': email,
|
||||
'deviceid': deviceid,
|
||||
'devicetype': devicetype,
|
||||
'authmode': authmode,
|
||||
'configid': configid,
|
||||
'customertoken': customertoken,
|
||||
'address': address,
|
||||
'suburb': suburb,
|
||||
'city': city,
|
||||
'state': state,
|
||||
'landmark': landmark,
|
||||
'doorno': doorno,
|
||||
'postcode': postcode,
|
||||
'latitude': latitude,
|
||||
'longitude': longitude,
|
||||
'applocationid': applocationid,
|
||||
'locationid': locationid,
|
||||
'defaultaddress': defaultaddress,
|
||||
'primaryaddress': primaryaddress,
|
||||
'tenantid': tenantid,
|
||||
'status': status,
|
||||
'intro': intro,
|
||||
'deliverylocationid': deliverylocationid,
|
||||
'allocationid': allocationid,
|
||||
'tenantlocationid': tenantlocationid,
|
||||
'selectedlatitude': selectedlatitude,
|
||||
'selectedlongitude': selectedlongitude,
|
||||
'radius': radius,
|
||||
'qrmode': qrmode,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class Customer {
|
||||
final int customerId;
|
||||
final String firstName;
|
||||
final String lastName;
|
||||
final String profileImage;
|
||||
final String gender;
|
||||
final String dob;
|
||||
final String dialCode;
|
||||
final String contactNo;
|
||||
final String email;
|
||||
final String deviceId;
|
||||
final String deviceType;
|
||||
final int authMode;
|
||||
final int configId;
|
||||
final String customerToken;
|
||||
final int deliveryLocationId;
|
||||
final String address;
|
||||
final String suburb;
|
||||
final String city;
|
||||
final String state;
|
||||
final String landmark;
|
||||
final String doorNo;
|
||||
final String postcode;
|
||||
final String latitude;
|
||||
final String longitude;
|
||||
final int appLocationId;
|
||||
final int allocationId;
|
||||
final int primaryAddress;
|
||||
final int tenantLocationId;
|
||||
final int tenantId;
|
||||
final int status;
|
||||
final String intro;
|
||||
final String selectedLatitude;
|
||||
final String selectedLongitude;
|
||||
final String radius;
|
||||
final int qrMode;
|
||||
|
||||
Customer({
|
||||
required this.customerId,
|
||||
required this.firstName,
|
||||
required this.lastName,
|
||||
required this.profileImage,
|
||||
required this.gender,
|
||||
required this.dob,
|
||||
required this.dialCode,
|
||||
required this.contactNo,
|
||||
required this.email,
|
||||
required this.deviceId,
|
||||
required this.deviceType,
|
||||
required this.authMode,
|
||||
required this.configId,
|
||||
required this.customerToken,
|
||||
required this.deliveryLocationId,
|
||||
required this.address,
|
||||
required this.suburb,
|
||||
required this.city,
|
||||
required this.state,
|
||||
required this.landmark,
|
||||
required this.doorNo,
|
||||
required this.postcode,
|
||||
required this.latitude,
|
||||
required this.longitude,
|
||||
required this.appLocationId,
|
||||
required this.allocationId,
|
||||
required this.primaryAddress,
|
||||
required this.tenantLocationId,
|
||||
required this.tenantId,
|
||||
required this.status,
|
||||
required this.intro,
|
||||
required this.selectedLatitude,
|
||||
required this.selectedLongitude,
|
||||
required this.radius,
|
||||
required this.qrMode,
|
||||
});
|
||||
|
||||
factory Customer.fromJson(Map<String, dynamic> json) {
|
||||
return Customer(
|
||||
customerId: json['customerid'] ?? 0,
|
||||
firstName: json['firstname'] ?? '',
|
||||
lastName: json['lastname'] ?? '',
|
||||
profileImage: json['profileimage'] ?? '',
|
||||
gender: json['gender'] ?? '',
|
||||
dob: json['dob'] ?? '',
|
||||
dialCode: json['dialcode'] ?? '',
|
||||
contactNo: json['contactno'] ?? '',
|
||||
email: json['email'] ?? '',
|
||||
deviceId: json['deviceid'] ?? '',
|
||||
deviceType: json['devicetype'] ?? '',
|
||||
authMode: json['authmode'] ?? 0,
|
||||
configId: json['configid'] ?? 0,
|
||||
customerToken: json['customertoken'] ?? '',
|
||||
deliveryLocationId: json['deliverylocationid'] ?? 0,
|
||||
address: json['address'] ?? '',
|
||||
suburb: json['suburb'] ?? '',
|
||||
city: json['city'] ?? '',
|
||||
state: json['state'] ?? '',
|
||||
landmark: json['landmark'] ?? '',
|
||||
doorNo: json['doorno'] ?? '',
|
||||
postcode: json['postcode'] ?? '',
|
||||
latitude: json['latitude'] ?? '',
|
||||
longitude: json['longitude'] ?? '',
|
||||
appLocationId: json['applocationid'] ?? 0,
|
||||
allocationId: json['allocationid'] ?? 0,
|
||||
primaryAddress: json['primaryaddress'] ?? 0,
|
||||
tenantLocationId: json['tenantlocationid'] ?? 0,
|
||||
tenantId: json['tenantid'] ?? 0,
|
||||
status: json['status'] ?? 0,
|
||||
intro: json['intro'] ?? '',
|
||||
selectedLatitude: json['selectedlatitude'] ?? '',
|
||||
selectedLongitude: json['selectedlongitude'] ?? '',
|
||||
radius: json['radius'] ?? '',
|
||||
qrMode: json['qrmode'] ?? 0,
|
||||
);
|
||||
}
|
||||
}
|
||||
126
lib/modules/authentication/getbyid.dart
Normal file
126
lib/modules/authentication/getbyid.dart
Normal file
@@ -0,0 +1,126 @@
|
||||
class Customerrequest {
|
||||
final List<CustomerFullView>? customerFullView;
|
||||
|
||||
Customerrequest({
|
||||
this.customerFullView,
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
||||
class CustomerFullView {
|
||||
final int? customerid;
|
||||
final String? firstname;
|
||||
final dynamic lastname;
|
||||
final String? profileimage;
|
||||
final String? gender;
|
||||
final DateTime? dob;
|
||||
final String? dialcode;
|
||||
final String? contactno;
|
||||
final String? email;
|
||||
final String? deviceid;
|
||||
final String? devicetype;
|
||||
final int? authmode;
|
||||
final int? configid;
|
||||
final String? customertoken;
|
||||
final int? deliverylocationid;
|
||||
final String? address;
|
||||
final String? suburb;
|
||||
final String? city;
|
||||
final String? state;
|
||||
final String? landmark;
|
||||
final String? doorno;
|
||||
final String? postcode;
|
||||
final String? latitude;
|
||||
final String? longitude;
|
||||
final int? applocationid;
|
||||
final int? allocationid;
|
||||
final int? primaryaddress;
|
||||
final int? tenantlocationid;
|
||||
final int? tenantid;
|
||||
final int? status;
|
||||
final dynamic intro;
|
||||
final String? selectedlatitude;
|
||||
final String? selectedlongitude;
|
||||
final int? radius;
|
||||
final int? qrmode;
|
||||
|
||||
CustomerFullView({
|
||||
this.customerid,
|
||||
this.firstname,
|
||||
this.lastname,
|
||||
this.profileimage,
|
||||
this.gender,
|
||||
this.dob,
|
||||
this.dialcode,
|
||||
this.contactno,
|
||||
this.email,
|
||||
this.deviceid,
|
||||
this.devicetype,
|
||||
this.authmode,
|
||||
this.configid,
|
||||
this.customertoken,
|
||||
this.deliverylocationid,
|
||||
this.address,
|
||||
this.suburb,
|
||||
this.city,
|
||||
this.state,
|
||||
this.landmark,
|
||||
this.doorno,
|
||||
this.postcode,
|
||||
this.latitude,
|
||||
this.longitude,
|
||||
this.applocationid,
|
||||
this.allocationid,
|
||||
this.primaryaddress,
|
||||
this.tenantlocationid,
|
||||
this.tenantid,
|
||||
this.status,
|
||||
this.intro,
|
||||
this.selectedlatitude,
|
||||
this.selectedlongitude,
|
||||
this.radius,
|
||||
this.qrmode,
|
||||
});
|
||||
|
||||
factory CustomerFullView.fromJson(Map<String, dynamic> json) {
|
||||
return CustomerFullView(
|
||||
customerid: json['customerid'] as int?,
|
||||
firstname: json['firstname'] as String?,
|
||||
lastname: json['lastname'],
|
||||
profileimage: json['profileimage'] as String?,
|
||||
gender: json['gender'] as String?,
|
||||
dob: json['dob'] != null ? DateTime.tryParse(json['dob']) : null,
|
||||
dialcode: json['dialcode'] as String?,
|
||||
contactno: json['contactno'] as String?,
|
||||
email: json['email'] as String?,
|
||||
deviceid: json['deviceid'] as String?,
|
||||
devicetype: json['devicetype'] as String?,
|
||||
authmode: json['authmode'] as int?,
|
||||
configid: json['configid'] as int?,
|
||||
customertoken: json['customertoken'] as String?,
|
||||
deliverylocationid: json['deliverylocationid'] as int?,
|
||||
address: json['address'] as String?,
|
||||
suburb: json['suburb'] as String?,
|
||||
city: json['city'] as String?,
|
||||
state: json['state'] as String?,
|
||||
landmark: json['landmark'] as String?,
|
||||
doorno: json['doorno'] as String?,
|
||||
postcode: json['postcode'] as String?,
|
||||
latitude: json['latitude'] as String?,
|
||||
longitude: json['longitude'] as String?,
|
||||
applocationid: json['applocationid'] as int?,
|
||||
allocationid: json['allocationid'] as int?,
|
||||
primaryaddress: json['primaryaddress'] as int?,
|
||||
tenantlocationid: json['tenantlocationid'] as int?,
|
||||
tenantid: json['tenantid'] as int?,
|
||||
status: json['status'] as int?,
|
||||
intro: json['intro'],
|
||||
selectedlatitude: json['selectedlatitude'] as String?,
|
||||
selectedlongitude: json['selectedlongitude'] as String?,
|
||||
radius: json['radius'] as int?,
|
||||
qrmode: json['qrmode'] as int?,
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
223
lib/modules/orders/create_order.dart
Normal file
223
lib/modules/orders/create_order.dart
Normal file
@@ -0,0 +1,223 @@
|
||||
// lib/modules/orders/create_order.dart
|
||||
import 'dart:convert';
|
||||
|
||||
CreateOrderRequest createOrderRequestFromJson(String str) =>
|
||||
CreateOrderRequest.fromJson(json.decode(str));
|
||||
|
||||
String createOrderRequestToJson(CreateOrderRequest data) =>
|
||||
json.encode(data.toJson());
|
||||
|
||||
CreateOrderResponse createOrderResponseFromJson(String str) =>
|
||||
CreateOrderResponse.fromJson(json.decode(str));
|
||||
|
||||
|
||||
|
||||
class CreateOrderRequest {
|
||||
final CreateOrder? orders;
|
||||
|
||||
CreateOrderRequest({this.orders});
|
||||
|
||||
factory CreateOrderRequest.fromJson(Map<String, dynamic> json) =>
|
||||
CreateOrderRequest(
|
||||
orders: json["orders"] == null ? null : CreateOrder.fromJson(json["orders"]),
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
"orders": orders?.toJson(),
|
||||
};
|
||||
}
|
||||
|
||||
class CreateOrder {
|
||||
final int? applocationid;
|
||||
final String? applocation;
|
||||
final int? tenantid;
|
||||
final int? partnerid;
|
||||
final int? locationid;
|
||||
final int? categoryid;
|
||||
final int? subcategoryid;
|
||||
final int? moduleid;
|
||||
final int? configid;
|
||||
final String? orderdate;
|
||||
final String? deliverydate;
|
||||
final String? orderstatus;
|
||||
final double? deliverycharge;
|
||||
final int? customerid;
|
||||
final String? pickupcustomer;
|
||||
final String? pickupcontactno;
|
||||
final String? pickupaddress;
|
||||
final int? pickuplocationid;
|
||||
final String? pickupcity;
|
||||
final String? deliverycustomer;
|
||||
final String? deliverycontactno;
|
||||
final String? deliveryaddress;
|
||||
final int? deliverylocationid;
|
||||
final String? deliverylat;
|
||||
final String? deliverylong;
|
||||
final int? paymenttype;
|
||||
final List<OrderItem>? items;
|
||||
|
||||
CreateOrder({
|
||||
this.applocationid,
|
||||
this.applocation,
|
||||
this.tenantid,
|
||||
this.partnerid,
|
||||
this.locationid,
|
||||
this.categoryid,
|
||||
this.subcategoryid,
|
||||
this.moduleid,
|
||||
this.configid,
|
||||
this.orderdate,
|
||||
this.deliverydate,
|
||||
this.orderstatus,
|
||||
this.deliverycharge,
|
||||
this.customerid,
|
||||
this.pickupcustomer,
|
||||
this.pickupcontactno,
|
||||
this.pickupaddress,
|
||||
this.pickuplocationid,
|
||||
this.pickupcity,
|
||||
this.deliverycustomer,
|
||||
this.deliverycontactno,
|
||||
this.deliveryaddress,
|
||||
this.deliverylocationid,
|
||||
this.deliverylat,
|
||||
this.deliverylong,
|
||||
this.paymenttype,
|
||||
this.items,
|
||||
});
|
||||
|
||||
factory CreateOrder.fromJson(Map<String, dynamic> json) => CreateOrder(
|
||||
applocationid: json["applocationid"],
|
||||
applocation: json["applocation"],
|
||||
tenantid: json["tenantid"],
|
||||
partnerid: json["partnerid"],
|
||||
locationid: json["locationid"],
|
||||
categoryid: json["categoryid"],
|
||||
subcategoryid: json["subcategoryid"],
|
||||
moduleid: json["moduleid"],
|
||||
configid: json["configid"],
|
||||
orderdate: json["orderdate"],
|
||||
deliverydate: json["deliverydate"],
|
||||
orderstatus: json["orderstatus"],
|
||||
deliverycharge: json["deliverycharge"]?.toDouble(),
|
||||
customerid: json["customerid"],
|
||||
pickupcustomer: json["pickupcustomer"],
|
||||
pickupcontactno: json["pickupcontactno"],
|
||||
pickupaddress: json["pickupaddress"],
|
||||
pickuplocationid: json["pickuplocationid"],
|
||||
pickupcity: json["pickupcity"],
|
||||
deliverycustomer: json["deliverycustomer"],
|
||||
deliverycontactno: json["deliverycontactno"],
|
||||
deliveryaddress: json["deliveryaddress"],
|
||||
deliverylocationid: json["deliverylocationid"],
|
||||
deliverylat: json["deliverylat"],
|
||||
deliverylong: json["deliverylong"],
|
||||
paymenttype: json["paymenttype"],
|
||||
items: json["items"] == null
|
||||
? []
|
||||
: List<OrderItem>.from(json["items"].map((x) => OrderItem.fromJson(x))),
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
"applocationid": applocationid,
|
||||
"applocation": applocation,
|
||||
"tenantid": tenantid,
|
||||
"partnerid": partnerid,
|
||||
"locationid": locationid,
|
||||
"categoryid": categoryid,
|
||||
"subcategoryid": subcategoryid,
|
||||
"moduleid": moduleid,
|
||||
"configid": configid,
|
||||
"orderdate": orderdate,
|
||||
"deliverydate": deliverydate,
|
||||
"orderstatus": orderstatus,
|
||||
"deliverycharge": deliverycharge,
|
||||
"customerid": customerid,
|
||||
"pickupcustomer": pickupcustomer,
|
||||
"pickupcontactno": pickupcontactno,
|
||||
"pickupaddress": pickupaddress,
|
||||
"pickuplocationid": pickuplocationid,
|
||||
"pickupcity": pickupcity,
|
||||
"deliverycustomer": deliverycustomer,
|
||||
"deliverycontactno": deliverycontactno,
|
||||
"deliveryaddress": deliveryaddress,
|
||||
"deliverylocationid": deliverylocationid,
|
||||
"deliverylat": deliverylat,
|
||||
"deliverylong": deliverylong,
|
||||
"paymenttype": paymenttype,
|
||||
"items": items?.map((x) => x.toJson()).toList(),
|
||||
};
|
||||
}
|
||||
|
||||
class OrderItem {
|
||||
final int? productid;
|
||||
final String? productname;
|
||||
final String? productdescription;
|
||||
final int? orderqty;
|
||||
final double? price;
|
||||
final double? discount;
|
||||
final double? tax;
|
||||
final double? tenantfee;
|
||||
final int? unitid;
|
||||
final String? unitname;
|
||||
final double? productsumprice;
|
||||
|
||||
|
||||
OrderItem({
|
||||
this.productid,
|
||||
this.productname,
|
||||
this.productdescription,
|
||||
this.orderqty,
|
||||
this.price,
|
||||
this.discount,
|
||||
this.tenantfee,
|
||||
this.tax,
|
||||
this.unitid,
|
||||
this.unitname,
|
||||
this.productsumprice,
|
||||
|
||||
});
|
||||
|
||||
factory OrderItem.fromJson(Map<String, dynamic> json) => OrderItem(
|
||||
productid: json["productid"],
|
||||
productname: json["productname"],
|
||||
productdescription: json["productdescription"],
|
||||
orderqty: json["orderqty"],
|
||||
price: json["price"]?.toDouble(),
|
||||
tenantfee: json["tenantfee"]?.toDouble(),
|
||||
|
||||
unitid: json["unitid"],
|
||||
unitname: json["unitname"],
|
||||
productsumprice: json["productsumprice"]?.toDouble(),
|
||||
discount: json["discount"]?.toDouble(),
|
||||
tax: json["tax"]?.toDouble(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
"productid": productid,
|
||||
"productname": productname,
|
||||
"productdescription": productdescription,
|
||||
"orderqty": orderqty,
|
||||
"price": price,
|
||||
"discount": discount,
|
||||
"tax": tax,
|
||||
"tenantfee": tenantfee,
|
||||
"unitid": unitid,
|
||||
"unitname": unitname,
|
||||
"productsumprice": productsumprice,
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
class CreateOrderResponse {
|
||||
final String? status;
|
||||
final int? message_id;
|
||||
|
||||
CreateOrderResponse({this.status, this.message_id});
|
||||
|
||||
factory CreateOrderResponse.fromJson(Map<String, dynamic> json) =>
|
||||
CreateOrderResponse(
|
||||
status: json["status"],
|
||||
message_id: json["message_id"],
|
||||
);
|
||||
}
|
||||
223
lib/modules/orders/getcustomerorders.dart
Normal file
223
lib/modules/orders/getcustomerorders.dart
Normal file
@@ -0,0 +1,223 @@
|
||||
class OrderResponse {
|
||||
final List<Order> orders;
|
||||
|
||||
OrderResponse({required this.orders});
|
||||
|
||||
factory OrderResponse.fromJson(Map<String, dynamic> json) {
|
||||
return OrderResponse(
|
||||
orders: (json['orders'] as List? ?? [])
|
||||
.map((e) => Order.fromJson(e))
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class Order {
|
||||
final int orderheaderid;
|
||||
final String orderid;
|
||||
final String orderstatus;
|
||||
final DateTime? orderdate;
|
||||
final String? ordernotes;
|
||||
|
||||
final String? deliverytime;
|
||||
final String? pending;
|
||||
final String? processing;
|
||||
final String? ready;
|
||||
final String? delivered;
|
||||
final String? cancelled;
|
||||
|
||||
final double? deliverycharge;
|
||||
final String? kms;
|
||||
|
||||
final String? pickupaddress;
|
||||
final String? pickupcustomer;
|
||||
final String? pickupcontactno;
|
||||
final String? pickupcity;
|
||||
|
||||
final String? deliveryaddress;
|
||||
final String? deliverylat;
|
||||
final String? deliverylong;
|
||||
final String? deliverycustomer;
|
||||
final String? deliverycontactno;
|
||||
|
||||
// ✅ Flat tenant fields (used directly in UI)
|
||||
final String? tenantname;
|
||||
final String? tenanttoken;
|
||||
final String? tenantcontactno;
|
||||
final String? tenantpostcode;
|
||||
final String? tenantsuburb;
|
||||
final String? tenantcity;
|
||||
final String? tenantimage;
|
||||
final String? registrationno;
|
||||
final String? gstno;
|
||||
|
||||
// ✅ Flat tenant location fields
|
||||
final String? locationname;
|
||||
final String? locationcontactno;
|
||||
final String? locationpostcode;
|
||||
final String? locationsuburb;
|
||||
final String? locationcity;
|
||||
|
||||
// ✅ App location
|
||||
final String? applocationname;
|
||||
|
||||
// ✅ Financial
|
||||
final double? totaltaxamount;
|
||||
|
||||
// ✅ Order details as a LIST (UI iterates over this)
|
||||
final List<OrderDetail>? orderdetails;
|
||||
|
||||
Order({
|
||||
required this.orderheaderid,
|
||||
required this.orderid,
|
||||
required this.orderstatus,
|
||||
this.orderdate,
|
||||
this.ordernotes,
|
||||
this.deliverytime,
|
||||
this.pending,
|
||||
this.processing,
|
||||
this.ready,
|
||||
this.delivered,
|
||||
this.cancelled,
|
||||
this.deliverycharge,
|
||||
this.kms,
|
||||
this.pickupaddress,
|
||||
this.pickupcustomer,
|
||||
this.pickupcontactno,
|
||||
this.pickupcity,
|
||||
this.deliveryaddress,
|
||||
this.deliverylat,
|
||||
this.deliverylong,
|
||||
this.deliverycustomer,
|
||||
this.deliverycontactno,
|
||||
this.tenantname,
|
||||
this.tenanttoken,
|
||||
this.tenantcontactno,
|
||||
this.tenantpostcode,
|
||||
this.tenantsuburb,
|
||||
this.tenantcity,
|
||||
this.tenantimage,
|
||||
this.registrationno,
|
||||
this.gstno,
|
||||
this.locationname,
|
||||
this.locationcontactno,
|
||||
this.locationpostcode,
|
||||
this.locationsuburb,
|
||||
this.locationcity,
|
||||
this.applocationname,
|
||||
this.totaltaxamount,
|
||||
this.orderdetails,
|
||||
});
|
||||
|
||||
factory Order.fromJson(Map<String, dynamic> json) {
|
||||
// ✅ Safely extract nested objects if present
|
||||
final tenant = json['tenant'] as Map<String, dynamic>? ?? {};
|
||||
final tenantlocation = json['tenantlocation'] as Map<String, dynamic>? ?? {};
|
||||
final applocation = json['applocation'] as Map<String, dynamic>? ?? {};
|
||||
|
||||
// ✅ Parse orderdate safely
|
||||
DateTime? parsedDate;
|
||||
final rawDate = json['orderdate'];
|
||||
if (rawDate != null && rawDate.toString().isNotEmpty) {
|
||||
try {
|
||||
parsedDate = DateTime.parse(rawDate.toString());
|
||||
} catch (_) {
|
||||
parsedDate = null;
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ Safe parser — API may return orderdetails as {}, [], or null
|
||||
List<OrderDetail> parseOrderDetails(dynamic raw) {
|
||||
if (raw == null) return [];
|
||||
if (raw is List) return raw.map((e) => OrderDetail.fromJson(e as Map<String, dynamic>)).toList();
|
||||
if (raw is Map<String, dynamic>) return [OrderDetail.fromJson(raw)];
|
||||
return [];
|
||||
}
|
||||
|
||||
return Order(
|
||||
orderheaderid: json['orderheaderid'] ?? 0,
|
||||
orderid: json['orderid'] ?? '',
|
||||
orderstatus: json['orderstatus'] ?? 'pending',
|
||||
orderdate: parsedDate,
|
||||
ordernotes: json['ordernotes'],
|
||||
|
||||
deliverytime: json['deliverytime'],
|
||||
pending: json['pending'],
|
||||
processing: json['processing'],
|
||||
ready: json['ready'],
|
||||
delivered: json['delivered'],
|
||||
cancelled: json['cancelled'],
|
||||
|
||||
deliverycharge: (json['deliverycharge'] ?? 0).toDouble(),
|
||||
kms: json['kms'],
|
||||
|
||||
pickupaddress: json['pickupaddress'] ?? '',
|
||||
pickupcustomer: json['pickupcustomer'] ?? '',
|
||||
pickupcontactno: json['pickupcontactno'] ?? '',
|
||||
pickupcity: json['pickupcity'] ?? '',
|
||||
|
||||
deliveryaddress: json['deliveryaddress'] ?? '',
|
||||
deliverylat: json['deliverylat']?.toString(),
|
||||
deliverylong: json['deliverylong']?.toString(),
|
||||
deliverycustomer: json['deliverycustomer'] ?? '',
|
||||
deliverycontactno: json['deliverycontactno'] ?? '',
|
||||
|
||||
// ✅ Try flat field first, fallback to nested tenant object
|
||||
tenantname: json['tenantname'] ?? tenant['tenantname'] ?? 'Unknown Store',
|
||||
tenanttoken: json['tenanttoken'] ?? tenant['tenanttoken'] ?? '',
|
||||
tenantcontactno: json['tenantcontactno'] ?? tenant['tenantcontactno'] ?? '',
|
||||
tenantpostcode: json['tenantpostcode'] ?? tenant['tenantpostcode'] ?? '',
|
||||
tenantsuburb: json['tenantsuburb'] ?? tenant['tenantsuburb'] ?? '',
|
||||
tenantcity: json['tenantcity'] ?? tenant['tenantcity'] ?? '',
|
||||
tenantimage: json['tenantimage'] ?? tenant['tenantimage'],
|
||||
registrationno: json['registrationno'] ?? tenant['registrationno'] ?? '',
|
||||
gstno: json['gstno'] ?? tenant['gstno'] ?? '',
|
||||
|
||||
locationname: json['locationname'] ?? tenantlocation['locationname'] ?? '',
|
||||
locationcontactno: json['locationcontactno'] ?? tenantlocation['locationcontactno'] ?? '',
|
||||
locationpostcode: json['locationpostcode'] ?? tenantlocation['locationpostcode'] ?? '',
|
||||
locationsuburb: json['locationsuburb'] ?? tenantlocation['locationsuburb'] ?? '',
|
||||
locationcity: json['locationcity'] ?? tenantlocation['locationcity'] ?? '',
|
||||
|
||||
applocationname: json['applocationname'] ?? applocation['locationname'] ?? '',
|
||||
|
||||
totaltaxamount: (json['totaltaxamount'] ?? 0).toDouble(),
|
||||
|
||||
// ✅ Handles Map {}, List [], or null safely
|
||||
orderdetails: parseOrderDetails(json['orderdetails']),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class OrderDetail {
|
||||
final int orderdetailid;
|
||||
final String? productname;
|
||||
final int? orderqty;
|
||||
final double? price;
|
||||
final double? productsumprice; // ✅ used for total amount calculation in UI
|
||||
final double? discountamount; // ✅ used in OrderDetailsPage
|
||||
final String? productimage; // ✅ used in OrderDetailsPage
|
||||
|
||||
OrderDetail({
|
||||
required this.orderdetailid,
|
||||
this.productname,
|
||||
this.orderqty,
|
||||
this.price,
|
||||
this.productsumprice,
|
||||
this.discountamount,
|
||||
this.productimage,
|
||||
});
|
||||
|
||||
factory OrderDetail.fromJson(Map<String, dynamic> json) {
|
||||
return OrderDetail(
|
||||
orderdetailid: json['orderdetailid'] ?? 0,
|
||||
productname: json['productname'] ?? 'Unknown Product',
|
||||
orderqty: json['orderqty'] ?? 0,
|
||||
price: (json['price'] ?? 0).toDouble(),
|
||||
// ✅ fallback to price if productsumprice not in response
|
||||
productsumprice: (json['productsumprice'] ?? json['price'] ?? 0).toDouble(),
|
||||
discountamount: (json['discountamount'] ?? 0).toDouble(),
|
||||
productimage: json['productimage'] ?? json['image'],
|
||||
);
|
||||
}
|
||||
}
|
||||
25
lib/modules/product/discount.dart
Normal file
25
lib/modules/product/discount.dart
Normal file
@@ -0,0 +1,25 @@
|
||||
class Discount {
|
||||
final int discountid;
|
||||
final String discountname;
|
||||
final String discountcode;
|
||||
final String discountterms;
|
||||
final double discountvalue;
|
||||
|
||||
Discount({
|
||||
required this.discountid,
|
||||
required this.discountname,
|
||||
required this.discountcode,
|
||||
required this.discountterms,
|
||||
required this.discountvalue,
|
||||
});
|
||||
|
||||
factory Discount.fromJson(Map<String, dynamic> json) {
|
||||
return Discount(
|
||||
discountid: json['discountid'],
|
||||
discountname: json['discountname'],
|
||||
discountcode: json['discountcode'],
|
||||
discountterms: json['discountterms'],
|
||||
discountvalue: (json['discountvalue'] ?? 0).toDouble(),
|
||||
);
|
||||
}
|
||||
}
|
||||
336
lib/modules/product/product.dart
Normal file
336
lib/modules/product/product.dart
Normal file
@@ -0,0 +1,336 @@
|
||||
import 'dart:convert';
|
||||
|
||||
/// Parse JSON to ProductResponse
|
||||
ProductResponse productResponseFromJson(String str) =>
|
||||
ProductResponse.fromJson(json.decode(str));
|
||||
|
||||
String productResponseToJson(ProductResponse data) =>
|
||||
json.encode(data.toJson());
|
||||
|
||||
class ProductResponse {
|
||||
final int? code;
|
||||
final Data? data;
|
||||
final String? message;
|
||||
final bool? status;
|
||||
final List<Product>? details; // for variants API
|
||||
|
||||
ProductResponse({
|
||||
this.code,
|
||||
this.data,
|
||||
this.message,
|
||||
this.status,
|
||||
this.details,
|
||||
});
|
||||
|
||||
factory ProductResponse.fromJson(Map<String, dynamic> json) =>
|
||||
ProductResponse(
|
||||
code: json["code"],
|
||||
data: json["data"] == null ? null : Data.fromJson(json["data"]),
|
||||
details: json["details"] != null
|
||||
? List<Product>.from(
|
||||
json["details"]!.map((x) => Product.fromVariantJson(x)))
|
||||
: null,
|
||||
message: json["message"],
|
||||
status: json["status"],
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
"code": code,
|
||||
"data": data?.toJson(),
|
||||
"details": details == null
|
||||
? null
|
||||
: List<dynamic>.from(details!.map((x) => x.toJson())),
|
||||
"message": message,
|
||||
"status": status,
|
||||
};
|
||||
}
|
||||
|
||||
/// Data class for main product API
|
||||
class Data {
|
||||
final String? address;
|
||||
final String? city;
|
||||
final List<Detail>? details;
|
||||
final String? licenseno;
|
||||
final String? locationname;
|
||||
final String? pickuplat;
|
||||
final int? pickuplocationid;
|
||||
final String? pickuplong;
|
||||
final String? postcode;
|
||||
final String? primarycontact;
|
||||
final String? primaryemail;
|
||||
final String? suburb;
|
||||
final String? tenantname;
|
||||
|
||||
Data({
|
||||
this.address,
|
||||
this.city,
|
||||
this.details,
|
||||
this.licenseno,
|
||||
this.locationname,
|
||||
this.pickuplat,
|
||||
this.pickuplocationid,
|
||||
this.pickuplong,
|
||||
this.postcode,
|
||||
this.primarycontact,
|
||||
this.primaryemail,
|
||||
this.suburb,
|
||||
this.tenantname,
|
||||
});
|
||||
|
||||
factory Data.fromJson(Map<String, dynamic> json) => Data(
|
||||
address: json["address"],
|
||||
city: json["city"],
|
||||
details: json["details"] == null
|
||||
? []
|
||||
: List<Detail>.from(
|
||||
json["details"]!.map((x) => Detail.fromJson(x))),
|
||||
licenseno: json["licenseno"],
|
||||
locationname: json["locationname"],
|
||||
pickuplat: json["pickuplat"],
|
||||
pickuplocationid: json["pickuplocationid"],
|
||||
pickuplong: json["pickuplong"],
|
||||
postcode: json["postcode"],
|
||||
primarycontact: json["primarycontact"],
|
||||
primaryemail: json["primaryemail"],
|
||||
suburb: json["suburb"],
|
||||
tenantname: json["tenantname"],
|
||||
);
|
||||
|
||||
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
"address": address,
|
||||
"city": city,
|
||||
"details": details == null
|
||||
? []
|
||||
: List<dynamic>.from(details!.map((x) => x.toJson())),
|
||||
"licenseno": licenseno,
|
||||
"locationname": locationname,
|
||||
"pickuplat": pickuplat,
|
||||
"pickuplocationid": pickuplocationid,
|
||||
"pickuplong": pickuplong,
|
||||
"postcode": postcode,
|
||||
"primarycontact": primarycontact,
|
||||
"primaryemail": primaryemail,
|
||||
"suburb": suburb,
|
||||
"tenantname": tenantname,
|
||||
};
|
||||
}
|
||||
|
||||
/// Detail class for subcategories
|
||||
class Detail {
|
||||
final int? subcategoryid;
|
||||
final String? subcategoryname;
|
||||
final String? image;
|
||||
final List<Product>? products;
|
||||
|
||||
Detail({
|
||||
this.subcategoryid,
|
||||
this.subcategoryname,
|
||||
this.image,
|
||||
this.products,
|
||||
});
|
||||
|
||||
factory Detail.fromJson(Map<String, dynamic> json) => Detail(
|
||||
subcategoryid: json["subcategoryid"],
|
||||
subcategoryname: json["subcategoryname"],
|
||||
image: json["image"],
|
||||
products: json["products"] == null
|
||||
? []
|
||||
: List<Product>.from(
|
||||
json["products"]!.map((x) => Product.fromJson(x))),
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
"subcategoryid": subcategoryid,
|
||||
"subcategoryname": subcategoryname,
|
||||
"image": image,
|
||||
"products": products == null
|
||||
? []
|
||||
: List<dynamic>.from(products!.map((x) => x.toJson())),
|
||||
};
|
||||
}
|
||||
|
||||
/// Product class for both main products and variants
|
||||
class Product {
|
||||
final int? productid;
|
||||
final int? applocationid;
|
||||
final int? productlocationid;
|
||||
final int? tenantid;
|
||||
final int? categoryid;
|
||||
final String? categoryname;
|
||||
final int? subcategoryid;
|
||||
final String? subcategoryname;
|
||||
final String? productname;
|
||||
final String? productimage;
|
||||
final String? productdesc;
|
||||
final Productunit? productunit;
|
||||
final String? unitvalue;
|
||||
final double? productcost;
|
||||
final double? discount;
|
||||
final double? taxamount;
|
||||
final int? taxpercent;
|
||||
final int? producttax;
|
||||
final int? productstock;
|
||||
final int? productcombo;
|
||||
final int? variants;
|
||||
final int? quantity;
|
||||
final int? approve;
|
||||
final Productstatus? productstatus;
|
||||
|
||||
Product({
|
||||
this.productid,
|
||||
this.applocationid,
|
||||
this.productlocationid,
|
||||
this.tenantid,
|
||||
this.categoryid,
|
||||
this.categoryname,
|
||||
this.subcategoryid,
|
||||
this.subcategoryname,
|
||||
this.productname,
|
||||
this.productimage,
|
||||
this.productdesc,
|
||||
this.productunit,
|
||||
this.unitvalue,
|
||||
this.productcost,
|
||||
this.discount,
|
||||
this.taxamount,
|
||||
this.taxpercent,
|
||||
this.producttax,
|
||||
this.productstock,
|
||||
this.productcombo,
|
||||
this.variants,
|
||||
this.quantity,
|
||||
this.approve,
|
||||
this.productstatus,
|
||||
});
|
||||
|
||||
/// Factory for main product API
|
||||
factory Product.fromJson(Map<String, dynamic> json) => Product(
|
||||
productid: json["productid"],
|
||||
applocationid: json["applocationid"],
|
||||
productlocationid: json["productlocationid"],
|
||||
tenantid: json["tenantid"],
|
||||
categoryid: json["categoryid"],
|
||||
categoryname: json["categoryname"],
|
||||
subcategoryid: json["subcategoryid"],
|
||||
subcategoryname: json["Subcategoryname"],
|
||||
productname: json["productname"],
|
||||
productimage: json["productimage"],
|
||||
productdesc: json["productdesc"],
|
||||
productunit: productunitValues.map[json["productunit"]] ?? Productunit.KG,
|
||||
unitvalue: json["unitvalue"],
|
||||
productcost: (json["productcost"] ?? 0).toDouble(),
|
||||
discount: (json["discountvalue"] ?? 0).toDouble(),
|
||||
taxamount: (json["taxamount"] ?? 0).toDouble(),
|
||||
taxpercent: json["taxpercent"],
|
||||
producttax: json["producttax"],
|
||||
productstock: json["productstock"],
|
||||
productcombo: json["productcombo"],
|
||||
variants: json["variants"],
|
||||
quantity: json["quantity"],
|
||||
approve: json["approve"],
|
||||
productstatus: productstatusValues.map[json["productstatus"]] ?? Productstatus.AVAILABLE,
|
||||
);
|
||||
|
||||
/// Factory for variants API (flat structure)
|
||||
factory Product.fromVariantJson(Map<String, dynamic> json) => Product(
|
||||
productid: json["productid"],
|
||||
applocationid: json["applocationid"],
|
||||
productlocationid: json["productlocationid"],
|
||||
tenantid: json["tenantid"],
|
||||
categoryid: json["categoryid"],
|
||||
categoryname: json["categoryname"],
|
||||
subcategoryid: json["subcategoryid"] ?? 0,
|
||||
subcategoryname: json["Subcategoryname"] ?? "",
|
||||
productname: json["productname"],
|
||||
productimage: json["productimage"],
|
||||
productdesc: json["productdesc"].toString(),
|
||||
productunit: productunitValues.map[json["productunit"]] ?? Productunit.KG,
|
||||
unitvalue: json["unitvalue"] ?? "",
|
||||
productcost: (json["productcost"] ?? 0).toDouble(),
|
||||
discount: (json["discountvalue"] ?? 0).toDouble(),
|
||||
taxamount: (json["producttax"] ?? 0).toDouble(),
|
||||
taxpercent: json["taxpercent"] ?? 0,
|
||||
producttax: json["producttax"] ?? 0,
|
||||
productstock: json["productstock"] ?? 0,
|
||||
productcombo: json["productcombo"] ?? 0,
|
||||
variants: json["variants"] ?? 0,
|
||||
quantity: json["quantity"] ?? 0,
|
||||
approve: json["approve"] ?? 0,
|
||||
productstatus: productstatusValues.map[json["productstatus"]] ?? Productstatus.AVAILABLE,
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
"productid": productid,
|
||||
"applocationid": applocationid,
|
||||
"productlocationid": productlocationid,
|
||||
"tenantid": tenantid,
|
||||
"categoryid": categoryid,
|
||||
"categoryname": categoryname,
|
||||
"subcategoryid": subcategoryid,
|
||||
"Subcategoryname": subcategoryname,
|
||||
"productname": productname,
|
||||
"productimage": productimage,
|
||||
"productdesc": productdesc,
|
||||
"productunit": productunitValues.reverse[productunit],
|
||||
"unitvalue": unitvalue,
|
||||
"productcost": productcost,
|
||||
"discount": discount,
|
||||
"taxamount": taxamount,
|
||||
"taxpercent": taxpercent,
|
||||
"producttax": producttax,
|
||||
"productstock": productstock,
|
||||
"productcombo": productcombo,
|
||||
"variants": variants,
|
||||
"quantity": quantity,
|
||||
"approve": approve,
|
||||
"productstatus": productstatusValues.reverse[productstatus],
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/// Convert a JSON list into List<Product>
|
||||
static List<Product> fromJsonList(List<dynamic> list) {
|
||||
return list.map((e) => Product.fromJson(e)).toList();
|
||||
}
|
||||
|
||||
/// Convert a JSON list for variants into List<Product>
|
||||
static List<Product> fromVariantJsonList(List<dynamic> list) {
|
||||
return list.map((e) => Product.fromVariantJson(e)).toList();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
enum Productstatus { ACTIVE, AVAILABLE, OUTOFSTOCK }
|
||||
final productstatusValues = EnumValues({
|
||||
"Active": Productstatus.ACTIVE,
|
||||
"available": Productstatus.AVAILABLE,
|
||||
"outofstock": Productstatus.OUTOFSTOCK
|
||||
});
|
||||
|
||||
enum Productunit { BOX, KG, LTR, PCS }
|
||||
|
||||
final productunitValues = EnumValues({
|
||||
"box": Productunit.BOX,
|
||||
"kg": Productunit.KG,
|
||||
"ltr": Productunit.LTR,
|
||||
"pcs": Productunit.PCS,
|
||||
});
|
||||
|
||||
/// Enum helper class
|
||||
class EnumValues<T> {
|
||||
Map<String, T> map;
|
||||
late Map<T, String> reverseMap;
|
||||
|
||||
EnumValues(this.map);
|
||||
|
||||
Map<T, String> get reverse {
|
||||
reverseMap = map.map((k, v) => MapEntry(v, k));
|
||||
return reverseMap;
|
||||
}
|
||||
}
|
||||
84
lib/modules/profile/customer_request.dart
Normal file
84
lib/modules/profile/customer_request.dart
Normal file
@@ -0,0 +1,84 @@
|
||||
class CustomerRequestModel {
|
||||
final String referencedate;
|
||||
final String referencetype;
|
||||
final int customerid;
|
||||
final int tenantid;
|
||||
final int locationid;
|
||||
final String subject;
|
||||
final String remarks;
|
||||
final int status;
|
||||
final int apptypeid;
|
||||
|
||||
CustomerRequestModel({
|
||||
required this.referencedate,
|
||||
required this.referencetype,
|
||||
required this.customerid,
|
||||
required this.tenantid,
|
||||
required this.locationid,
|
||||
required this.subject,
|
||||
required this.remarks,
|
||||
required this.status,
|
||||
required this.apptypeid,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
"referencedate": referencedate,
|
||||
"referencetype": referencetype,
|
||||
"customerid": customerid,
|
||||
"tenantid": tenantid,
|
||||
"locationid": locationid,
|
||||
"subject": subject,
|
||||
"remarks": remarks,
|
||||
"status": status,
|
||||
"apptypeid": apptypeid,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class CustomerRequestStatusModel {
|
||||
final int customerrequestid;
|
||||
final String referencedate;
|
||||
final String referencetype;
|
||||
final int customerid;
|
||||
final int tenantid;
|
||||
final int apptypeid;
|
||||
final int locationid;
|
||||
final String subject;
|
||||
final String remarks;
|
||||
final int status;
|
||||
final String created;
|
||||
final String updated;
|
||||
|
||||
CustomerRequestStatusModel({
|
||||
required this.customerrequestid,
|
||||
required this.referencedate,
|
||||
required this.referencetype,
|
||||
required this.customerid,
|
||||
required this.tenantid,
|
||||
required this.apptypeid,
|
||||
required this.locationid,
|
||||
required this.subject,
|
||||
required this.remarks,
|
||||
required this.status,
|
||||
required this.created,
|
||||
required this.updated,
|
||||
});
|
||||
|
||||
factory CustomerRequestStatusModel.fromJson(Map<String, dynamic> json) {
|
||||
return CustomerRequestStatusModel(
|
||||
customerrequestid: json['customerrequestid'] ?? 0,
|
||||
referencedate: json['referencedate'] ?? "",
|
||||
referencetype: json['referencetype'] ?? "",
|
||||
customerid: json['customerid'] ?? 0,
|
||||
tenantid: json['tenantid'] ?? 0,
|
||||
apptypeid: json['apptypeid'] ?? 0,
|
||||
locationid: json['locationid'] ?? 0,
|
||||
subject: json['subject'] ?? "",
|
||||
remarks: json['remarks'] ?? "",
|
||||
status: json['status'] ?? 0,
|
||||
created: json['created'] ?? "",
|
||||
updated: json['updated'] ?? "",
|
||||
);
|
||||
}
|
||||
}
|
||||
19
lib/modules/tenant/category.dart
Normal file
19
lib/modules/tenant/category.dart
Normal file
@@ -0,0 +1,19 @@
|
||||
class Category {
|
||||
final int id;
|
||||
final String name;
|
||||
final String icon;
|
||||
|
||||
Category({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.icon,
|
||||
});
|
||||
|
||||
factory Category.fromJson(Map<String, dynamic> json) {
|
||||
return Category(
|
||||
id: json['categoryid'],
|
||||
name: json['categoryname'] ?? '',
|
||||
icon: json['iconurl'] ?? '',
|
||||
);
|
||||
}
|
||||
}
|
||||
680
lib/modules/tenant/get_tenant.dart
Normal file
680
lib/modules/tenant/get_tenant.dart
Normal file
@@ -0,0 +1,680 @@
|
||||
import 'dart:convert';
|
||||
|
||||
CustomerTenantsResponse customerTenantsResponseFromJson(String str) =>
|
||||
CustomerTenantsResponse.fromJson(json.decode(str));
|
||||
|
||||
String customerTenantsResponseToJson(CustomerTenantsResponse data) =>
|
||||
json.encode(data.toJson());
|
||||
|
||||
class CustomerTenantsResponse {
|
||||
final int? code;
|
||||
final List<Tenant>? details;
|
||||
final String? message;
|
||||
final bool? status;
|
||||
|
||||
CustomerTenantsResponse({
|
||||
this.code,
|
||||
this.details,
|
||||
this.message,
|
||||
this.status,
|
||||
});
|
||||
|
||||
factory CustomerTenantsResponse.fromJson(Map<String, dynamic> json) =>
|
||||
CustomerTenantsResponse(
|
||||
code: json["code"],
|
||||
details: json["details"] == null
|
||||
? []
|
||||
: List<Tenant>.from(json["details"].map((x) => Tenant.fromJson(x))),
|
||||
message: json["message"],
|
||||
status: json["status"],
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
"code": code,
|
||||
"details": details == null
|
||||
? []
|
||||
: List<dynamic>.from(details!.map((x) => x.toJson())),
|
||||
"message": message,
|
||||
"status": status,
|
||||
};
|
||||
}
|
||||
|
||||
class Data {
|
||||
final Customer? customer;
|
||||
final List<Tenant>? tenants;
|
||||
|
||||
Data({
|
||||
this.customer,
|
||||
this.tenants,
|
||||
});
|
||||
|
||||
factory Data.fromJson(Map<String, dynamic> json) => Data(
|
||||
customer: json["customer"] == null
|
||||
? null
|
||||
: Customer.fromJson(json["customer"]),
|
||||
tenants: json["tenants"] == null
|
||||
? []
|
||||
: List<Tenant>.from(
|
||||
json["tenants"]!.map((x) => Tenant.fromJson(x))),
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
"customer": customer?.toJson(),
|
||||
"tenants": tenants == null
|
||||
? []
|
||||
: List<dynamic>.from(tenants!.map((x) => x.toJson())),
|
||||
};
|
||||
}
|
||||
|
||||
class Customer {
|
||||
final int? customerid;
|
||||
final String? firstname;
|
||||
final String? lastname;
|
||||
final String? profileimage;
|
||||
final String? gender;
|
||||
final String? dob;
|
||||
final String? dialcode;
|
||||
final String? contactno;
|
||||
final String? email;
|
||||
final String? deviceid;
|
||||
final String? devicetype;
|
||||
final int? authmode;
|
||||
final int? configid;
|
||||
final String? customertoken;
|
||||
final String? address;
|
||||
final String? suburb;
|
||||
final String? city;
|
||||
final String? state;
|
||||
final String? landmark;
|
||||
final String? doorno;
|
||||
final String? postcode;
|
||||
final String? latitude;
|
||||
final String? longitude;
|
||||
final int? applocationid;
|
||||
final int? status;
|
||||
final String? intro;
|
||||
|
||||
Customer({
|
||||
this.customerid,
|
||||
this.firstname,
|
||||
this.lastname,
|
||||
this.profileimage,
|
||||
this.gender,
|
||||
this.dob,
|
||||
this.dialcode,
|
||||
this.contactno,
|
||||
this.email,
|
||||
this.deviceid,
|
||||
this.devicetype,
|
||||
this.authmode,
|
||||
this.configid,
|
||||
this.customertoken,
|
||||
this.address,
|
||||
this.suburb,
|
||||
this.city,
|
||||
this.state,
|
||||
this.landmark,
|
||||
this.doorno,
|
||||
this.postcode,
|
||||
this.latitude,
|
||||
this.longitude,
|
||||
this.applocationid,
|
||||
this.status,
|
||||
this.intro,
|
||||
});
|
||||
|
||||
factory Customer.fromJson(Map<String, dynamic> json) => Customer(
|
||||
customerid: json["customerid"],
|
||||
firstname: json["firstname"],
|
||||
lastname: json["lastname"],
|
||||
profileimage: json["profileimage"],
|
||||
gender: json["gender"],
|
||||
dob: json["dob"],
|
||||
dialcode: json["dialcode"],
|
||||
contactno: json["contactno"],
|
||||
email: json["email"],
|
||||
deviceid: json["deviceid"],
|
||||
devicetype: json["devicetype"],
|
||||
authmode: json["authmode"],
|
||||
configid: json["configid"],
|
||||
customertoken: json["customertoken"],
|
||||
address: json["address"],
|
||||
suburb: json["suburb"],
|
||||
city: json["city"],
|
||||
state: json["state"],
|
||||
landmark: json["landmark"],
|
||||
doorno: json["doorno"],
|
||||
postcode: json["postcode"],
|
||||
latitude: json["latitude"],
|
||||
longitude: json["longitude"],
|
||||
applocationid: json["applocationid"],
|
||||
status: json["status"],
|
||||
intro: json["intro"],
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
"customerid": customerid,
|
||||
"firstname": firstname,
|
||||
"lastname": lastname,
|
||||
"profileimage": profileimage,
|
||||
"gender": gender,
|
||||
"dob": dob,
|
||||
"dialcode": dialcode,
|
||||
"contactno": contactno,
|
||||
"email": email,
|
||||
"deviceid": deviceid,
|
||||
"devicetype": devicetype,
|
||||
"authmode": authmode,
|
||||
"configid": configid,
|
||||
"customertoken": customertoken,
|
||||
"address": address,
|
||||
"suburb": suburb,
|
||||
"city": city,
|
||||
"state": state,
|
||||
"landmark": landmark,
|
||||
"doorno": doorno,
|
||||
"postcode": postcode,
|
||||
"latitude": latitude,
|
||||
"longitude": longitude,
|
||||
"applocationid": applocationid,
|
||||
"status": status,
|
||||
"intro": intro,
|
||||
};
|
||||
}
|
||||
|
||||
class Tenant {
|
||||
final int? tenantid;
|
||||
final String? tenantname;
|
||||
final String? tenanttoken;
|
||||
final String? tenantbanner;
|
||||
final double? tenantcharge;
|
||||
final String? address;
|
||||
final String? licenseno;
|
||||
final String? primaryemail;
|
||||
final String? primarycontact;
|
||||
final int? pickuplocationid;
|
||||
final int? applocationid;
|
||||
final String? suburb;
|
||||
final String? city;
|
||||
final String? latitude;
|
||||
final String? longitude;
|
||||
final String? postcode;
|
||||
final String? tenantimage;
|
||||
final int? locationid;
|
||||
final String? locationname;
|
||||
final int? subcategoryid;
|
||||
final int? categoryid;
|
||||
final String? registrationno;
|
||||
final int? orderscount;
|
||||
final List<Subcategory>? subcategories;
|
||||
|
||||
Tenant({
|
||||
this.tenantid,
|
||||
this.tenantname,
|
||||
this.tenanttoken,
|
||||
this.tenantbanner,
|
||||
this.tenantcharge,
|
||||
this.address,
|
||||
this.licenseno,
|
||||
this.primaryemail,
|
||||
this.primarycontact,
|
||||
this.pickuplocationid,
|
||||
this.applocationid,
|
||||
this.suburb,
|
||||
this.city,
|
||||
this.latitude,
|
||||
this.longitude,
|
||||
this.postcode,
|
||||
this.tenantimage,
|
||||
this.locationid,
|
||||
this.locationname,
|
||||
this.subcategoryid,
|
||||
this.categoryid,
|
||||
this.registrationno,
|
||||
this.orderscount,
|
||||
this.subcategories,
|
||||
});
|
||||
|
||||
factory Tenant.fromJson(Map<String, dynamic> json) => Tenant(
|
||||
tenantid: json["tenantid"],
|
||||
tenantname: json["tenantname"],
|
||||
tenanttoken: json["userfcmtoken"],
|
||||
tenantbanner: json["tenantbanner"],
|
||||
tenantcharge: json["tenantcharge"],
|
||||
address: json["address"],
|
||||
licenseno: json["licenseno"],
|
||||
primaryemail: json["primaryemail"],
|
||||
primarycontact: json["primarycontact"],
|
||||
pickuplocationid: json["pickuplocationid"],
|
||||
applocationid: json["applocationid"],
|
||||
suburb: json["suburb"],
|
||||
city: json["city"],
|
||||
latitude: json["latitude"],
|
||||
longitude: json["longitude"],
|
||||
postcode: json["postcode"],
|
||||
tenantimage: json["tenantimage"],
|
||||
locationid: json["locationid"],
|
||||
locationname: json["locationname"],
|
||||
subcategoryid: json["subcategoryid"],
|
||||
categoryid: json["categoryid"],
|
||||
registrationno: json["registrationno"],
|
||||
orderscount: json["orderscount"],
|
||||
subcategories: json["productsubcategory"] == null
|
||||
? []
|
||||
: List<Subcategory>.from(
|
||||
json["productsubcategory"].map((x) => Subcategory.fromJson(x))),
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
"tenantid": tenantid,
|
||||
"tenantname": tenantname,
|
||||
"userfcmtoken": tenanttoken,
|
||||
"tenantbanner": tenantbanner,
|
||||
"tenantchanrge": tenantcharge,
|
||||
"address": address,
|
||||
"licenseno": licenseno,
|
||||
"primaryemail": primaryemail,
|
||||
"primarycontact": primarycontact,
|
||||
"pickuplocationid": pickuplocationid,
|
||||
"applocationid": applocationid,
|
||||
"suburb": suburb,
|
||||
"city": city,
|
||||
"latitude": latitude,
|
||||
"longitude": longitude,
|
||||
"postcode": postcode,
|
||||
"tenantimage": tenantimage,
|
||||
"locationid": locationid,
|
||||
"locationname": locationname,
|
||||
"subcategoryid": subcategoryid,
|
||||
"categoryid": categoryid,
|
||||
"registrationno": registrationno,
|
||||
"orderscount": orderscount,
|
||||
"productsubcategory": subcategories == null
|
||||
? []
|
||||
: List<dynamic>.from(subcategories!.map((x) => x.toJson())),
|
||||
};
|
||||
}
|
||||
|
||||
class Subcategory {
|
||||
final int? subcatid;
|
||||
final int? categoryid;
|
||||
final int? tenantid;
|
||||
final String? subcatname;
|
||||
final String? status;
|
||||
final String? image;
|
||||
|
||||
Subcategory({
|
||||
this.subcatid,
|
||||
this.categoryid,
|
||||
this.tenantid,
|
||||
this.subcatname,
|
||||
this.status,
|
||||
this.image,
|
||||
});
|
||||
|
||||
factory Subcategory.fromJson(Map<String, dynamic> json) => Subcategory(
|
||||
subcatid: json["subcatid"],
|
||||
categoryid: json["categoryid"],
|
||||
tenantid: json["tenantid"],
|
||||
subcatname: json["subcatname"],
|
||||
status: json["status"],
|
||||
image: json["image"],
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
"subcatid": subcatid,
|
||||
"categoryid": categoryid,
|
||||
"tenantid": tenantid,
|
||||
"subcatname": subcatname,
|
||||
"status": status,
|
||||
"image": image,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
OrdersResponse ordersResponseFromJson(String str) =>
|
||||
OrdersResponse.fromJson(json.decode(str));
|
||||
|
||||
String ordersResponseToJson(OrdersResponse data) =>
|
||||
json.encode(data.toJson());
|
||||
|
||||
class OrdersResponse {
|
||||
final int? code;
|
||||
final List<OrderDatum>? data;
|
||||
final String? message;
|
||||
final bool? status;
|
||||
|
||||
OrdersResponse({
|
||||
this.code,
|
||||
this.data,
|
||||
this.message,
|
||||
this.status,
|
||||
});
|
||||
|
||||
factory OrdersResponse.fromJson(Map<String, dynamic> json) =>
|
||||
OrdersResponse(
|
||||
code: json["code"],
|
||||
data: json["data"] == null
|
||||
? []
|
||||
: List<OrderDatum>.from(
|
||||
json["data"].map((x) => OrderDatum.fromJson(x))),
|
||||
message: json["message"],
|
||||
status: json["status"],
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
"code": code,
|
||||
"data": data == null
|
||||
? []
|
||||
: List<dynamic>.from(data!.map((x) => x.toJson())),
|
||||
"message": message,
|
||||
"status": status,
|
||||
};
|
||||
}
|
||||
|
||||
class OrderDatum {
|
||||
final int? orderheaderid;
|
||||
final String? gstno;
|
||||
final String? orderid;
|
||||
final String? orderstatus;
|
||||
final DateTime? orderdate;
|
||||
final int? itemcount;
|
||||
final double? deliverycharge;
|
||||
final double? orderamount;
|
||||
final double? taxamount;
|
||||
final double? totaltaxamount;
|
||||
final String? tenantname;
|
||||
final String? tenantsuburb;
|
||||
final String? tenantcity;
|
||||
final String? deliveryaddress;
|
||||
final String? deliverystatus;
|
||||
final String? pickupaddress;
|
||||
final String? pickupcustomer;
|
||||
final String? pickupcontactno;
|
||||
final String? deliverycustomer;
|
||||
final String? deliverycontactno;
|
||||
final String? locationname;
|
||||
final String? locationcity;
|
||||
final String? tenantimage;
|
||||
final List<OrderDetail>? orderdetails;
|
||||
|
||||
OrderDatum({
|
||||
this.orderheaderid,
|
||||
this.orderid,
|
||||
this.gstno,
|
||||
this.orderstatus,
|
||||
this.orderdate,
|
||||
this.itemcount,
|
||||
this.deliverycharge,
|
||||
this.orderamount,
|
||||
this.taxamount,
|
||||
this.totaltaxamount,
|
||||
this.tenantname,
|
||||
this.tenantsuburb,
|
||||
this.tenantcity,
|
||||
this.deliveryaddress,
|
||||
this.deliverystatus,
|
||||
this.pickupaddress,
|
||||
this.pickupcustomer,
|
||||
this.pickupcontactno,
|
||||
this.deliverycustomer,
|
||||
this.deliverycontactno,
|
||||
this.locationname,
|
||||
this.locationcity,
|
||||
this.orderdetails,
|
||||
this.tenantimage,
|
||||
});
|
||||
|
||||
factory OrderDatum.fromJson(Map<String, dynamic> json) => OrderDatum(
|
||||
orderheaderid: json["orderheaderid"],
|
||||
orderid: json["orderid"],
|
||||
gstno: json["registrationno"],
|
||||
orderstatus: json["orderstatus"],
|
||||
orderdate:
|
||||
json["orderdate"] == null ? null : DateTime.parse(json["orderdate"]),
|
||||
itemcount: json["itemcount"],
|
||||
deliverycharge: json["deliverycharge"]?.toDouble(),
|
||||
orderamount: json["orderamount"]?.toDouble(),
|
||||
taxamount: json["taxamount"]?.toDouble(),
|
||||
totaltaxamount: json["totaltaxamount"]?.toDouble(),
|
||||
tenantname: json["tenantname"],
|
||||
tenantsuburb: json["tenantsuburb"],
|
||||
tenantcity: json["tenantcity"],
|
||||
deliveryaddress: json["deliveryaddress"],
|
||||
deliverystatus: json["deliverystatus"],
|
||||
pickupaddress: json["pickupaddress"],
|
||||
pickupcustomer: json["pickupcustomer"],
|
||||
pickupcontactno: json["pickupcontactno"],
|
||||
deliverycustomer: json["deliverycustomer"],
|
||||
deliverycontactno: json["deliverycontactno"],
|
||||
locationname: json["locationname"],
|
||||
tenantimage: json["tenantimage"],
|
||||
locationcity: json["locationcity"],
|
||||
orderdetails: json["orderdetails"] == null
|
||||
? []
|
||||
: List<OrderDetail>.from(
|
||||
json["orderdetails"].map((x) => OrderDetail.fromJson(x))),
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
"orderheaderid": orderheaderid,
|
||||
"orderid": orderid,
|
||||
"gstno": gstno,
|
||||
"orderstatus": orderstatus,
|
||||
"orderdate": orderdate?.toIso8601String(),
|
||||
"itemcount": itemcount,
|
||||
"deliverycharge": deliverycharge,
|
||||
"orderamount": orderamount,
|
||||
"taxamount": taxamount,
|
||||
"totaltaxamount": totaltaxamount,
|
||||
"tenantname": tenantname,
|
||||
"tenantsuburb": tenantsuburb,
|
||||
"tenantcity": tenantcity,
|
||||
"deliveryaddress": deliveryaddress,
|
||||
"deliverystatus": deliverystatus,
|
||||
"pickupaddress": pickupaddress,
|
||||
"pickupcustomer": pickupcustomer,
|
||||
"pickupcontactno": pickupcontactno,
|
||||
"deliverycustomer": deliverycustomer,
|
||||
"deliverycontactno": deliverycontactno,
|
||||
"locationname": locationname,
|
||||
"locationcity": locationcity,
|
||||
"orderdetails": orderdetails == null
|
||||
? []
|
||||
: List<dynamic>.from(orderdetails!.map((x) => x.toJson())),
|
||||
};
|
||||
}
|
||||
|
||||
class OrderDetail {
|
||||
final int? orderdetailid;
|
||||
final int? orderheaderid;
|
||||
final int? productid;
|
||||
final String? productname;
|
||||
final String? productdescription;
|
||||
final int? orderqty;
|
||||
final double? price;
|
||||
final String? unitname;
|
||||
final double? productsumprice;
|
||||
final String? productimage;
|
||||
|
||||
OrderDetail({
|
||||
this.orderdetailid,
|
||||
this.orderheaderid,
|
||||
this.productid,
|
||||
this.productname,
|
||||
this.productdescription,
|
||||
this.orderqty,
|
||||
this.price,
|
||||
this.unitname,
|
||||
this.productsumprice,
|
||||
this.productimage,
|
||||
});
|
||||
|
||||
factory OrderDetail.fromJson(Map<String, dynamic> json) => OrderDetail(
|
||||
orderdetailid: json["orderdetailid"],
|
||||
orderheaderid: json["orderheaderid"],
|
||||
productid: json["productid"],
|
||||
productname: json["productname"],
|
||||
productdescription: json["productdescription"],
|
||||
orderqty: json["orderqty"],
|
||||
price: json["price"]?.toDouble(),
|
||||
unitname: json["unitname"],
|
||||
productsumprice: json["productsumprice"]?.toDouble(),
|
||||
productimage: json["productimage"],
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
"orderdetailid": orderdetailid,
|
||||
"orderheaderid": orderheaderid,
|
||||
"productid": productid,
|
||||
"productname": productname,
|
||||
"productdescription": productdescription,
|
||||
"orderqty": orderqty,
|
||||
"price": price,
|
||||
"unitname": unitname,
|
||||
"productsumprice": productsumprice,
|
||||
"productimage": productimage,
|
||||
};
|
||||
}
|
||||
class TenantLocation {
|
||||
final int locationId;
|
||||
final int tenantId;
|
||||
final int applocationId;
|
||||
final int moduleId;
|
||||
final int roleId;
|
||||
final String locationName;
|
||||
final String email;
|
||||
final String contactNo;
|
||||
final String latitude;
|
||||
final String longitude;
|
||||
final String address;
|
||||
final String suburb;
|
||||
final String city;
|
||||
final String state;
|
||||
final String postcode;
|
||||
final String openTime;
|
||||
final String closeTime;
|
||||
final int partnerId;
|
||||
final int deliveryRadius;
|
||||
final int deliveryMins;
|
||||
final int cancelSecs;
|
||||
final String status;
|
||||
|
||||
TenantLocation({
|
||||
required this.locationId,
|
||||
required this.tenantId,
|
||||
required this.applocationId,
|
||||
required this.moduleId,
|
||||
required this.roleId,
|
||||
required this.locationName,
|
||||
required this.email,
|
||||
required this.contactNo,
|
||||
required this.latitude,
|
||||
required this.longitude,
|
||||
required this.address,
|
||||
required this.suburb,
|
||||
required this.city,
|
||||
required this.state,
|
||||
required this.postcode,
|
||||
required this.openTime,
|
||||
required this.closeTime,
|
||||
required this.partnerId,
|
||||
required this.deliveryRadius,
|
||||
required this.deliveryMins,
|
||||
required this.cancelSecs,
|
||||
required this.status,
|
||||
});
|
||||
|
||||
factory TenantLocation.fromJson(Map<String, dynamic> json) {
|
||||
return TenantLocation(
|
||||
locationId: json['locationid'] ?? 0,
|
||||
tenantId: json['tenantid'] ?? 0,
|
||||
applocationId: json['applocationid'] ?? 0,
|
||||
moduleId: json['moduleid'] ?? 0,
|
||||
roleId: json['roleid'] ?? 0,
|
||||
locationName: json['locationname'] ?? '',
|
||||
email: json['email'] ?? '',
|
||||
contactNo: json['contactno'] ?? '',
|
||||
latitude: json['latitude'] ?? '',
|
||||
longitude: json['longitude'] ?? '',
|
||||
address: json['address'] ?? '',
|
||||
suburb: json['suburb'] ?? '',
|
||||
city: json['city'] ?? '',
|
||||
state: json['state'] ?? '',
|
||||
postcode: json['postcode'] ?? '',
|
||||
openTime: json['opentime'] ?? '',
|
||||
closeTime: json['closetime'] ?? '',
|
||||
partnerId: json['partnerid'] ?? 0,
|
||||
deliveryRadius: json['deliveryradius'] ?? 0,
|
||||
deliveryMins: json['deliverymins'] ?? 0,
|
||||
cancelSecs: json['cancelsecs'] ?? 0,
|
||||
status: json['status'] ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'locationid': locationId,
|
||||
'tenantid': tenantId,
|
||||
'applocationid': applocationId,
|
||||
'moduleid': moduleId,
|
||||
'roleid': roleId,
|
||||
'locationname': locationName,
|
||||
'email': email,
|
||||
'contactno': contactNo,
|
||||
'latitude': latitude,
|
||||
'longitude': longitude,
|
||||
'address': address,
|
||||
'suburb': suburb,
|
||||
'city': city,
|
||||
'state': state,
|
||||
'postcode': postcode,
|
||||
'opentime': openTime,
|
||||
'closetime': closeTime,
|
||||
'partnerid': partnerId,
|
||||
'deliveryradius': deliveryRadius,
|
||||
'deliverymins': deliveryMins,
|
||||
'cancelsecs': cancelSecs,
|
||||
'status': status,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class TenantLocationsResponse {
|
||||
final int code;
|
||||
final List<TenantLocation> details;
|
||||
final String message;
|
||||
final bool status;
|
||||
|
||||
TenantLocationsResponse({
|
||||
required this.code,
|
||||
required this.details,
|
||||
required this.message,
|
||||
required this.status,
|
||||
});
|
||||
|
||||
factory TenantLocationsResponse.fromJson(Map<String, dynamic> json) {
|
||||
final listJson = json['details'] as List<dynamic>?; // safer cast
|
||||
final detailsList = listJson != null
|
||||
? listJson.map((e) => TenantLocation.fromJson(e as Map<String, dynamic>)).toList()
|
||||
: <TenantLocation>[];
|
||||
|
||||
return TenantLocationsResponse(
|
||||
code: json['code'] ?? 0,
|
||||
details: detailsList,
|
||||
message: json['message'] ?? '',
|
||||
status: json['status'] ?? true, // default true if API doesn't send
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'code': code,
|
||||
'details': details.map((e) => e.toJson()).toList(),
|
||||
'message': message,
|
||||
'status': status,
|
||||
};
|
||||
}
|
||||
}
|
||||
15
lib/service/bindings.dart
Normal file
15
lib/service/bindings.dart
Normal file
@@ -0,0 +1,15 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:get/get_core/src/get_main.dart';
|
||||
|
||||
import '../controllers/authentication/auth_controller.dart';
|
||||
import '../controllers/intro_controller/intro_screen_controller.dart';
|
||||
import '../modules/authentication/auth.dart';
|
||||
|
||||
class GlobalBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
|
||||
Get.lazyPut(()=>IntroScreenController(),fenix:true);}
|
||||
|
||||
|
||||
}
|
||||
27
lib/service/connectivity/connectivity_controller.dart
Normal file
27
lib/service/connectivity/connectivity_controller.dart
Normal file
@@ -0,0 +1,27 @@
|
||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class ConnectivityController extends GetxController {
|
||||
var isConnected = true.obs;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
_checkInitialConnection();
|
||||
|
||||
// Listen to connection changes
|
||||
Connectivity().onConnectivityChanged.listen((status) {
|
||||
isConnected.value = (status != ConnectivityResult.none);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _checkInitialConnection() async {
|
||||
final status = await Connectivity().checkConnectivity();
|
||||
isConnected.value = (status != ConnectivityResult.none);
|
||||
}
|
||||
|
||||
Future<void> retryConnection() async {
|
||||
final status = await Connectivity().checkConnectivity();
|
||||
isConnected.value = (status != ConnectivityResult.none);
|
||||
}
|
||||
}
|
||||
47
lib/service/device_info/device_info.dart
Normal file
47
lib/service/device_info/device_info.dart
Normal file
@@ -0,0 +1,47 @@
|
||||
import 'dart:io';
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
class DeviceInfo {
|
||||
Future<Map<String, dynamic>> getDeviceInfo() async {
|
||||
DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
|
||||
Map<String, dynamic> info;
|
||||
String? currentDeviceId;
|
||||
|
||||
if (Platform.isAndroid) {
|
||||
AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo;
|
||||
currentDeviceId = androidInfo.id; // ✅ Device ID for Android
|
||||
info = {
|
||||
"device": "Android",
|
||||
"modules": androidInfo.model,
|
||||
"manufacturer": androidInfo.manufacturer,
|
||||
"androidVersion": androidInfo.version.release,
|
||||
"deviceId": currentDeviceId,
|
||||
};
|
||||
} else if (Platform.isIOS) {
|
||||
IosDeviceInfo iosInfo = await deviceInfo.iosInfo;
|
||||
currentDeviceId = iosInfo.identifierForVendor; // ✅ Device ID for iOS
|
||||
info = {
|
||||
"device": "iOS",
|
||||
"name": iosInfo.name,
|
||||
"systemName": iosInfo.systemName,
|
||||
"systemVersion": iosInfo.systemVersion,
|
||||
"modules": iosInfo.model,
|
||||
"identifierForVendor": currentDeviceId,
|
||||
};
|
||||
} else {
|
||||
info = {"device": "Unknown"};
|
||||
}
|
||||
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
// Save full info as string (optional)
|
||||
prefs.setString('deviceInfo', info.toString());
|
||||
|
||||
// ✅ Save only current device ID
|
||||
if (currentDeviceId != null) {
|
||||
prefs.setString('currentDeviceId', currentDeviceId);
|
||||
}
|
||||
|
||||
return info;
|
||||
}
|
||||
}
|
||||
86
lib/service/dio.dart
Normal file
86
lib/service/dio.dart
Normal file
@@ -0,0 +1,86 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
|
||||
import '../constants/api_constants.dart';
|
||||
|
||||
class CustomDio {
|
||||
final Dio _dio = Dio(
|
||||
BaseOptions(
|
||||
baseUrl: ApiConstants.baseUrl, // Change this to your API base URL
|
||||
connectTimeout: Duration(seconds: 20), // Timeout settings
|
||||
receiveTimeout: Duration(seconds: 40),
|
||||
headers: {'Content-Type': 'application/json'}, // Default headers
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
|
||||
|
||||
Future<dynamic> getData(
|
||||
String endpoint, {
|
||||
Map<String, dynamic>? headers,
|
||||
}) async {
|
||||
try {
|
||||
Response response = await _dio.get(
|
||||
endpoint,
|
||||
options: Options(headers: headers),
|
||||
);
|
||||
|
||||
return response.data;
|
||||
} on DioException catch (e) {
|
||||
// 🔥 THIS IS THE FIX
|
||||
if (e.response != null) {
|
||||
print("ERROR DATA: ${e.response?.data}");
|
||||
return e.response?.data; // ✅ return actual backend response
|
||||
} else {
|
||||
print("DIO ERROR: ${e.message}");
|
||||
return null;
|
||||
}
|
||||
} catch (e) {
|
||||
print("UNKNOWN ERROR: $e");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// POST Request
|
||||
Future<dynamic> postData(String endpoint, Map<String, dynamic> data) async {
|
||||
try {
|
||||
Response response = await _dio.post(endpoint, data: data);
|
||||
return response.data;
|
||||
} catch (e) {
|
||||
return _handleError(e);
|
||||
}
|
||||
}
|
||||
|
||||
/// PUT Request
|
||||
Future<dynamic> putData(String endpoint, Map<String, dynamic> data) async {
|
||||
try {
|
||||
Response response = await _dio.put(endpoint, data: data);
|
||||
return response.data;
|
||||
} catch (e) {
|
||||
return _handleError(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// DELETE Request
|
||||
Future<dynamic> deleteData(String endpoint) async {
|
||||
try {
|
||||
Response response = await _dio.delete(endpoint);
|
||||
return response.data;
|
||||
} catch (e) {
|
||||
return _handleError(e);
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle Errors
|
||||
dynamic _handleError(dynamic error) {
|
||||
if (error is DioException) {
|
||||
return "Error: ${error.message}";
|
||||
}
|
||||
return "Unexpected Error";
|
||||
}
|
||||
}
|
||||
32
lib/service/location/location.dart
Normal file
32
lib/service/location/location.dart
Normal file
@@ -0,0 +1,32 @@
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
|
||||
class LocationService {
|
||||
static Future<Position> getCurrentLocation() async {
|
||||
bool serviceEnabled;
|
||||
LocationPermission permission;
|
||||
|
||||
// Check if location services are enabled
|
||||
serviceEnabled = await Geolocator.isLocationServiceEnabled();
|
||||
if (!serviceEnabled) {
|
||||
return Future.error('Location services are disabled.');
|
||||
}
|
||||
|
||||
// Check for permission
|
||||
permission = await Geolocator.checkPermission();
|
||||
if (permission == LocationPermission.denied) {
|
||||
permission = await Geolocator.requestPermission();
|
||||
if (permission == LocationPermission.denied) {
|
||||
return Future.error('Location permissions are denied');
|
||||
}
|
||||
}
|
||||
|
||||
if (permission == LocationPermission.deniedForever) {
|
||||
return Future.error(
|
||||
'Location permissions are permanently denied.');
|
||||
}
|
||||
|
||||
// Get current location
|
||||
return await Geolocator.getCurrentPosition(
|
||||
desiredAccuracy: LocationAccuracy.high);
|
||||
}
|
||||
}
|
||||
331
lib/service/notification.dart
Normal file
331
lib/service/notification.dart
Normal file
@@ -0,0 +1,331 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
import '../Helper/Logger.dart';
|
||||
|
||||
|
||||
class LocalNotificationService {
|
||||
static final FirebaseMessaging _firebaseMessaging = FirebaseMessaging.instance;
|
||||
static final FlutterLocalNotificationsPlugin _notificationsPlugin =
|
||||
FlutterLocalNotificationsPlugin();
|
||||
|
||||
static const AndroidNotificationChannel channel = AndroidNotificationChannel(
|
||||
'Nearle', // Channel ID
|
||||
'Nearle Notification', // Channel name
|
||||
description: 'Channel for Nearle notifications', // Channel description
|
||||
importance: Importance.max,
|
||||
playSound: true,
|
||||
sound: RawResourceAndroidNotificationSound('notification_ring'),
|
||||
enableVibration: true,
|
||||
showBadge: true,
|
||||
);
|
||||
|
||||
static Future<void> initialize(BuildContext context, String status) async {
|
||||
// Create Android notification channel
|
||||
await _notificationsPlugin
|
||||
.resolvePlatformSpecificImplementation<
|
||||
AndroidFlutterLocalNotificationsPlugin>()
|
||||
?.createNotificationChannel(channel);
|
||||
|
||||
// Initialize local notifications
|
||||
const InitializationSettings initializationSettings = InitializationSettings(
|
||||
android: AndroidInitializationSettings("@mipmap/ic_launcher"),
|
||||
iOS: DarwinInitializationSettings(
|
||||
requestSoundPermission: true,
|
||||
requestBadgePermission: true,
|
||||
requestAlertPermission: true,
|
||||
defaultPresentSound: true,
|
||||
defaultPresentBadge: true,
|
||||
defaultPresentBanner: true,
|
||||
defaultPresentAlert: true,
|
||||
defaultPresentList: true,
|
||||
),
|
||||
);
|
||||
|
||||
await _notificationsPlugin.initialize(
|
||||
initializationSettings,
|
||||
onDidReceiveNotificationResponse: (NotificationResponse response) async {
|
||||
if (response.payload != null) {
|
||||
await selectNotification(response.payload);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Handle messages when the app is opened from a terminated state
|
||||
RemoteMessage? initialMessage = await _firebaseMessaging.getInitialMessage();
|
||||
if (initialMessage != null) {
|
||||
await _handleInitialMessage(initialMessage);
|
||||
}
|
||||
|
||||
// Handle foreground messages
|
||||
FirebaseMessaging.onMessage.listen((RemoteMessage message) async {
|
||||
logger.i('Received foreground message: ${message.data}');
|
||||
await _handleMessage(message);
|
||||
});
|
||||
|
||||
// Handle messages when the app is opened from a notification
|
||||
FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) async {
|
||||
logger.i('Message opened app: ${message.data}');
|
||||
await _handleMessageOpenedApp(message);
|
||||
});
|
||||
}
|
||||
|
||||
static Future<void> _handleInitialMessage(RemoteMessage message) async {
|
||||
if (message.notification != null) {
|
||||
// Cancel the notification
|
||||
final notificationId = int.parse(message.data['notification_id'] ?? '0');
|
||||
await _notificationsPlugin.cancel(notificationId);
|
||||
await display(message);
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> _handleMessage(RemoteMessage message) async {
|
||||
if (message.notification != null) {
|
||||
logger.i('Displaying notification: ${message.notification!.body}');
|
||||
await display(message);
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> _handleMessageOpenedApp(RemoteMessage message) async {
|
||||
try {
|
||||
// Cancel the notification using the ID from message.data
|
||||
final notificationId = int.parse(message.data['notification_id'] ?? '0');
|
||||
await _notificationsPlugin.cancel(notificationId);
|
||||
|
||||
if (message.notification != null) {
|
||||
// Navigator.of(Get.context!).push(
|
||||
// MaterialPageRoute(
|
||||
// builder: (BuildContext context) => HomeView(selectedIndex: 1),
|
||||
// ),
|
||||
// );
|
||||
}
|
||||
} on Exception catch (e) {
|
||||
logger.e('Error in handleMessageOpenedApp: $e');
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> selectNotification(String? payload) async {
|
||||
try {
|
||||
if (payload != null) {
|
||||
final decodedPayload = jsonDecode(payload);
|
||||
final notificationId = int.parse(decodedPayload['id'] ?? '0');
|
||||
|
||||
// Cancel the specific notification
|
||||
await _notificationsPlugin.cancel(notificationId);
|
||||
|
||||
// Navigate to the desired page
|
||||
// Navigator.of(Get.context!).push(
|
||||
// MaterialPageRoute(
|
||||
// builder: (BuildContext context) => HomeView(selectedIndex: 1),
|
||||
// ),
|
||||
// );
|
||||
}
|
||||
} on Exception catch (e) {
|
||||
logger.e('Error in selectNotification: $e');
|
||||
}
|
||||
}
|
||||
|
||||
static Future<String?> _downloadAndSaveImage(String imageUrl, String fileName) async {
|
||||
try {
|
||||
final directory = await getTemporaryDirectory();
|
||||
final filePath = '${directory.path}/$fileName';
|
||||
final response = await http.get(Uri.parse(imageUrl));
|
||||
if (response.statusCode == 200) {
|
||||
final file = File(filePath);
|
||||
await file.writeAsBytes(response.bodyBytes);
|
||||
logger.i('Image downloaded to: $filePath');
|
||||
return filePath;
|
||||
} else {
|
||||
logger.e('Failed to download image: ${response.statusCode}');
|
||||
return null;
|
||||
}
|
||||
} catch (e) {
|
||||
logger.e('Error downloading image: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static String? _extractImageUrl(RemoteMessage message) {
|
||||
// General data field
|
||||
String? imageUrl = message.data['image'] as String?;
|
||||
|
||||
// Android-specific
|
||||
if (imageUrl == null && message.notification?.android?.imageUrl != null) {
|
||||
imageUrl = message.notification!.android!.imageUrl;
|
||||
}
|
||||
|
||||
// iOS-specific
|
||||
if (imageUrl == null && message.notification?.apple?.imageUrl != null) {
|
||||
imageUrl = message.notification!.apple!.imageUrl;
|
||||
}
|
||||
|
||||
logger.i('Extracted image URL: $imageUrl');
|
||||
return imageUrl;
|
||||
}
|
||||
|
||||
static Future<void> display(RemoteMessage message) async {
|
||||
if (message.notification != null) {
|
||||
// Navigator.of(Get.context!).push(
|
||||
// MaterialPageRoute(
|
||||
// builder: (BuildContext context) => HomeView(selectedIndex: 1),
|
||||
// ),
|
||||
// );
|
||||
}
|
||||
|
||||
try {
|
||||
final id = DateTime.now().millisecondsSinceEpoch ~/ 1000;
|
||||
|
||||
// Create a payload that includes the notification ID
|
||||
final payload = {
|
||||
'id': id.toString(),
|
||||
'data': message.data,
|
||||
};
|
||||
|
||||
NotificationDetails notificationDetails;
|
||||
|
||||
// Extract image URL from data or android-specific field
|
||||
final imageUrl = _extractImageUrl(message);
|
||||
|
||||
if (imageUrl != null && imageUrl.isNotEmpty) {
|
||||
// Download the image
|
||||
final imagePath = await _downloadAndSaveImage(imageUrl, 'notification_image.jpg');
|
||||
if (imagePath != null) {
|
||||
if (Platform.isAndroid) {
|
||||
|
||||
String? bodyText = message.notification?.body ?? message.data['body'] ?? '';
|
||||
List<String>? lines = bodyText?.split('\n'); // or split on ',' or build manually
|
||||
|
||||
final inboxStyle = InboxStyleInformation(
|
||||
lines ?? [],
|
||||
contentTitle: message.notification?.title ?? message.data['title'],
|
||||
summaryText: '', // optional, can be empty or a short summary
|
||||
);
|
||||
|
||||
notificationDetails = NotificationDetails(
|
||||
android: AndroidNotificationDetails(
|
||||
'Nearle',
|
||||
'Nearle Notification',
|
||||
importance: Importance.max,
|
||||
priority: Priority.high,
|
||||
icon: 'notification',
|
||||
playSound: true,
|
||||
sound: RawResourceAndroidNotificationSound('notification_ring'),
|
||||
enableVibration: true,
|
||||
fullScreenIntent: true,
|
||||
channelShowBadge: true,
|
||||
ongoing: false, // Allow dismissal
|
||||
autoCancel: true, // Cancel when tapped
|
||||
styleInformation: inboxStyle,
|
||||
),
|
||||
iOS: DarwinNotificationDetails(
|
||||
presentAlert: true,
|
||||
presentBadge: true,
|
||||
presentSound: true,
|
||||
presentList: true,
|
||||
presentBanner: true,
|
||||
attachments: [
|
||||
DarwinNotificationAttachment(imagePath),
|
||||
],
|
||||
),
|
||||
);
|
||||
} else {
|
||||
// iOS or other platforms
|
||||
notificationDetails = NotificationDetails(
|
||||
android: AndroidNotificationDetails(
|
||||
'Nearle',
|
||||
'Nearle Notification',
|
||||
importance: Importance.max,
|
||||
priority: Priority.high,
|
||||
icon: 'notification',
|
||||
playSound: true,
|
||||
sound: RawResourceAndroidNotificationSound('notification_ring'),
|
||||
enableVibration: true,
|
||||
fullScreenIntent: true,
|
||||
channelShowBadge: true,
|
||||
ongoing: false, // Allow dismissal
|
||||
autoCancel: true, // Cancel when tapped
|
||||
),
|
||||
iOS: DarwinNotificationDetails(
|
||||
presentAlert: true,
|
||||
presentBadge: true,
|
||||
presentSound: true,
|
||||
presentList: true,
|
||||
presentBanner: true,
|
||||
attachments: [
|
||||
DarwinNotificationAttachment(imagePath),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Fallback if image download fails
|
||||
notificationDetails = const NotificationDetails(
|
||||
android: AndroidNotificationDetails(
|
||||
'Nearle',
|
||||
'Nearle Notification',
|
||||
importance: Importance.max,
|
||||
priority: Priority.high,
|
||||
icon: 'notification',
|
||||
playSound: true,
|
||||
sound: RawResourceAndroidNotificationSound('notification_ring'),
|
||||
enableVibration: true,
|
||||
fullScreenIntent: true,
|
||||
channelShowBadge: true,
|
||||
ongoing: false, // Allow dismissal
|
||||
autoCancel: true, // Cancel when tapped
|
||||
),
|
||||
iOS: DarwinNotificationDetails(
|
||||
presentAlert: true,
|
||||
presentBadge: true,
|
||||
presentSound: true,
|
||||
presentList: true,
|
||||
presentBanner: true,
|
||||
),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// No image in payload, use default notification details
|
||||
notificationDetails = const NotificationDetails(
|
||||
android: AndroidNotificationDetails(
|
||||
'Nearle',
|
||||
'Nearle Notification',
|
||||
importance: Importance.max,
|
||||
priority: Priority.high,
|
||||
icon: 'notification',
|
||||
playSound: true,
|
||||
sound: RawResourceAndroidNotificationSound('notification_ring'),
|
||||
enableVibration: true,
|
||||
fullScreenIntent: true,
|
||||
channelShowBadge: true,
|
||||
ongoing: false, // Allow dismissal
|
||||
autoCancel: true, // Cancel when tapped
|
||||
),
|
||||
iOS: DarwinNotificationDetails(
|
||||
presentAlert: true,
|
||||
presentBadge: true,
|
||||
presentSound: true,
|
||||
presentList: true,
|
||||
presentBanner: true,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
await _notificationsPlugin.show(
|
||||
id,
|
||||
message.notification?.title ?? message.data['title'] ?? 'Nearle',
|
||||
message.notification?.body ?? message.data['body'] ?? 'Notification',
|
||||
notificationDetails,
|
||||
payload: jsonEncode(payload),
|
||||
);
|
||||
}
|
||||
on Exception catch (e) {
|
||||
logger.e('Error displaying notification: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
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,
|
||||
// ),
|
||||
// )
|
||||
|
||||
);
|
||||
}
|
||||
}
|
||||
139
lib/widgets/slider_button.dart
Normal file
139
lib/widgets/slider_button.dart
Normal file
@@ -0,0 +1,139 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:nearledaily/constants/color_constants.dart';
|
||||
|
||||
class SliderCheckoutButton extends StatefulWidget {
|
||||
final Future<void> Function() onConfirmed;
|
||||
|
||||
const SliderCheckoutButton({super.key, required this.onConfirmed});
|
||||
|
||||
@override
|
||||
State<SliderCheckoutButton> createState() => _SliderCheckoutButtonState();
|
||||
}
|
||||
|
||||
class _SliderCheckoutButtonState extends State<SliderCheckoutButton> {
|
||||
double _dragPosition = 0;
|
||||
bool _isLoading = false;
|
||||
final double _thumbSize = 52;
|
||||
final double _trackHeight = 56;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(builder: (context, constraints) {
|
||||
final trackWidth = constraints.maxWidth;
|
||||
final maxDrag = trackWidth - _thumbSize - 8;
|
||||
final progress = _dragPosition / maxDrag;
|
||||
|
||||
return Container(
|
||||
height: _trackHeight,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
ColorConstants.primaryColor,
|
||||
ColorConstants.primaryColor,
|
||||
|
||||
],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
),
|
||||
child: Stack(
|
||||
alignment: Alignment.centerLeft,
|
||||
children: [
|
||||
// ── CENTER LABEL ──────────────────────────────
|
||||
Center(
|
||||
child: _isLoading
|
||||
? Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: const [
|
||||
SizedBox(
|
||||
width: 18,
|
||||
height: 18,
|
||||
child: CircularProgressIndicator(
|
||||
color: Colors.white,
|
||||
strokeWidth: 2,
|
||||
),
|
||||
),
|
||||
SizedBox(width: 10),
|
||||
Text(
|
||||
'Please wait...',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
letterSpacing: 0.4,
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: Opacity(
|
||||
opacity: (1 - progress * 2).clamp(0.0, 1.0),
|
||||
child: const Text(
|
||||
'Slide to Checkout',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// ── THUMB (hidden while loading) ──────────────
|
||||
if (!_isLoading)
|
||||
Positioned(
|
||||
left: 4 + _dragPosition,
|
||||
child: GestureDetector(
|
||||
onHorizontalDragUpdate: (details) {
|
||||
setState(() {
|
||||
_dragPosition =
|
||||
(_dragPosition + details.delta.dx).clamp(0.0, maxDrag);
|
||||
});
|
||||
},
|
||||
onHorizontalDragEnd: (_) async {
|
||||
if (_dragPosition >= maxDrag * 0.85) {
|
||||
setState(() {
|
||||
_dragPosition = 0;
|
||||
_isLoading = true;
|
||||
});
|
||||
try {
|
||||
await widget.onConfirmed();
|
||||
} finally {
|
||||
|
||||
if (mounted) {
|
||||
setState(() => _isLoading = false);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
setState(() => _dragPosition = 0);
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
width: _thumbSize,
|
||||
height: _thumbSize,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.15),
|
||||
blurRadius: 6,
|
||||
offset: const Offset(0, 3),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Icon(
|
||||
Icons.arrow_forward_ios_rounded,
|
||||
color: Theme.of(context).primaryColor,
|
||||
size: 22,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
117
lib/widgets/tenantcategory.dart
Normal file
117
lib/widgets/tenantcategory.dart
Normal file
@@ -0,0 +1,117 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:nearledaily/constants/color_constants.dart';
|
||||
import '../modules/tenant/category.dart';
|
||||
|
||||
class CategoryHeaderDelegate extends SliverPersistentHeaderDelegate {
|
||||
final List<Category> categories;
|
||||
final int selectedIndex;
|
||||
final Function(int) onTap;
|
||||
|
||||
CategoryHeaderDelegate({
|
||||
required this.categories,
|
||||
required this.selectedIndex,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
double get minExtent => 90;
|
||||
|
||||
@override
|
||||
double get maxExtent => 90;
|
||||
|
||||
@override
|
||||
Widget build(
|
||||
BuildContext context, double shrinkOffset, bool overlapsContent) {
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
|
||||
return Container(
|
||||
color: Colors.white,
|
||||
child: ListView.builder(
|
||||
scrollDirection: Axis.horizontal,
|
||||
physics: const BouncingScrollPhysics(),
|
||||
itemCount: categories.length,
|
||||
itemBuilder: (context, index) {
|
||||
final item = categories[index];
|
||||
|
||||
// 🔥 Responsive width (5 items visible)
|
||||
final itemWidth = screenWidth / 5;
|
||||
|
||||
final isSelected = selectedIndex == index;
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () => onTap(index),
|
||||
child: SizedBox(
|
||||
|
||||
width: itemWidth,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
// 🔵 Icon Container
|
||||
AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
height: 55,
|
||||
width: 55,
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected
|
||||
? Colors.transparent
|
||||
: Colors.transparent,
|
||||
// borderRadius: BorderRadius.circular(14),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: item.icon.isNotEmpty
|
||||
? Image.network(
|
||||
item.icon,
|
||||
height: 50,
|
||||
width: 50,
|
||||
fit: BoxFit.contain,
|
||||
errorBuilder: (_, __, ___) =>
|
||||
const Icon(Icons.image, size: 40),
|
||||
)
|
||||
: const Icon(Icons.image, size: 40),
|
||||
),
|
||||
|
||||
const SizedBox(height: 3),
|
||||
|
||||
// 🏷️ Text
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
child: Text(
|
||||
item.name,
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w600,
|
||||
color:
|
||||
isSelected ? Colors.black87 : Colors.grey.shade800,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 3),
|
||||
|
||||
// 🔥 Bottom Indicator
|
||||
AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
height: 3,
|
||||
width: isSelected ? 30 : 0,
|
||||
decoration: BoxDecoration(
|
||||
color: ColorConstants.primaryColor,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRebuild(covariant CategoryHeaderDelegate oldDelegate) {
|
||||
return true; // required for GetX updates
|
||||
}
|
||||
}
|
||||
233
lib/widgets/text_widget.dart
Normal file
233
lib/widgets/text_widget.dart
Normal file
@@ -0,0 +1,233 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:nearledaily/constants/color_constants.dart';
|
||||
|
||||
import '../constants/font_constants.dart';
|
||||
|
||||
|
||||
class ReusableTextWidget extends StatelessWidget {
|
||||
final String text;
|
||||
final double? fontSize;
|
||||
final double? textHeight;
|
||||
final TextOverflow? overflow;
|
||||
final String? fontFamily;
|
||||
final FontWeight? fontWeight;
|
||||
final FontStyle? fontStyle;
|
||||
final Color? color;
|
||||
final TextAlign? textAlign;
|
||||
final int? maxLines;
|
||||
final TextDecoration? isUnderText;
|
||||
final TextDecoration? textDecoration;
|
||||
|
||||
|
||||
const ReusableTextWidget({
|
||||
super.key,
|
||||
required this.text,
|
||||
this.fontSize,
|
||||
this.textHeight,
|
||||
this.fontFamily,
|
||||
this.fontWeight,
|
||||
this.fontStyle,
|
||||
this.overflow,
|
||||
this.color,
|
||||
this.textAlign,
|
||||
this.maxLines,
|
||||
this.isUnderText,
|
||||
this.textDecoration,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Text(
|
||||
text,
|
||||
softWrap: true,
|
||||
maxLines: maxLines,
|
||||
overflow: overflow ?? TextOverflow.ellipsis,
|
||||
textAlign: textAlign ?? TextAlign.start,
|
||||
style: TextStyle(
|
||||
fontFamily: fontFamily ?? FontConstants.fontFamily,
|
||||
fontWeight: fontWeight ?? FontWeight.normal,
|
||||
fontStyle: fontStyle ?? FontStyle.normal,
|
||||
fontSize: fontSize ?? 13,
|
||||
height: textHeight,
|
||||
color: color ?? Colors.grey.shade900,
|
||||
decoration: isUnderText,
|
||||
decorationColor: color,
|
||||
decorationStyle: TextDecorationStyle.solid,
|
||||
decorationThickness: 1,
|
||||
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class CategoryStickyHeader extends SliverPersistentHeaderDelegate {
|
||||
final List categories;
|
||||
int currentIndex;
|
||||
|
||||
CategoryStickyHeader({
|
||||
required this.categories,
|
||||
this.currentIndex = 0,
|
||||
});
|
||||
|
||||
@override
|
||||
double get minExtent => 80;
|
||||
|
||||
@override
|
||||
double get maxExtent => 80;
|
||||
|
||||
@override
|
||||
Widget build(
|
||||
BuildContext context,
|
||||
double shrinkOffset,
|
||||
bool overlapsContent,
|
||||
) {
|
||||
final bool isPinned = shrinkOffset > 0 || overlapsContent;
|
||||
|
||||
return AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 180),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
boxShadow: isPinned
|
||||
? [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.12),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
]
|
||||
: [],
|
||||
),
|
||||
child: SizedBox(
|
||||
height: 80,
|
||||
child: StatefulBuilder(
|
||||
builder: (context, setHeaderState) {
|
||||
return ListView.builder(
|
||||
scrollDirection: Axis.horizontal,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10),
|
||||
itemCount: categories.length,
|
||||
itemBuilder: (context, index) {
|
||||
final item = categories[index];
|
||||
final bool isActive = index == currentIndex;
|
||||
|
||||
return _CategoryItem(
|
||||
item: item,
|
||||
isActive: isActive,
|
||||
onTap: () {
|
||||
setHeaderState(() {
|
||||
currentIndex = index;
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/// -------------------------------------------------------------------------
|
||||
/// CATEGORY ITEM WIDGET (handles zoom + bounce cleanly)
|
||||
/// -------------------------------------------------------------------------
|
||||
class _CategoryItem extends StatefulWidget {
|
||||
final Map item;
|
||||
final bool isActive;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const _CategoryItem({
|
||||
required this.item,
|
||||
required this.isActive,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
State<_CategoryItem> createState() => _CategoryItemState();
|
||||
}
|
||||
|
||||
class _CategoryItemState extends State<_CategoryItem> {
|
||||
double _scale = 1.0;
|
||||
|
||||
Future<void> _animateTap() async {
|
||||
setState(() => _scale = 0.88);
|
||||
await Future.delayed(const Duration(milliseconds: 90));
|
||||
setState(() => _scale = 1.0);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTapDown: (_) {
|
||||
setState(() => _scale = 0.88);
|
||||
},
|
||||
onTapCancel: () {
|
||||
setState(() => _scale = 1.0);
|
||||
},
|
||||
onTap: () async {
|
||||
await _animateTap();
|
||||
widget.onTap();
|
||||
},
|
||||
child: AnimatedScale(
|
||||
scale: _scale,
|
||||
duration: const Duration(milliseconds: 120),
|
||||
curve: Curves.easeOutBack,
|
||||
child: Container(
|
||||
width: 57,
|
||||
margin: const EdgeInsets.only(right: 14),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
Container(
|
||||
height: 57,
|
||||
width: 57,
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: const BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Image.network(
|
||||
widget.item["icon"],
|
||||
fit: BoxFit.contain,
|
||||
errorBuilder: (_, __, ___) =>
|
||||
const Icon(Icons.image, size: 24),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 0),
|
||||
ReusableTextWidget(
|
||||
text: widget.item["name"],
|
||||
color: Colors.black,
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w600,
|
||||
maxLines: 1,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Container(
|
||||
height: 3,
|
||||
width: 30,
|
||||
decoration: BoxDecoration(
|
||||
color: widget.isActive
|
||||
? ColorConstants.primaryColor
|
||||
: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user