QueenFruits/Mobile Commerce/lib/features/cart/presentation/screens/cart_screen.dart

935 lines
33 KiB
Dart

import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:niogu_ecommerce_v1/core/constant/app_font_size.dart';
import 'package:niogu_ecommerce_v1/core/enums/stock_type.dart';
import 'package:niogu_ecommerce_v1/core/errors/exceptions.dart';
import 'package:niogu_ecommerce_v1/core/providers/app_provider.dart';
import 'package:niogu_ecommerce_v1/core/router/app_route.dart';
import 'package:niogu_ecommerce_v1/core/system/system_setting.dart';
import 'package:niogu_ecommerce_v1/core/utils/currency_format.dart';
import 'package:niogu_ecommerce_v1/core/utils/extenstion_format.dart';
import 'package:niogu_ecommerce_v1/core/utils/log_message.dart';
import 'package:niogu_ecommerce_v1/core/widgets/custom_empty_screen.dart';
import 'package:niogu_ecommerce_v1/core/widgets/custom_snackbar.dart';
import 'package:niogu_ecommerce_v1/features/account/presentation/widgets/login_auth_modal.dart';
import 'package:niogu_ecommerce_v1/features/cart/domain/entities/cart.dart';
import 'package:niogu_ecommerce_v1/features/cart/presentation/providers/cart_provider.dart';
import 'package:niogu_ecommerce_v1/features/favorite/domain/entities/favorite.dart';
import 'package:niogu_ecommerce_v1/features/favorite/presentation/providers/favorite_provider.dart';
import 'package:niogu_ecommerce_v1/features/home/domain/entities/home.dart';
import 'package:niogu_ecommerce_v1/features/home/presentation/providers/home_provider.dart';
import 'package:niogu_ecommerce_v1/features/product/presentation/providers/product_provider.dart';
import 'package:shimmer/shimmer.dart';
import 'package:sizer/sizer.dart';
import 'package:niogu_ecommerce_v1/core/constant/app_color.dart';
class CartScreen extends ConsumerStatefulWidget {
const CartScreen({super.key});
@override
ConsumerState<CartScreen> createState() => _CartScreenState();
}
class _CartScreenState extends ConsumerState<CartScreen> {
@override
void initState() {
// TODO: implement initState
super.initState();
}
@override
void dispose() {
// TODO: implement dispose
super.dispose();
}
double _calculateTotalOrder(List<CartItem> cartItems) {
return cartItems.fold(0.0, (sum, cartItem) {
return sum + (cartItem.sellingPrice * cartItem.quantity);
});
}
Future<void> _processCheckout() async {
final isLoggedIn = ref.read(currentStatusLoginProvider);
if (!isLoggedIn) {
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
constraints: BoxConstraints(maxWidth: double.infinity),
builder: (context) => const LoginAuthModal(),
);
return;
}
final cartItems = ref.read(cartItemProvider);
await SystemSetting.saveCartItemByOutlet(cartItems);
context.pushNamed(AppRoute.checkoutScreen);
}
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
final productState = ref.watch(productBestSellerControllerProvider);
final currentOutletName = ref.watch(currentOutletNameProvider);
final cartItemState = ref.watch(cartItemProvider);
final currentOutletId = ref.watch(currentOutletIdProvider);
final favoriteState = ref.watch(favoriteControllerProvider);
return SafeArea(
top: false,
bottom: true,
right: false,
left: false,
child: Scaffold(
backgroundColor: Colors.white,
resizeToAvoidBottomInset: false,
appBar: AppBar(
backgroundColor: Colors.white,
elevation: 0,
centerTitle: true,
leading: IconButton(
icon: Icon(
Icons.arrow_back,
size: 7.w,
color: AppColor.primaryColor,
),
onPressed: () => context.goNamed(AppRoute.homeScreen),
),
title: Text(
"Keranjang Belanja",
style: TextStyle(
color: Colors.black,
fontSize: AppFontSize.medium.sp,
fontWeight: FontWeight.bold,
),
),
),
body: productState.when(
data: (products) {
if (cartItemState.isEmpty) {
return _buildEmptyCart();
}
return RefreshIndicator(
onRefresh: () async {
await ref
.read(productBestSellerControllerProvider.notifier)
.refresh();
},
color: AppColor.primaryColor,
backgroundColor: Colors.white,
child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
child: Column(
children: [
_buildOutletInfoSection(currentOutletName!),
SizedBox(height: 2.h),
_buildProductListSection(cartItemState),
SizedBox(height: 3.h),
_buildAddOtherProductSection(
products: products,
currentOutletId: currentOutletId!,
favorites: favoriteState,
),
SizedBox(height: 5.h),
],
),
),
);
},
error: (error, stackTrace) => CustomEmptyScreen(
icon: Icons.cloud_off_outlined,
title: "Terjadi Kesalahan Koneksi",
subtitle: "Tarik ke bawah untuk mencoba lagi",
height: 40.h,
),
loading: () => _buildCartLoading(),
),
bottomNavigationBar: cartItemState.isEmpty
? null
: _buildBottomCartAction(cartItemState),
),
);
},
);
}
Widget _buildOutletInfoSection(String outletName) {
return Container(
width: double.infinity,
margin: EdgeInsets.all(4.w),
padding: EdgeInsets.all(4.w),
decoration: BoxDecoration(
color: const Color(0xFFFFF9E5),
borderRadius: BorderRadius.circular(2.5.w),
border: Border.all(color: const Color(0xFFFFEBB0)),
),
child: Row(
children: [
Container(
padding: EdgeInsets.all(2.5.w),
decoration: const BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
),
child: Icon(
Icons.storefront,
color: AppColor.primaryColor,
size: 5.w,
),
),
SizedBox(width: 4.w),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Pengiriman dari :",
style: TextStyle(
fontSize: (AppFontSize.small - 1.25).sp,
color: Colors.grey.shade700,
),
),
Text(
outletName,
style: TextStyle(
fontSize: (AppFontSize.small - 1.25).sp,
fontWeight: FontWeight.bold,
),
),
],
),
),
/**
Text(
"Ubah",
style: TextStyle(
color: AppColor.primaryColor,
fontWeight: FontWeight.bold,
fontSize: (AppFontSize.small - 1.25).sp,
),
),
*/
],
),
);
}
Widget _buildProductListSection(List<CartItem> cartItems) {
return Container(
margin: EdgeInsets.symmetric(horizontal: 4.w),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(2.5.w),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 10,
offset: const Offset(0, 5),
),
],
),
child: Column(
children: List.generate(cartItems.length, (index) {
final cartItem = cartItems[index];
return Column(
children: [
Padding(
padding: EdgeInsets.all(4.w),
child: Row(
children: [
CachedNetworkImage(
imageUrl: cartItem.image ?? 'error',
imageBuilder: (context, imageProvider) {
return Container(
width: 18.w,
height: 18.w,
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(2.w),
image: DecorationImage(
image: imageProvider,
fit: BoxFit.cover,
),
),
);
},
placeholder: (context, url) {
return Shimmer.fromColors(
baseColor: Colors.grey.shade300,
highlightColor: Colors.grey.shade100,
child: Container(
height: 18.w,
width: 18.w,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(2.w),
),
),
);
},
errorWidget: (context, url, error) {
return Container(
width: 18.w,
height: 18.w,
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(2.w),
),
child: Icon(Icons.image, color: Colors.grey.shade300),
);
},
),
SizedBox(width: 3.w),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
cartItem.name,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: (AppFontSize.small - 1.25).sp,
),
),
if (cartItem.isProductVariant) ...[
SizedBox(height: 0.5.h),
Text(
cartItem.variantName,
style: TextStyle(
color: Colors.grey.shade700,
fontSize: (AppFontSize.small - 1.25).sp,
),
),
],
if (cartItem.stockType == StockType.fixed.type) ...[
SizedBox(height: 0.5.h),
Text(
"Stok: ${cartItem.stock.toStringWithoutTrailingZero()} ${cartItem.unit}",
style: TextStyle(
color: Colors.grey.shade700,
fontSize: (AppFontSize.small - 1.25).sp,
),
),
],
SizedBox(height: 1.h),
Text(
CurrencyFormat.formatToIdr(
cartItem.sellingPrice,
0,
),
style: TextStyle(
fontWeight: FontWeight.bold,
color: AppColor.primaryColor,
fontSize: (AppFontSize.small - 1.25).sp,
),
),
],
),
),
Row(
children: [
_buildCounterBtn(
Icons.remove,
isMinus: true,
onTap: () async {
ref
.read(cartItemControllerProvider.notifier)
.decrement(cartItem.outletId, cartItem.id);
final cartItems = ref.read(cartItemProvider);
await SystemSetting.saveCartItemByOutlet(cartItems);
},
),
Padding(
padding: EdgeInsets.symmetric(horizontal: 3.5.w),
child: Text(
cartItem.quantity.toString(),
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: AppFontSize.small.sp,
),
),
),
_buildCounterBtn(
Icons.add,
onTap: () async {
if (cartItem.stockType == StockType.fixed.type &&
cartItem.quantity >= cartItem.stock) {
CustomSnackbar.showError(
context,
"Kuantitas mencapai batas maksimum",
);
return;
}
ref
.read(cartItemControllerProvider.notifier)
.increment(cartItem);
final cartItems = ref.read(cartItemProvider);
await SystemSetting.saveCartItemByOutlet(cartItems);
},
),
],
),
],
),
),
if (index != 2)
Divider(
height: 1,
thickness: 1,
color: Colors.grey.shade100,
indent: 4.w,
endIndent: 4.w,
),
],
);
}),
),
);
}
Widget _buildCounterBtn(
IconData icon, {
bool isMinus = false,
required VoidCallback onTap,
}) {
return GestureDetector(
onTap: onTap,
child: Container(
padding: EdgeInsets.all(1.w),
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(color: Colors.grey.shade300),
color: isMinus ? Colors.white : AppColor.primaryColor,
),
child: Icon(
icon,
size: 4.w,
color: isMinus ? Colors.grey : Colors.white,
),
),
);
}
Widget _buildAddOtherProductSection({
required List<ProductItem> products,
required String currentOutletId,
required Map<String, SelectedFavorite> favorites,
}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: EdgeInsets.symmetric(horizontal: 4.w),
child: Text(
"Produk Terlaris",
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: AppFontSize.small.sp,
),
),
),
SizedBox(height: 1.5.h),
SizedBox(
height: 25.h,
child: ListView.builder(
scrollDirection: Axis.horizontal,
padding: EdgeInsets.only(left: 4.w),
itemCount: products.length,
itemBuilder: (context, index) {
final product = products[index];
return _buildProductCard(
product,
currentOutletId,
favorites.containsKey("$currentOutletId-${product.id}"),
);
},
),
),
],
);
}
Widget _buildProductCard(
ProductItem product,
String currentOutletId,
bool isFavorite,
) {
return GestureDetector(
onTap: () async {
try {
final productDetail = await ref
.read(productRepositoryProvider)
.fetchProductById(product.id);
if (productDetail == null) {
CustomSnackbar.showError(context, "Produk tidak ditemukan");
ref.read(homeControllerProvider.notifier).refresh();
return;
}
context.pushNamed(AppRoute.productDetailScreen, extra: productDetail);
} on ServerException catch (e, st) {
LogMessage.log.i(e.toString(), error: e, stackTrace: st);
}
},
child: Container(
width: 45.w,
margin: EdgeInsets.only(right: 3.w),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(2.5.w),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
flex: 3,
child: CachedNetworkImage(
imageUrl: product.image ?? 'error',
imageBuilder: (context, imageProvider) {
return Container(
width: double.infinity,
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.vertical(
top: Radius.circular(2.5.w),
),
image: DecorationImage(
image: imageProvider,
fit: BoxFit.cover,
),
),
child: Stack(
children: [
Positioned(
top: 1.w,
right: 1.w,
child: GestureDetector(
onTap: () {
final currentOutletId = ref.read(
currentOutletIdProvider,
);
ref
.read(favoriteControllerProvider.notifier)
.toggle(product, currentOutletId!);
},
child: CircleAvatar(
radius: 14,
backgroundColor: Colors.white.withOpacity(0.9),
child: Icon(
isFavorite
? Icons.favorite
: Icons.favorite_outline,
size: 5.w,
color: isFavorite ? Colors.red : Colors.grey,
),
),
),
),
],
),
);
},
errorWidget: (context, url, error) {
return Container(
width: double.infinity,
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.vertical(
top: Radius.circular(2.5.w),
),
),
child: Stack(
children: [
Center(
child: Icon(
Icons.image_outlined,
color: Colors.grey.shade300,
size: 10.w,
),
),
Positioned(
top: 1.w,
right: 1.w,
child: GestureDetector(
onTap: () {
ref
.read(favoriteControllerProvider.notifier)
.toggle(product, currentOutletId);
},
child: CircleAvatar(
radius: 14,
backgroundColor: Colors.white.withOpacity(0.9),
child: Icon(
isFavorite
? Icons.favorite
: Icons.favorite_outline,
size: 5.w,
color: isFavorite ? Colors.red : Colors.grey,
),
),
),
),
],
),
);
},
),
),
Expanded(
flex: 2,
child: Padding(
padding: EdgeInsets.all(3.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
product.name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: (AppFontSize.small - 1.25).sp,
color: const Color(0xFF102851),
),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"${product.totalSold.toCompact} Terjual",
style: TextStyle(
color: Colors.black87,
fontWeight: FontWeight.bold,
fontSize: (AppFontSize.small - 1.25).sp,
),
),
Row(
children: [
Icon(Icons.star, color: Colors.orange, size: 3.5.w),
SizedBox(width: 1.w),
Text(
product.averageRating.toStringAsFixed(1),
style: TextStyle(
fontSize: (AppFontSize.small - 1.25).sp,
color: Colors.grey.shade600,
),
),
],
),
],
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
CurrencyFormat.formatToIdr(product.sellingPrice, 0),
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: AppFontSize.small.sp,
color: AppColor.primaryColor,
),
),
Row(
children: [
Icon(
Icons.thumb_up,
color: Colors.red,
size: 3.5.w,
),
SizedBox(width: 1.w),
Text(
product.likes.toString(),
style: TextStyle(
fontSize: (AppFontSize.small - 1.25).sp,
color: Colors.red,
),
),
],
),
],
),
],
),
),
),
],
),
),
);
}
Widget _buildBottomCartAction(List<CartItem> cartItems) {
return Container(
height: 10.h,
padding: EdgeInsets.symmetric(horizontal: 4.w, vertical: 1.5.h),
decoration: BoxDecoration(
color: Colors.white,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, -5),
),
],
),
child: Row(
children: [
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Total Pesanan",
style: TextStyle(
fontSize: AppFontSize.small.sp,
color: Colors.grey.shade700,
),
),
Text(
CurrencyFormat.formatToIdr(
_calculateTotalOrder(cartItems),
0,
),
style: TextStyle(
fontSize: AppFontSize.small.sp,
fontWeight: FontWeight.bold,
color: AppColor.primaryColor,
),
),
],
),
),
ElevatedButton(
onPressed: _processCheckout,
style: ElevatedButton.styleFrom(
backgroundColor: AppColor.primaryColor,
padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 1.5.h),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(2.w),
),
elevation: 0,
),
child: Text(
"Checkout Sekarang",
style: TextStyle(
fontSize: AppFontSize.small.sp,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
),
],
),
);
}
Widget _buildEmptyCart() {
return Container(
width: double.infinity,
padding: EdgeInsets.symmetric(horizontal: 10.w),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: 60.w,
height: 60.w,
decoration: BoxDecoration(
color: AppColor.primaryColor.withOpacity(0.05),
shape: BoxShape.circle,
),
child: Icon(
Icons.shopping_cart_outlined,
size: 20.w,
color: AppColor.primaryColor.withOpacity(0.5),
),
),
SizedBox(height: 4.h),
Text(
"Keranjangmu Kosong",
style: TextStyle(
fontSize: AppFontSize.small.sp,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
SizedBox(height: 1.5.h),
Text(
"Sepertinya kamu belum menambahkan produk apa pun ke keranjang belanjaanmu.",
textAlign: TextAlign.center,
style: TextStyle(
fontSize: AppFontSize.small.sp,
color: Colors.grey.shade600,
height: 1.5,
),
),
SizedBox(height: 5.h),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () => context.goNamed(AppRoute.homeScreen),
style: ElevatedButton.styleFrom(
backgroundColor: AppColor.primaryColor,
padding: EdgeInsets.symmetric(vertical: 1.8.h),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(2.5.w),
),
elevation: 0,
),
child: Text(
"Mulai Belanja",
style: TextStyle(
fontSize: AppFontSize.small.sp,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
),
),
],
),
);
}
Widget _buildCartLoading() {
return Shimmer.fromColors(
baseColor: Colors.grey.shade300,
highlightColor: Colors.grey.shade100,
child: SingleChildScrollView(
physics: const NeverScrollableScrollPhysics(),
child: Column(
children: [
Container(
width: double.infinity,
height: 10.h,
margin: EdgeInsets.all(4.w),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(2.5.w),
),
),
Container(
margin: EdgeInsets.symmetric(horizontal: 4.w),
padding: EdgeInsets.all(4.w),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(2.5.w),
),
child: Column(
children: List.generate(
2,
(index) => Padding(
padding: EdgeInsets.only(bottom: index == 0 ? 2.h : 0),
child: Row(
children: [
Container(
width: 18.w,
height: 18.w,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(2.w),
),
),
SizedBox(width: 3.w),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: 40.w,
height: 2.h,
color: Colors.white,
),
SizedBox(height: 1.h),
Container(
width: 20.w,
height: 2.h,
color: Colors.white,
),
],
),
),
Container(
width: 20.w,
height: 4.h,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
),
),
],
),
),
),
),
),
SizedBox(height: 3.h),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: EdgeInsets.symmetric(horizontal: 4.w),
child: Container(
width: 35.w,
height: 2.h,
color: Colors.white,
),
),
SizedBox(height: 1.5.h),
SizedBox(
height: 25.h,
child: ListView.builder(
scrollDirection: Axis.horizontal,
padding: EdgeInsets.only(left: 4.w),
itemCount: 3,
itemBuilder: (_, __) => Container(
width: 45.w,
margin: EdgeInsets.only(right: 3.w),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(2.5.w),
),
),
),
),
],
),
],
),
),
);
}
}