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