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