Files
2026-05-26 18:01:57 +05:30

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