first commit
This commit is contained in:
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user