diff --git a/lib/controller/product_controller.dart b/lib/controller/product_controller.dart new file mode 100644 index 0000000..5af5929 --- /dev/null +++ b/lib/controller/product_controller.dart @@ -0,0 +1,64 @@ +import 'package:cheminova/controller/product_service.dart'; +import 'package:get/get.dart'; + + +class ProductController extends GetxController { + final ProductService productService = ProductService(); + var products = >[].obs; + var categories = [].obs; // Holds the list of categories + var selectedCategory = Rxn(); // Holds the selected category + int _currentPage = 1; + bool isLoading = false; + + @override + void onInit() { + super.onInit(); + getCategory(); + getUser(); + } + + Future getUser() async { + if (isLoading) return; + isLoading = true; + try { + final category = selectedCategory.value; // Get the selected category + final fetchedProducts = await productService.getProduct( + _currentPage, + category: category, + ); + + if (fetchedProducts != null) { + products.addAll(fetchedProducts as Iterable>); + } + } catch (e) { + print("Error fetching products: $e"); + } finally { + isLoading = false; + update(); + } + } + + Future getCategory() async { + try { + final fetchedCategories = await productService.getCategory(); + if (fetchedCategories != null) { + categories.assignAll(fetchedCategories.map((category) => category['categoryName'] as String)); + categories.insert(0, 'All'); // Add "All" option + } + } catch (e) { + print("Error fetching categories: $e"); + } + } + + void setCategory(String category) { + selectedCategory.value = category == 'All' ? null : category; + _currentPage = 1; + products.clear(); + getUser(); + } + + void loadMoreProducts() { + _currentPage++; + getUser(); + } +} diff --git a/lib/controller/product_service.dart b/lib/controller/product_service.dart new file mode 100644 index 0000000..0b13240 --- /dev/null +++ b/lib/controller/product_service.dart @@ -0,0 +1,60 @@ +import '../utils/common_api_service.dart'; +import '../utils/show_snackbar.dart'; + +class ProductService { + Future>?> getProduct(int page, {String? category}) async { + try { + String url; + if (category != null && category.isNotEmpty) { + url = "/api/product/getAll/user?page=$page&category=$category"; + } else { + url = "/api/product/getAll/user?page=$page"; // URL without category filter + } + + final response = await commonApiService>>( + method: "GET", + url: url, + fromJson: (json) { + if (json['products'] != null) { + final List> products = (json['products'] as List) + .map((productJson) => productJson as Map) + .toList(); + return products; + } else { + return []; + } + }, + ); + return response; + } catch (e) { + showSnackbar(e.toString()); + //print("Error: $e"); + } + return null; + } + + + Future>?> getCategory() async { + try { + final response = await commonApiService>>( + method: "GET", + url: "/api/category/getCategories", + fromJson: (json) { + if (json['categories'] != null) { + final List> category = (json['categories'] as List) + .map((productJson) => productJson as Map) + .toList(); + return category; + } else { + return []; + } + }, + ); + return response; + } catch (e) { + showSnackbar(e.toString()); + print("Error: $e"); + } + return null; + } +} diff --git a/lib/screens/authentication/forget_password_screen.dart b/lib/screens/authentication/forget_password_screen.dart index aa55670..2a9cebf 100644 --- a/lib/screens/authentication/forget_password_screen.dart +++ b/lib/screens/authentication/forget_password_screen.dart @@ -110,7 +110,7 @@ class _ForgetPasswordScreenState extends State { InputField( hintText: "Email", labelText: "Email", - controller: userNameController, + controller: authController.emailController, keyboardType: TextInputType.emailAddress, ), const SizedBox(height: 30), diff --git a/lib/screens/product/product_catalog_screen.dart b/lib/screens/product/product_catalog_screen.dart index a19202a..6a35d9e 100644 --- a/lib/screens/product/product_catalog_screen.dart +++ b/lib/screens/product/product_catalog_screen.dart @@ -1,10 +1,10 @@ -import 'package:cheminova/models/product_model.dart'; -import 'package:cheminova/widgets/my_drawer.dart'; -import 'package:cheminova/widgets/product_card.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; import 'package:get/get.dart'; import 'package:google_fonts/google_fonts.dart'; +import '../../widgets/my_drawer.dart'; +import '../../widgets/product_card.dart'; +import '../../controller/product_service.dart'; class ProductCatalogScreen extends StatefulWidget { const ProductCatalogScreen({super.key}); @@ -14,22 +14,101 @@ class ProductCatalogScreen extends StatefulWidget { } class _ProductCatalogScreenState extends State { - final List _productList = [ - ProductModel( - id: '1', - name: 'Product 1', - price: 100, - description: 'Description 1', - category: ProductCategory.food, - image: 'assets/images/product.png', - ) - ]; + final ProductService _productService = ProductService(); + final ScrollController _scrollController = ScrollController(); + List> _products = []; + List> _categories = []; + + int _currentPage = 1; + bool _isLoading = false; + bool _hasMoreData = true; + + String? _selectedCategory; // Default to null to handle 'All' explicitly + String? _selectedPriceRange; + String? _selectedAvailability; + + @override + void initState() { + super.initState(); + _fetchCategories(); // Fetch categories first to set initial filter + _fetchProducts(); // Fetch products after setting initial filters + _scrollController.addListener(() { + if (_scrollController.position.pixels == _scrollController.position.maxScrollExtent && + !_isLoading && + _hasMoreData) { + _fetchMoreProducts(); + } + }); + } + + Future _fetchProducts() async { + setState(() { + _isLoading = true; + }); + + // Adjust the category parameter based on the selected category + final category = _selectedCategory == 'All' ? null : _selectedCategory; + + final products = await _productService.getProduct(_currentPage, category: category); + setState(() { + if (products != null) { + _products.addAll(products); + _hasMoreData = products.isNotEmpty; + } + _isLoading = false; + }); + } + + Future _fetchCategories() async { + final categories = await _productService.getCategory(); + if (categories != null) { + setState(() { + _categories = categories; + // Add "All" option + _categories.insert(1, {'categoryName': 'All'}); + _selectedCategory = 'All'; // Set initial selected category to "All" + }); + } + } + + void _onCategoryChanged(String? newCategory) { + if (newCategory != null) { + setState(() { + _selectedCategory = newCategory; + _products.clear(); + _currentPage = 1; + }); + _fetchProducts(); + } + } + + Future _fetchMoreProducts() async { + if (!_isLoading && _hasMoreData) { + setState(() { + _isLoading = true; + _currentPage++; + }); + await _fetchProducts(); + } + } + + void _clearFilters() { + setState(() { + _selectedCategory = null; + _selectedPriceRange = null; + _selectedAvailability = null; + _products.clear(); + _currentPage = 1; + }); + _fetchProducts(); + } + + @override + void dispose() { + _scrollController.dispose(); + super.dispose(); + } - final List _filterList = [ - "Category", - "Price Range", - "Availability", - ]; @override Widget build(BuildContext context) { return Scaffold( @@ -74,67 +153,158 @@ class _ProductCatalogScreenState extends State { fit: BoxFit.cover, ), SafeArea( - child: Column( - children: [ - SizedBox( - height: Get.height * 0.02, - ), - Card( - margin: const EdgeInsets.symmetric(horizontal: 18), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(19), - side: const BorderSide(color: Color(0xFFFDFDFD)), + child: SingleChildScrollView( + child: Column( + children: [ + SizedBox(height: Get.height * 0.02), + // Search Bar + Padding( + padding: const EdgeInsets.symmetric(horizontal: 18.0), + child: TextField( + decoration: InputDecoration( + hintText: "Search Products", + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(15), + ), + filled: true, + fillColor: Colors.white.withOpacity(0.9), + ), + ), ), - color: const Color(0xFFB4D1E5).withOpacity(0.9), - child: Padding( - padding: const EdgeInsets.all(12.0), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - SizedBox( - height: Get.height * 0.05, - child: ListView.builder( - shrinkWrap: true, - scrollDirection: Axis.horizontal, - itemCount: _filterList.length, - itemBuilder: (context, index) => Padding( - padding: - const EdgeInsets.symmetric(horizontal: 4), - child: Chip( - label: Text( - _filterList[index], - style: GoogleFonts.roboto( + SizedBox(height: Get.height * 0.02), + Card( + margin: const EdgeInsets.symmetric(horizontal: 18), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(19), + side: const BorderSide(color: Color(0xFFFDFDFD)), + ), + color: const Color(0xFFB4D1E5).withOpacity(0.9), + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Filters", + style: GoogleFonts.poppins( + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), + TextButton( + onPressed: _clearFilters, + child: Text( + "Clear Filters", + style: GoogleFonts.poppins( + color: Colors.red, fontSize: 14, - fontWeight: FontWeight.w500, ), ), ), - ), + ], ), - ), - SizedBox( - height: Get.height * 0.6, - child: ListView.builder( - padding: EdgeInsets.zero, - shrinkWrap: true, - itemCount: 10, - itemBuilder: (context, index) => Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: ProductCard( - product: _productList[0], + SizedBox( + height: Get.height * 0.05, + child: ListView.builder( + shrinkWrap: true, + scrollDirection: Axis.horizontal, + itemCount: 3, + itemBuilder: (context, index) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: _buildFilterDropdown(index), ), ), ), - ) - ], + SizedBox( + height: Get.height * 0.6, + child: ListView.builder( + controller: _scrollController, + padding: EdgeInsets.zero, + itemCount: _products.length + (_isLoading ? 1 : 0), + itemBuilder: (context, index) { + if (index >= _products.length) { + print("Product length $_products"); + return const Center(child: CircularProgressIndicator()); + } + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: ProductCard( + productModel: _products[index], + ), + ); + }, + ), + ), + ], + ), ), ), - ), - ], + ], + ), ), ), ], ), ); } + + Widget _buildFilterDropdown(int index) { + switch (index) { + case 0: + return DropdownButton( + value: _selectedCategory, + hint: const Text("Category"), + onChanged: (value) { + if (value != null) { + _onCategoryChanged(value); + } + }, + items: _categories.map>((category) { + return DropdownMenuItem( + value: category['categoryName'], + child: Text(category['categoryName']), + ); + }).toList(), + ); + case 1: + return DropdownButton( + value: _selectedPriceRange, + hint: const Text("Price Range"), + onChanged: (value) { + setState(() { + _selectedPriceRange = value; + }); + }, + items: ['\$0-\$50', '\$51-\$100', '\$101-\$150'] + .map>((String value) { + return DropdownMenuItem( + value: value, + child: Text(value), + ); + }).toList(), + ); + case 2: + return DropdownButton( + value: _selectedAvailability, + hint: const Text("Availability"), + onChanged: (value) { + setState(() { + _selectedAvailability = value; + }); + }, + items: ['In Stock', 'Out of Stock'] + .map>((String value) { + return DropdownMenuItem( + value: value, + child: Text(value), + ); + }).toList(), + ); + default: + return Container(); + } + } } diff --git a/lib/screens/product/product_detail_screen.dart b/lib/screens/product/product_detail_screen.dart index 133cd40..edd8574 100644 --- a/lib/screens/product/product_detail_screen.dart +++ b/lib/screens/product/product_detail_screen.dart @@ -1,14 +1,19 @@ import 'package:cheminova/models/product_model.dart'; -import 'package:cheminova/screens/product/cart_screen.dart'; +import 'package:cheminova/screens/order/checkout_screen.dart'; import 'package:cheminova/widgets/my_drawer.dart'; +import 'package:cheminova/widgets/product_card.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; import 'package:get/get.dart'; import 'package:google_fonts/google_fonts.dart'; +import 'cart_screen.dart'; + class ProductDetailScreen extends StatefulWidget { - final ProductModel product; - const ProductDetailScreen({super.key, required this.product}); + final Map? productModel; + ProductModel? product; + + ProductDetailScreen({super.key, this.product, this.productModel}); @override State createState() => _ProductDetailScreenState(); @@ -93,17 +98,18 @@ class _ProductDetailScreenState extends State { ), child: ClipRRect( borderRadius: BorderRadius.circular(10), - child: Image.asset( - widget.product.image, - fit: BoxFit.cover, - ), + child: Image.asset("assets/images/product.png", fit: BoxFit.cover,), + // Image.asset( + // widget.product.image, + // fit: BoxFit.cover, + // ), ), ), ), Padding( padding: const EdgeInsets.symmetric(horizontal: 8), child: Text( - widget.product.name, + widget.productModel!['name'], style: GoogleFonts.roboto( fontSize: 20, fontWeight: FontWeight.w600, @@ -114,7 +120,7 @@ class _ProductDetailScreenState extends State { Padding( padding: const EdgeInsets.symmetric(horizontal: 8), child: Text( - "₹ ${widget.product.price.toString()}", + "₹ ${widget.productModel!['price'].toString()}", style: GoogleFonts.roboto( fontSize: 24, fontWeight: FontWeight.w800, @@ -125,7 +131,7 @@ class _ProductDetailScreenState extends State { Padding( padding: const EdgeInsets.symmetric(horizontal: 8), child: Text( - widget.product.category.name, + widget.productModel!['category']['categoryName'], style: GoogleFonts.roboto( fontSize: 16, fontWeight: FontWeight.w400, @@ -137,7 +143,7 @@ class _ProductDetailScreenState extends State { Padding( padding: const EdgeInsets.symmetric(horizontal: 8), child: Text( - widget.product.description, + widget.productModel!['description'], style: GoogleFonts.roboto( fontSize: 16, fontWeight: FontWeight.w400, @@ -154,7 +160,12 @@ class _ProductDetailScreenState extends State { width: Get.width * 0.9, height: Get.height * 0.06, child: ElevatedButton( - onPressed: () => Get.to(() => const CartScreen()), + onPressed: () { + // Pass the product data to the CartScreen + Get.to(() => CartScreen( + // Pass the product in a list + )); + }, style: ElevatedButton.styleFrom( foregroundColor: Colors.white, backgroundColor: const Color(0xFF00784C), diff --git a/lib/widgets/product_card.dart b/lib/widgets/product_card.dart index bac0cfb..aea5019 100644 --- a/lib/widgets/product_card.dart +++ b/lib/widgets/product_card.dart @@ -5,12 +5,15 @@ import 'package:get/get.dart'; import 'package:google_fonts/google_fonts.dart'; class ProductCard extends StatelessWidget { - final ProductModel product; + final Map? productModel; + ProductModel? product; final bool isInCart; final bool isCheckout; - const ProductCard({ + + ProductCard({ super.key, - required this.product, + this.product, + this.productModel, this.isInCart = false, this.isCheckout = false, }); @@ -20,7 +23,8 @@ class ProductCard extends StatelessWidget { return GestureDetector( onTap: () => isInCart || isCheckout ? null - : Get.to(() => ProductDetailScreen(product: product)), + : Get.to(() => + ProductDetailScreen(productModel: productModel)), child: Card( child: Row( children: [ @@ -33,7 +37,8 @@ class ProductCard extends StatelessWidget { width: Get.width * 0.30, decoration: BoxDecoration( image: DecorationImage( - image: Image.asset(product.image).image, + image: AssetImage("assets/images/product.png"), + // Image.asset(productModel!['image']).image, fit: BoxFit.cover, ), ), @@ -47,21 +52,21 @@ class ProductCard extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - product.name, + productModel!['name'], style: GoogleFonts.roboto( fontSize: 16, fontWeight: FontWeight.w500, ), ), Text( - product.category.name, + productModel!['category']['categoryName'], style: GoogleFonts.roboto( fontSize: 14, fontWeight: FontWeight.w400, ), ), Text( - "₹ ${product.price.toString()}0", + "₹ ${ productModel!['price'].toString()}0", style: GoogleFonts.roboto( fontSize: 22, fontWeight: FontWeight.w700, @@ -70,100 +75,102 @@ class ProductCard extends StatelessWidget { isCheckout ? const SizedBox() : isInCart - ? Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Container( - height: 40, - width: 100, - decoration: BoxDecoration( - color: const Color(0xFF004791), - borderRadius: BorderRadius.circular(10), - ), - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceAround, - children: [ - SizedBox( - height: 24, - width: 24, - child: ElevatedButton( - onPressed: () {}, - style: ElevatedButton.styleFrom( - padding: EdgeInsets.zero, - shape: RoundedRectangleBorder( - borderRadius: - BorderRadius.circular(8.0), - ), - ), - child: Text( - '+', - style: GoogleFonts.roboto( - fontSize: 16, - fontWeight: FontWeight.w800, - ), - ), - ), - ), - Text( - product.quantity.toString(), - style: const TextStyle( - color: Colors.white, - fontSize: 16, - ), - ), - SizedBox( - height: 24, - width: 24, - child: ElevatedButton( - onPressed: () {}, - style: ElevatedButton.styleFrom( - padding: EdgeInsets.zero, - shape: RoundedRectangleBorder( - borderRadius: - BorderRadius.circular(8.0), - ), - ), - child: Text( - '-', - style: GoogleFonts.roboto( - fontSize: 16, - fontWeight: FontWeight.w800, - ), - ), - ), - ), - ], + ? Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Container( + height: 40, + width: 100, + decoration: BoxDecoration( + color: const Color(0xFF004791), + borderRadius: BorderRadius.circular(10), + ), + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceAround, + children: [ + SizedBox( + height: 24, + width: 24, + child: ElevatedButton( + onPressed: () {}, + style: ElevatedButton.styleFrom( + padding: EdgeInsets.zero, + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular(8.0), ), ), - IconButton( - onPressed: () {}, - icon: const Icon( - Icons.delete_outline_rounded, - color: Colors.red, + child: Text( + '+', + style: GoogleFonts.roboto( + fontSize: 16, + fontWeight: FontWeight.w800, ), - ) - ], - ) - : ElevatedButton( - onPressed: () {}, - style: ElevatedButton.styleFrom( - foregroundColor: Colors.white, - backgroundColor: const Color(0xFF00784C), - padding: const EdgeInsets.symmetric( - horizontal: 18, vertical: 8), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(30), - ), - ), - child: Text( - "Add To Cart", - style: GoogleFonts.roboto( - fontSize: 14, - fontWeight: FontWeight.w600, ), ), ), + Text( + productModel!['brand']['brandName'], + style: const TextStyle( + color: Colors.white, + fontSize: 16, + ), + ), + SizedBox( + height: 24, + width: 24, + child: ElevatedButton( + onPressed: () {}, + style: ElevatedButton.styleFrom( + padding: EdgeInsets.zero, + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular(8.0), + ), + ), + child: Text( + '-', + style: GoogleFonts.roboto( + fontSize: 16, + fontWeight: FontWeight.w800, + ), + ), + ), + ), + ], + ), + ), + IconButton( + onPressed: () {}, + icon: const Icon( + Icons.delete_outline_rounded, + color: Colors.red, + ), + ) + ], + ) + : ElevatedButton( + onPressed: () { + + }, + style: ElevatedButton.styleFrom( + foregroundColor: Colors.white, + backgroundColor: const Color(0xFF00784C), + padding: const EdgeInsets.symmetric( + horizontal: 18, vertical: 8), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(30), + ), + ), + child: Text( + "Add To Cart", + style: GoogleFonts.roboto( + fontSize: 14, + fontWeight: FontWeight.w600, + ), + ), + ), ], ) ],