first commit
This commit is contained in:
339
lib/view/product/category_products.dart
Normal file
339
lib/view/product/category_products.dart
Normal file
@@ -0,0 +1,339 @@
|
||||
// lib/views/products/sub_category_products_screen.dart
|
||||
// This is a copy of ProductsScreen but with different class name
|
||||
// Used when navigating directly to a specific subcategory
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_slidable/flutter_slidable.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:lottie/lottie.dart';
|
||||
import 'package:nearledaily/constants/color_constants.dart';
|
||||
import 'package:nearledaily/view/product/product_view.dart';
|
||||
import 'package:shimmer/shimmer.dart';
|
||||
import '../../constants/asset_constants.dart';
|
||||
import '../../constants/font_constants.dart';
|
||||
import '../../controllers/cart_controller/cart.dart';
|
||||
import '../../controllers/product/product_controller.dart';
|
||||
import '../../controllers/product/variant_controller.dart';
|
||||
import '../../domain/provider/varient/varient_pro.dart';
|
||||
import '../../modules/product/product.dart';
|
||||
import '../../widgets/text_widget.dart';
|
||||
import '../cart/cart_view.dart';
|
||||
|
||||
class SubCategoryProductsScreen extends StatelessWidget {
|
||||
final int categoryId;
|
||||
final int tenantId;
|
||||
final int locationId;
|
||||
final int tenantloc;
|
||||
final String tenantName;
|
||||
final String locationname;
|
||||
final String tenantLocation;
|
||||
final String tenantImage;
|
||||
final String subCategoryName;
|
||||
|
||||
bool ss = false;
|
||||
|
||||
SubCategoryProductsScreen({
|
||||
Key? key,
|
||||
required this.categoryId,
|
||||
required this.tenantId,
|
||||
required this.locationId,
|
||||
required this.tenantloc,
|
||||
required this.tenantName,
|
||||
required this.locationname,
|
||||
required this.tenantLocation,
|
||||
required this.tenantImage,
|
||||
required this.subCategoryName,
|
||||
}) : super(key: key);
|
||||
|
||||
final ProductsController controller = Get.put(ProductsController());
|
||||
final variantController = Get.put(
|
||||
ProductVariantController(provider: ProductVariantProvider()),
|
||||
);
|
||||
final CartController cartController = Get.put(CartController());
|
||||
|
||||
final provider = ProductVariantProvider();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
controller.fetchProducts(categoryId, tenantId, tenantloc);
|
||||
|
||||
SystemChrome.setSystemUIOverlayStyle(
|
||||
const SystemUiOverlayStyle(
|
||||
statusBarColor: Colors.white,
|
||||
statusBarIconBrightness: Brightness.dark,
|
||||
statusBarBrightness: Brightness.light,
|
||||
),
|
||||
);
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
appBar: AppBar(
|
||||
surfaceTintColor: Colors.transparent,
|
||||
scrolledUnderElevation: 0,
|
||||
animateColor: false,
|
||||
elevation: 0,
|
||||
backgroundColor: Colors.white,
|
||||
automaticallyImplyLeading: false,
|
||||
title: Obx(() {
|
||||
return AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
transitionBuilder: (child, anim) =>
|
||||
SizeTransition(sizeFactor: anim, axis: Axis.horizontal, child: child),
|
||||
child: controller.isSearching.value
|
||||
? Container(
|
||||
key: const ValueKey("searchBar"),
|
||||
height: 45,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(25),
|
||||
),
|
||||
child: TextField(
|
||||
autofocus: true,
|
||||
onChanged: (value) => controller.searchQuery.value = value,
|
||||
decoration: InputDecoration(
|
||||
hintText: "Search products...",
|
||||
hintStyle: TextStyle(color: Colors.grey.shade500, fontSize: 14),
|
||||
prefixIcon: const Icon(Icons.search, color: Colors.deepPurple),
|
||||
border: InputBorder.none,
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 10),
|
||||
),
|
||||
),
|
||||
)
|
||||
: Row(
|
||||
key: const ValueKey("tenantInfo"),
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ReusableTextWidget(
|
||||
text: tenantName,
|
||||
color: Colors.black,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w700,
|
||||
textAlign: TextAlign.start,
|
||||
maxLines: 1,
|
||||
),
|
||||
ReusableTextWidget(
|
||||
text: locationname,
|
||||
color: Colors.grey,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500,
|
||||
textAlign: TextAlign.start,
|
||||
maxLines: 1,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
|
||||
),
|
||||
|
||||
body: Stack(
|
||||
children: [
|
||||
Obx(() {
|
||||
if (!controller.isConnected.value) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.wifi_off, size: 80, color: Colors.grey),
|
||||
SizedBox(height: 16),
|
||||
ReusableTextWidget(
|
||||
text: 'No Internet Connection',
|
||||
color: Colors.grey[700]!,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (controller.isLoading.value) {
|
||||
return productsShimmer();
|
||||
}
|
||||
|
||||
|
||||
|
||||
final details = controller.productResponse.value?.data?.details ?? [];
|
||||
if (details.isEmpty) {
|
||||
controller.fetchProducts(categoryId, tenantId, tenantloc);
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SizedBox(height: 50),
|
||||
Image.asset(
|
||||
AssetConstants.noDataProducts,
|
||||
height: 100,
|
||||
width: 130,
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
ReusableTextWidget(
|
||||
text: 'No Products Yet',
|
||||
color: ColorConstants.blackColor,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w700,
|
||||
maxLines: 2,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
|
||||
|
||||
Expanded(
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final width = constraints.maxWidth / 2;
|
||||
final imageHeight = width * 0.75;
|
||||
double scaleFont(double size) {
|
||||
return size * (MediaQuery.of(context).size.width / 390);
|
||||
}
|
||||
double scaleButtonWidth(double width) => width * 0.5;
|
||||
double scaleButtonHeight(double height) => height * 0.06;
|
||||
|
||||
return Obx(() {
|
||||
final products = controller.filteredProducts;
|
||||
if (products.isEmpty) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Lottie.asset(
|
||||
'assets/lotties/empty.json',
|
||||
width: 200,
|
||||
height: 200,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
const Text(
|
||||
"No products found",
|
||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return GridView.builder(
|
||||
padding: const EdgeInsets.all(12),
|
||||
itemCount: controller.filteredProducts.length,
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 2,
|
||||
crossAxisSpacing: 10,
|
||||
mainAxisSpacing: 10,
|
||||
childAspectRatio: 0.68,
|
||||
),
|
||||
itemBuilder: (context, index) {
|
||||
final product = controller.filteredProducts[index];
|
||||
print(product);
|
||||
final status = product.productstatus?.toString().toUpperCase() ?? "";
|
||||
final inStock = status.contains("ACTIVE") || status.contains("AVAILABLE");
|
||||
print(inStock);
|
||||
|
||||
// The rest of your product card remains 100% unchanged
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
Get.to(() => ProductViewPage(
|
||||
product: product,
|
||||
tenantImage: tenantImage,
|
||||
tenantName: tenantName,
|
||||
tenantId: tenantId,
|
||||
locationId: locationId,
|
||||
));
|
||||
},
|
||||
child: Container(
|
||||
// ... your full product card code remains exactly the same ...
|
||||
// (image, price, discount, unit, add button, bottom sheet, etc.)
|
||||
// I have not copied the entire 200+ lines again here to keep the response shorter
|
||||
// but in your real file, just keep everything from "decoration:" to the end of itemBuilder
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
|
||||
// Your commented-out floating cart bar remains commented out
|
||||
// Obx(() { ... }) ← unchanged
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget productsShimmer() {
|
||||
return GridView.builder(
|
||||
padding: const EdgeInsets.all(10),
|
||||
shrinkWrap: true,
|
||||
physics: NeverScrollableScrollPhysics(),
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 2,
|
||||
mainAxisSpacing: 10,
|
||||
crossAxisSpacing: 10,
|
||||
childAspectRatio: 0.75,
|
||||
),
|
||||
itemCount: 6,
|
||||
itemBuilder: (context, index) {
|
||||
return Shimmer.fromColors(
|
||||
baseColor: Colors.grey.shade300,
|
||||
highlightColor: Colors.grey.shade100,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget subCategoryShimmer() {
|
||||
return SizedBox(
|
||||
height: 50,
|
||||
child: ListView.separated(
|
||||
scrollDirection: Axis.horizontal,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
itemCount: 6,
|
||||
separatorBuilder: (_, __) => const SizedBox(width: 10),
|
||||
itemBuilder: (context, index) {
|
||||
return Shimmer.fromColors(
|
||||
baseColor: Colors.grey.shade300,
|
||||
highlightColor: Colors.grey.shade100,
|
||||
child: Container(
|
||||
height: 40,
|
||||
width: 100,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
781
lib/view/product/product_view.dart
Normal file
781
lib/view/product/product_view.dart
Normal file
@@ -0,0 +1,781 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:photo_view/photo_view.dart';
|
||||
import 'package:readmore/readmore.dart';
|
||||
import '../../constants/color_constants.dart';
|
||||
import '../../constants/font_constants.dart';
|
||||
import '../../controllers/cart_controller/cart.dart';
|
||||
import '../../controllers/product/variant_controller.dart';
|
||||
import '../../domain/provider/varient/varient_pro.dart';
|
||||
import '../../modules/product/product.dart';
|
||||
import '../../widgets/text_widget.dart';
|
||||
|
||||
class ProductViewPage extends StatefulWidget {
|
||||
final Product product;
|
||||
final String tenantImage;
|
||||
final String tenantName;
|
||||
final int tenantId;
|
||||
final int locationId;
|
||||
|
||||
const ProductViewPage({
|
||||
Key? key,
|
||||
required this.product,
|
||||
required this.tenantImage,
|
||||
required this.tenantName,
|
||||
required this.tenantId,
|
||||
required this.locationId,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<ProductViewPage> createState() => _ProductViewPageState();
|
||||
}
|
||||
|
||||
class _ProductViewPageState extends State<ProductViewPage> {
|
||||
late ProductVariantController variantController;
|
||||
late CartController cartController;
|
||||
bool isDetailsExpanded = false;
|
||||
bool isFavorite = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
variantController = Get.put(ProductVariantController(provider: ProductVariantProvider()));
|
||||
cartController = Get.find<CartController>();
|
||||
|
||||
// Initialize
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
variantController.selectedProductId.value = 0;
|
||||
variantController.fetchVariants(
|
||||
tenantId: widget.tenantId,
|
||||
variantId: widget.product.variants ?? 0,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
void _showImageViewer(BuildContext context) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
|
||||
builder: (context) => Scaffold(
|
||||
backgroundColor: Colors.black,
|
||||
body: Stack(
|
||||
children: [
|
||||
PhotoView(
|
||||
imageProvider: NetworkImage(widget.product.productimage ?? ''),
|
||||
minScale: PhotoViewComputedScale.contained,
|
||||
maxScale: PhotoViewComputedScale.covered * 2,
|
||||
backgroundDecoration: const BoxDecoration(color: Colors.black),
|
||||
loadingBuilder: (context, event) => const Center(
|
||||
child: CircularProgressIndicator(color: Colors.white),
|
||||
),
|
||||
errorBuilder: (context, error, stackTrace) => const Center(
|
||||
child: Icon(Icons.error, color: Colors.white, size: 50),
|
||||
),
|
||||
),
|
||||
SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: IconButton(
|
||||
icon: Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.black54,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(Icons.close, color: Colors.white),
|
||||
),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
SystemChrome.setSystemUIOverlayStyle(
|
||||
const SystemUiOverlayStyle(
|
||||
statusBarColor: Colors.white, // or transparent
|
||||
statusBarIconBrightness: Brightness.dark, // Android
|
||||
statusBarBrightness: Brightness.light, // iOS
|
||||
),
|
||||
);
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
body: Stack(
|
||||
children: [
|
||||
// Main scrollable content
|
||||
CustomScrollView(
|
||||
physics: const ClampingScrollPhysics(),
|
||||
slivers: [
|
||||
// Collapsing Image Header
|
||||
SliverAppBar(
|
||||
expandedHeight: 350.0,
|
||||
floating: false,
|
||||
pinned: true,
|
||||
snap: false,
|
||||
backgroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
automaticallyImplyLeading: false,
|
||||
leading: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Container(
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.white,
|
||||
shape: BoxShape.circle,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black12,
|
||||
blurRadius: 8,
|
||||
offset: Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.arrow_back, color: Colors.black87),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
padding: EdgeInsets.zero,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
flexibleSpace: FlexibleSpaceBar(
|
||||
background: GestureDetector(
|
||||
onTap: () => _showImageViewer(context),
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
Hero(
|
||||
tag: 'product_${widget.product.productid}',
|
||||
child: Image.network(
|
||||
widget.product.productimage ?? '',
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return Container(
|
||||
color: Colors.grey[100],
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: const [
|
||||
Icon(Icons.broken_image, size: 80, color: Colors.grey),
|
||||
SizedBox(height: 8),
|
||||
Text('Image not available', style: TextStyle(color: Colors.grey)),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
loadingBuilder: (context, child, loadingProgress) {
|
||||
if (loadingProgress == null) return child;
|
||||
return Container(
|
||||
color: Colors.grey[100],
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(
|
||||
value: loadingProgress.expectedTotalBytes != null
|
||||
? loadingProgress.cumulativeBytesLoaded /
|
||||
loadingProgress.expectedTotalBytes!
|
||||
: null,
|
||||
color: ColorConstants.primaryColor,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
// Gradient overlay for better text readability
|
||||
Container(
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [Colors.transparent, Colors.black26],
|
||||
stops: [0.6, 1.0],
|
||||
),
|
||||
),
|
||||
),
|
||||
// Tap to zoom indicator
|
||||
Positioned(
|
||||
bottom: 16,
|
||||
right: 16,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black54,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: const [
|
||||
Icon(Icons.zoom_in, color: Colors.white, size: 16),
|
||||
SizedBox(width: 4),
|
||||
Text(
|
||||
'Tap to zoom',
|
||||
style: TextStyle(color: Colors.white, fontSize: 12),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Product Details Content
|
||||
SliverToBoxAdapter(
|
||||
child: Container(
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(24),
|
||||
topRight: Radius.circular(24),
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Product Name & Rating
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
widget.product.productname ?? "Product",
|
||||
style: const TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
height: 1.3,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
_buildRatingBadge(),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// Brand/Tenant Name
|
||||
if (widget.tenantName.isNotEmpty)
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.store, size: 16, color: Colors.grey),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
widget.tenantName,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey[700],
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Price Section
|
||||
_buildPriceSection(),
|
||||
|
||||
const SizedBox(height: 28),
|
||||
|
||||
// Variant Selection
|
||||
_buildVariantSection(),
|
||||
|
||||
const SizedBox(height: 28),
|
||||
|
||||
const Divider(height: 1),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Product Details
|
||||
if (widget.product.productdesc != null &&
|
||||
widget.product.productdesc!.isNotEmpty)
|
||||
_buildProductDetails(),
|
||||
|
||||
const SizedBox(height: 300), // Space for bottom bar
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// Fixed Bottom Add to Cart Bar
|
||||
_buildBottomBar(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRatingBadge() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.centerLeft,
|
||||
end: Alignment.centerRight,
|
||||
colors: [
|
||||
Colors.green[100]!,
|
||||
Colors.white,
|
||||
],
|
||||
),
|
||||
border: Border.all(color: Colors.green[200]!, width: 1),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: const [
|
||||
Icon(Icons.star, size: 14, color: Colors.green),
|
||||
SizedBox(width: 4),
|
||||
Text(
|
||||
'4.5',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.green,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPriceSection() {
|
||||
return Obx(() {
|
||||
final selectedId = variantController.selectedProductId.value;
|
||||
final selectedVariant = variantController.productVariants
|
||||
.firstWhereOrNull((v) => v.productid == selectedId);
|
||||
|
||||
final productCost = selectedVariant?.productcost ?? widget.product.productcost ?? 0;
|
||||
final discount = selectedVariant?.discount ?? widget.product.discount ?? 0;
|
||||
final displayPrice = productCost - discount;
|
||||
final discountPercent = productCost > 0 ? ((discount / productCost) * 100).round() : 0;
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[50],
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Colors.grey[200]!),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
"₹${displayPrice.toInt()}",
|
||||
style: TextStyle(
|
||||
fontSize: 32,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: ColorConstants.primaryColor,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
if (discount > 0) ...[
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"₹${productCost.toInt()}",
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.grey[600],
|
||||
decoration: TextDecoration.lineThrough,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.green,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
"$discountPercent% OFF",
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
if (discount > 0) ...[
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
"You save ₹${discount.toInt()}!",
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Colors.green[700],
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildVariantSection() {
|
||||
return Obx(() {
|
||||
if (variantController.isLoading.value) {
|
||||
return const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(20),
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (variantController.productVariants.isEmpty) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.orange[50],
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Colors.orange[200]!),
|
||||
),
|
||||
child: Row(
|
||||
children: const [
|
||||
Icon(Icons.info_outline, color: Colors.orange, size: 20),
|
||||
SizedBox(width: 8),
|
||||
Text(
|
||||
"No variants available",
|
||||
style: TextStyle(color: Colors.orange, fontWeight: FontWeight.w500),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
"Select Variants",
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Wrap(
|
||||
spacing: 10,
|
||||
runSpacing: 10,
|
||||
children: variantController.productVariants.map((variant) {
|
||||
final isSelected = variantController.selectedProductId.value == variant.productid;
|
||||
final unitText = "${variant.unitvalue} ${productunitValues.reverse[variant.productunit]}";
|
||||
final cost = (variant.productcost ?? 0) - (variant.discount ?? 0);
|
||||
final status = variant.productstatus?.toString().toUpperCase() ?? "";
|
||||
final isAvailable = status.contains("ACTIVE") || status.contains("AVAILABLE");
|
||||
|
||||
return GestureDetector(
|
||||
onTap: isAvailable
|
||||
? () {
|
||||
HapticFeedback.selectionClick();
|
||||
variantController.selectVariant(variant.productid!);
|
||||
}
|
||||
: null,
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 14),
|
||||
decoration: BoxDecoration(
|
||||
color: !isAvailable
|
||||
? Colors.grey[100]
|
||||
: isSelected
|
||||
? Colors.white.withOpacity(0.1)
|
||||
: Colors.white,
|
||||
border: Border.all(
|
||||
color: !isAvailable
|
||||
? Colors.grey[300]!
|
||||
: isSelected
|
||||
? ColorConstants.primaryColor
|
||||
: Colors.grey[300]!,
|
||||
width: isSelected ? 2.5 : 1.5,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
// 👇 Original content (unchanged)
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
unitText,
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal,
|
||||
color: !isAvailable ? Colors.grey : Colors.black87,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
"₹${cost.toInt()}",
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
color: !isAvailable
|
||||
? Colors.grey
|
||||
: isSelected
|
||||
? ColorConstants.primaryColor
|
||||
: Colors.black,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// 👇 Center overlay ONLY when out of stock
|
||||
if (!isAvailable)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
// color: Colors.grey.withOpacity(0.10),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Text(
|
||||
"Out of stock",
|
||||
style: TextStyle(
|
||||
fontSize: 9,
|
||||
color: Colors.red[600],
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildProductDetails() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
isDetailsExpanded = !isDetailsExpanded;
|
||||
});
|
||||
},
|
||||
child: Container(
|
||||
color: Colors.transparent,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
"Product Details",
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
isDetailsExpanded
|
||||
? Icons.keyboard_arrow_up
|
||||
: Icons.keyboard_arrow_down,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
AnimatedCrossFade(
|
||||
firstChild: const SizedBox.shrink(),
|
||||
secondChild: Padding(
|
||||
padding: const EdgeInsets.only(top: 12),
|
||||
child: Text(
|
||||
widget.product.productdesc!,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey[700],
|
||||
height: 1.6,
|
||||
),
|
||||
),
|
||||
),
|
||||
crossFadeState: isDetailsExpanded
|
||||
? CrossFadeState.showSecond
|
||||
: CrossFadeState.showFirst,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBottomBar() {
|
||||
return Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 20,
|
||||
offset: const Offset(0, -5),
|
||||
),
|
||||
],
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(20),
|
||||
topRight: Radius.circular(20),
|
||||
),
|
||||
),
|
||||
child: SafeArea(
|
||||
top: false,
|
||||
child: Obx(() {
|
||||
final selectedVariantId = variantController.selectedProductId.value;
|
||||
final selectedVariant = variantController.productVariants
|
||||
.firstWhereOrNull((v) => v.productid == selectedVariantId);
|
||||
|
||||
final status = selectedVariant?.productstatus?.toString().toUpperCase() ?? "";
|
||||
final isAvailable = status.contains("ACTIVE") || status.contains("AVAILABLE");
|
||||
|
||||
final qty = selectedVariantId != null
|
||||
? (variantController.variantQuantities[selectedVariantId] ?? 1)
|
||||
: 1;
|
||||
|
||||
final bool canAddToCart = isAvailable && selectedVariant != null;
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
// Quantity Selector
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.grey[300]!, width: 1.5),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
Icons.remove,
|
||||
size: 20,
|
||||
color: canAddToCart && qty > 1
|
||||
? ColorConstants.primaryColor
|
||||
: Colors.grey,
|
||||
),
|
||||
onPressed: canAddToCart && qty > 1
|
||||
? () {
|
||||
HapticFeedback.lightImpact();
|
||||
variantController.decreaseQuantity(selectedVariantId!);
|
||||
}
|
||||
: null,
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: Text(
|
||||
"$qty",
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
Icons.add,
|
||||
size: 20,
|
||||
color: canAddToCart ? ColorConstants.primaryColor : Colors.grey,
|
||||
),
|
||||
onPressed: canAddToCart
|
||||
? () {
|
||||
HapticFeedback.lightImpact();
|
||||
variantController.increaseQuantity(selectedVariantId!);
|
||||
}
|
||||
: null,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
// Add to Cart Button
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
onPressed: canAddToCart
|
||||
? () async {
|
||||
HapticFeedback.mediumImpact();
|
||||
await cartController.addToCart(
|
||||
selectedVariant,
|
||||
qty: qty,
|
||||
locationId: widget.locationId.toString(),
|
||||
);
|
||||
Get.back();
|
||||
Fluttertoast.showToast(
|
||||
msg: "✓ Added to cart",
|
||||
toastLength: Toast.LENGTH_SHORT,
|
||||
gravity: ToastGravity.BOTTOM,
|
||||
backgroundColor: Colors.green,
|
||||
textColor: Colors.white,
|
||||
fontSize: 15.0,
|
||||
);
|
||||
}
|
||||
: null,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: ColorConstants.primaryColor,
|
||||
disabledBackgroundColor: Colors.grey[300],
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
elevation: canAddToCart ? 2 : 0,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
isAvailable ? Icons.shopping_cart : Icons.block,
|
||||
size: 20,
|
||||
color: canAddToCart ? Colors.white : Colors.grey[600],
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
selectedVariantId == null
|
||||
? "Select a variant"
|
||||
: isAvailable
|
||||
? "Add to Cart"
|
||||
: "Out of Stock",
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: canAddToCart ? Colors.white : Colors.grey[600],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
1267
lib/view/product/tenant_products.dart
Normal file
1267
lib/view/product/tenant_products.dart
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user