diff --git a/lib/application/checkout/checkout_form/checkout_form_bloc.dart b/lib/application/checkout/checkout_form/checkout_form_bloc.dart index bf9f0f4..fd4ba6f 100644 --- a/lib/application/checkout/checkout_form/checkout_form_bloc.dart +++ b/lib/application/checkout/checkout_form/checkout_form_bloc.dart @@ -46,30 +46,33 @@ class CheckoutFormBloc extends Bloc { product: e.product, quantity: 1, variant: e.variant, + notes: '', ), ); } - log('🛒 Items updated: ${items.length} items total'); + log('🛒 Items updated: ${items.length} items total ${items.toList()}'); final totalQuantity = items.fold( 0, (sum, item) => sum + item.quantity, ); - final totalPrice = items.fold( - 0, - (sum, item) => - sum + - (item.quantity * - (item.variant?.priceModifier.toInt() ?? - item.product.price.toInt())), - ); + + final totalPrice = state.items.isEmpty + ? 0.0 + : state.items + .map( + (e) => + (e.product.price * e.quantity) + + (e.variant?.priceModifier ?? 0), + ) + .reduce((value, element) => value + element); emit( currentState.copyWith( items: items, totalQuantity: totalQuantity, - totalPrice: totalPrice, + totalPrice: totalPrice.toInt(), isLoading: false, ), ); diff --git a/lib/domain/product/entities/product_quantity_entity.dart b/lib/domain/product/entities/product_quantity_entity.dart index e7b4548..7e98a6d 100644 --- a/lib/domain/product/entities/product_quantity_entity.dart +++ b/lib/domain/product/entities/product_quantity_entity.dart @@ -6,9 +6,9 @@ class ProductQuantity with _$ProductQuantity { required Product product, ProductVariant? variant, required int quantity, - String? notes, + required String notes, }) = _ProductQuantity; factory ProductQuantity.empty() => - ProductQuantity(product: Product.empty(), quantity: 0); + ProductQuantity(product: Product.empty(), quantity: 0, notes: ''); } diff --git a/lib/domain/product/product.freezed.dart b/lib/domain/product/product.freezed.dart index 7f760e7..2f9192c 100644 --- a/lib/domain/product/product.freezed.dart +++ b/lib/domain/product/product.freezed.dart @@ -1097,7 +1097,7 @@ mixin _$ProductQuantity { Product get product => throw _privateConstructorUsedError; ProductVariant? get variant => throw _privateConstructorUsedError; int get quantity => throw _privateConstructorUsedError; - String? get notes => throw _privateConstructorUsedError; + String get notes => throw _privateConstructorUsedError; /// Create a copy of ProductQuantity /// with the given fields replaced by the non-null parameter values. @@ -1117,7 +1117,7 @@ abstract class $ProductQuantityCopyWith<$Res> { Product product, ProductVariant? variant, int quantity, - String? notes, + String notes, }); $ProductCopyWith<$Res> get product; @@ -1142,7 +1142,7 @@ class _$ProductQuantityCopyWithImpl<$Res, $Val extends ProductQuantity> Object? product = null, Object? variant = freezed, Object? quantity = null, - Object? notes = freezed, + Object? notes = null, }) { return _then( _value.copyWith( @@ -1158,10 +1158,10 @@ class _$ProductQuantityCopyWithImpl<$Res, $Val extends ProductQuantity> ? _value.quantity : quantity // ignore: cast_nullable_to_non_nullable as int, - notes: freezed == notes + notes: null == notes ? _value.notes : notes // ignore: cast_nullable_to_non_nullable - as String?, + as String, ) as $Val, ); @@ -1205,7 +1205,7 @@ abstract class _$$ProductQuantityImplCopyWith<$Res> Product product, ProductVariant? variant, int quantity, - String? notes, + String notes, }); @override @@ -1231,7 +1231,7 @@ class __$$ProductQuantityImplCopyWithImpl<$Res> Object? product = null, Object? variant = freezed, Object? quantity = null, - Object? notes = freezed, + Object? notes = null, }) { return _then( _$ProductQuantityImpl( @@ -1247,10 +1247,10 @@ class __$$ProductQuantityImplCopyWithImpl<$Res> ? _value.quantity : quantity // ignore: cast_nullable_to_non_nullable as int, - notes: freezed == notes + notes: null == notes ? _value.notes : notes // ignore: cast_nullable_to_non_nullable - as String?, + as String, ), ); } @@ -1265,7 +1265,7 @@ class _$ProductQuantityImpl required this.product, this.variant, required this.quantity, - this.notes, + required this.notes, }); @override @@ -1275,7 +1275,7 @@ class _$ProductQuantityImpl @override final int quantity; @override - final String? notes; + final String notes; @override String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { @@ -1326,7 +1326,7 @@ abstract class _ProductQuantity implements ProductQuantity { required final Product product, final ProductVariant? variant, required final int quantity, - final String? notes, + required final String notes, }) = _$ProductQuantityImpl; @override @@ -1336,7 +1336,7 @@ abstract class _ProductQuantity implements ProductQuantity { @override int get quantity; @override - String? get notes; + String get notes; /// Create a copy of ProductQuantity /// with the given fields replaced by the non-null parameter values. diff --git a/lib/presentation/app_widget.dart b/lib/presentation/app_widget.dart index 5a9eed3..76c5bf4 100644 --- a/lib/presentation/app_widget.dart +++ b/lib/presentation/app_widget.dart @@ -3,6 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import '../application/auth/auth_bloc.dart'; import '../application/category/category_loader/category_loader_bloc.dart'; +import '../application/checkout/checkout_form/checkout_form_bloc.dart'; import '../application/outlet/outlet_loader/outlet_loader_bloc.dart'; import '../application/product/product_loader/product_loader_bloc.dart'; import '../common/theme/theme.dart'; @@ -29,6 +30,7 @@ class _AppWidgetState extends State { BlocProvider(create: (context) => getIt()), BlocProvider(create: (context) => getIt()), BlocProvider(create: (context) => getIt()), + BlocProvider(create: (context) => getIt()), ], child: MaterialApp.router( debugShowCheckedModeBanner: false, diff --git a/lib/presentation/components/card/order_menu.dart b/lib/presentation/components/card/order_menu.dart new file mode 100644 index 0000000..8b2b15c --- /dev/null +++ b/lib/presentation/components/card/order_menu.dart @@ -0,0 +1,156 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../../../application/checkout/checkout_form/checkout_form_bloc.dart'; +import '../../../common/extension/extension.dart'; +import '../../../common/theme/theme.dart'; +import '../../../domain/product/product.dart'; +import '../image/image.dart'; +import '../spaces/space.dart'; + +class OrderMenu extends StatefulWidget { + final ProductQuantity data; + const OrderMenu({super.key, required this.data}); + + @override + State createState() => _OrderMenuState(); +} + +class _OrderMenuState extends State { + final _controller = TextEditingController(); + + @override + void initState() { + super.initState(); + _controller.text = widget.data.notes; + + _controller.addListener(() { + context.read().add( + CheckoutFormEvent.updateItemNotes( + widget.data.product, + _controller.text, + ), + ); + }); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Row( + children: [ + Flexible( + child: ListTile( + contentPadding: EdgeInsets.zero, + leading: ClipRRect( + borderRadius: BorderRadius.all(Radius.circular(8.0)), + child: AppNetworkImage( + url: widget.data.product.imageUrl, + width: 50.0, + height: 50.0, + fit: BoxFit.cover, + ), + ), + title: Row( + children: [ + Expanded( + child: Text( + widget.data.product.name, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + (widget.data.product.price + + (widget.data.variant?.priceModifier ?? 0)) + .currencyFormatRp, + ), + if (widget.data.variant != null) + Text(widget.data.variant?.name ?? ""), + ], + ), + ), + ), + Row( + children: [ + GestureDetector( + onTap: () { + context.read().add( + CheckoutFormEvent.removeItem( + widget.data.product, + widget.data.variant, + ), + ); + }, + child: Container( + width: 30, + height: 30, + color: AppColor.white, + child: const Icon( + Icons.remove_circle, + color: AppColor.primary, + ), + ), + ), + SizedBox( + width: 30.0, + child: Center(child: Text(widget.data.quantity.toString())), + ), + GestureDetector( + onTap: () { + context.read().add( + CheckoutFormEvent.addItem( + widget.data.product, + widget.data.variant, + ), + ); + }, + child: Container( + width: 30, + height: 30, + color: AppColor.white, + child: const Icon( + Icons.add_circle, + color: AppColor.primary, + ), + ), + ), + ], + ), + const SpaceWidth(8), + SizedBox( + width: 80.0, + child: Text( + ((widget.data.product.price + + (widget.data.variant?.priceModifier ?? 0)) * + widget.data.quantity) + .currencyFormatRp, + textAlign: TextAlign.right, + style: const TextStyle( + color: AppColor.primary, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + ], + ); + } +} diff --git a/lib/presentation/components/card/product_card.dart b/lib/presentation/components/card/product_card.dart index 11a4d85..d14f633 100644 --- a/lib/presentation/components/card/product_card.dart +++ b/lib/presentation/components/card/product_card.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import '../../../application/checkout/checkout_form/checkout_form_bloc.dart'; import '../../../common/extension/extension.dart'; import '../../../common/theme/theme.dart'; import '../../../domain/product/product.dart'; @@ -17,9 +19,9 @@ class ProductCard extends StatelessWidget { onTap: () { if (product.isActive == true) { if (product.variants.isEmpty) { - // context.read().add( - // CheckoutEvent.addItem(data, null), - // ); + context.read().add( + CheckoutFormEvent.addItem(product, null), + ); } else { showDialog( context: context, @@ -85,28 +87,40 @@ class ProductCard extends StatelessWidget { ), ), - Positioned( - top: 4, - right: 4, - child: Container( - width: 40, - height: 40, - padding: const EdgeInsets.all(6), - decoration: const BoxDecoration( - borderRadius: BorderRadius.all(Radius.circular(9.0)), - color: AppColor.primary, - ), - child: Center( - child: Text( - 0.toString(), - style: const TextStyle( - color: Colors.white, - fontSize: 20, - fontWeight: FontWeight.bold, + BlocBuilder( + builder: (context, state) { + final totalQuantity = state.items + .where((item) => item.product.id == product.id) + .map((item) => item.quantity) + .fold(0, (sum, qty) => sum + qty); + + if (totalQuantity == 0) { + return const SizedBox.shrink(); + } + return Positioned( + top: 4, + right: 4, + child: Container( + width: 40, + height: 40, + padding: const EdgeInsets.all(6), + decoration: const BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(9.0)), + color: AppColor.primary, + ), + child: Center( + child: Text( + totalQuantity.toString(), + style: const TextStyle( + color: Colors.white, + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), ), ), - ), - ), + ); + }, ), if (product.isActive == false) Container( diff --git a/lib/presentation/components/dialog/dialog.dart b/lib/presentation/components/dialog/dialog.dart index 0de076f..550b1b5 100644 --- a/lib/presentation/components/dialog/dialog.dart +++ b/lib/presentation/components/dialog/dialog.dart @@ -2,6 +2,7 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import '../../../application/checkout/checkout_form/checkout_form_bloc.dart'; import '../../../application/outlet/outlet_loader/outlet_loader_bloc.dart'; import '../../../common/extension/extension.dart'; import '../../../common/theme/theme.dart'; diff --git a/lib/presentation/components/dialog/variant_dialog.dart b/lib/presentation/components/dialog/variant_dialog.dart index 97bb274..2821771 100644 --- a/lib/presentation/components/dialog/variant_dialog.dart +++ b/lib/presentation/components/dialog/variant_dialog.dart @@ -21,10 +21,10 @@ class VariantDialog extends StatelessWidget { return GestureDetector( onTap: () { // Aksi saat varian dipilih - // context.pop(); - // context.read().add( - // CheckoutEvent.addItem(product, variant), - // ); + context.maybePop(); + context.read().add( + CheckoutFormEvent.addItem(product, variant), + ); }, child: Container( width: (context.deviceWidth * 0.4 - 12 - 32) / 2 - 6, // 2 per row diff --git a/lib/presentation/pages/main/pages/home/widgets/home_right_panel.dart b/lib/presentation/pages/main/pages/home/widgets/home_right_panel.dart index 550a5ef..3014835 100644 --- a/lib/presentation/pages/main/pages/home/widgets/home_right_panel.dart +++ b/lib/presentation/pages/main/pages/home/widgets/home_right_panel.dart @@ -1,6 +1,15 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; +import 'dart:developer'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../../../../../../application/checkout/checkout_form/checkout_form_bloc.dart'; +import '../../../../../../common/extension/extension.dart'; +import '../../../../../../common/theme/theme.dart'; +import '../../../../../components/button/button.dart'; +import '../../../../../components/card/order_menu.dart'; +import '../../../../../components/spaces/space.dart'; +import '../../../../../components/toast/flushbar.dart'; import 'home_right_title.dart'; class HomeRightPanel extends StatelessWidget { @@ -8,12 +17,168 @@ class HomeRightPanel extends StatelessWidget { @override Widget build(BuildContext context) { - return Align( - alignment: Alignment.topCenter, - child: Material( - color: Colors.white, - child: Column(children: [HomeRightTitle()]), - ), + return BlocBuilder( + builder: (context, state) { + final price = state.items.isEmpty + ? 0.0 + : state.items + .map( + (e) => + (e.product.price + (e.variant?.priceModifier ?? 0)) * + e.quantity, + ) + .reduce((value, element) => value + element); + log('🛒 Total Price: $price'); + return Align( + alignment: Alignment.topCenter, + child: Material( + color: Colors.white, + child: Column( + children: [ + HomeRightTitle(), + Padding( + padding: const EdgeInsets.all( + 16.0, + ).copyWith(bottom: 0, top: 20), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Item', + style: AppStyle.lg.copyWith( + color: AppColor.primary, + fontWeight: FontWeight.w600, + ), + ), + SizedBox(width: 130), + SizedBox( + width: 50.0, + child: Text( + 'Qty', + style: AppStyle.lg.copyWith( + color: AppColor.primary, + fontWeight: FontWeight.w600, + ), + ), + ), + SizedBox( + child: Text( + 'Price', + style: AppStyle.lg.copyWith( + color: AppColor.primary, + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), + const SpaceHeight(8), + const Divider(), + ], + ), + ), + Expanded( + child: state.items.isEmpty + ? const Center(child: Text('No Items')) + : ListView.separated( + shrinkWrap: true, + padding: const EdgeInsets.symmetric(horizontal: 16), + itemBuilder: (context, index) => + OrderMenu(data: state.items[index]), + separatorBuilder: (context, index) => + const SpaceHeight(1.0), + itemCount: state.items.length, + ), + ), + + Padding( + padding: const EdgeInsets.all(16.0).copyWith(top: 0), + child: Column( + children: [ + const Divider(), + const SpaceHeight(16.0), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Pajak', + style: AppStyle.md.copyWith( + color: AppColor.black, + fontWeight: FontWeight.bold, + ), + ), + Text( + '${state.tax} %', + style: AppStyle.md.copyWith( + color: AppColor.primary, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + const SpaceHeight(16.0), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + 'Sub total', + style: TextStyle( + color: AppColor.black, + fontWeight: FontWeight.bold, + ), + ), + Text( + price.currencyFormatRp, + style: const TextStyle( + color: AppColor.primary, + fontWeight: FontWeight.w900, + ), + ), + ], + ), + SpaceHeight(16.0), + Align( + alignment: Alignment.bottomCenter, + child: AppElevatedButton.filled( + borderRadius: 12, + elevation: 1, + disabled: state.items.isEmpty, + onPressed: () { + if (state.orderType.name == 'dineIn') { + AppFlushbar.showError( + context, + 'Mohon pilih meja terlebih dahulu', + ); + return; + } + if (state.orderType.name == 'delivery' && + state.delivery == null) { + AppFlushbar.showError( + context, + 'Mohon pilih pengiriman terlebih dahulu', + ); + return; + } + // context.push( + // ConfirmPaymentPage( + // isTable: widget.table == null ? false : true, + // table: widget.table, + // ), + // ); + }, + label: 'Lanjutkan Pembayaran', + ), + ), + ], + ), + ), + ], + ), + ), + ); + }, ); } }