first commit

This commit is contained in:
Anbarasu
2026-05-26 18:01:57 +05:30
commit 6d59c8daf6
297 changed files with 35238 additions and 0 deletions

View File

@@ -0,0 +1,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');
}
}
}
}

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

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

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

View 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;
}
}

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

View File

@@ -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());
}
}

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

View 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 whats 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)");
}
}

View File

@@ -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
}
}}

View 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 ?? [];
}
}

View 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 products 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();
}
}

View 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;
}
}
}

View 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;
}
}
}

View 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;
}
}
}