629 lines
20 KiB
Dart
629 lines
20 KiB
Dart
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});
|
|
}
|
|
|
|
|