diff --git a/lib/application/order/order_form/order_form_bloc.dart b/lib/application/order/order_form/order_form_bloc.dart index a5ce553..0cb8f4b 100644 --- a/lib/application/order/order_form/order_form_bloc.dart +++ b/lib/application/order/order_form/order_form_bloc.dart @@ -41,7 +41,12 @@ class OrderFormBloc extends Bloc { createOrderWithPayment: (e) async { Either failureOrOrder; - emit(state.copyWith(isCreating: true, failureOrCreateOrder: none())); + emit( + state.copyWith( + isCreatingWithPayment: true, + failureOrCreateOrderWithPayment: none(), + ), + ); final outlet = await _outletRepository.currentOutlet(); @@ -73,8 +78,8 @@ class OrderFormBloc extends Bloc { emit( state.copyWith( - isCreating: false, - failureOrCreateOrder: optionOf(failureOrOrder), + isCreatingWithPayment: false, + failureOrCreateOrderWithPayment: optionOf(failureOrOrder), ), ); }, diff --git a/lib/presentation/components/dialog/order/order_pay_later_dialog.dart b/lib/presentation/components/dialog/order/order_pay_later_dialog.dart new file mode 100644 index 0000000..f6c9fab --- /dev/null +++ b/lib/presentation/components/dialog/order/order_pay_later_dialog.dart @@ -0,0 +1,260 @@ +import 'package:dropdown_search/dropdown_search.dart'; +import 'package:flutter/material.dart' hide Table; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../../../../application/checkout/checkout_form/checkout_form_bloc.dart'; +import '../../../../application/order/order_form/order_form_bloc.dart'; +import '../../../../common/extension/extension.dart'; +import '../../../../common/theme/theme.dart'; +import '../../../../domain/table/table.dart'; +import '../../button/button.dart'; +import '../../spaces/space.dart'; +import '../dialog.dart'; + +class OrderPayLaterDialog extends StatelessWidget { + final List tables; + const OrderPayLaterDialog({super.key, required this.tables}); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return CustomModalDialog( + title: 'Bayar Nanti', + subtitle: 'Simpan pesanan dan bayar nanti', + contentPadding: const EdgeInsets.symmetric( + horizontal: 16.0, + vertical: 24.0, + ), + minWidth: context.deviceWidth * 0.4, + minHeight: context.deviceHeight * 0.4, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Pilih Meja', + style: AppStyle.md.copyWith( + color: AppColor.black, + fontWeight: FontWeight.w600, + ), + ), + const SpaceHeight(6.0), + Builder( + builder: (context) { + if (tables.isEmpty) { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppColor.warning.withOpacity(0.15), + borderRadius: BorderRadius.circular(16), + border: Border.all(color: AppColor.warning, width: 1), + ), + child: Text( + 'Tidak ada meja yang tersedia. Silakan pilih opsi lain.', + style: AppStyle.md.copyWith(color: AppColor.warning), + ), + ); + } + + return DropdownSearch
( + items: tables, + selectedItem: state.table, + + // Dropdown properties + dropdownDecoratorProps: DropDownDecoratorProps( + dropdownSearchDecoration: InputDecoration( + hintText: "Pilih meja", + hintStyle: TextStyle( + color: Colors.grey.shade600, + fontSize: 14, + ), + prefixIcon: Icon( + Icons.category_outlined, + color: Colors.grey.shade500, + size: 20, + ), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide( + color: Colors.grey.shade300, + width: 1.5, + ), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide( + color: Colors.grey.shade300, + width: 1.5, + ), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide( + color: Colors.blue.shade400, + width: 2, + ), + ), + filled: true, + fillColor: Colors.white, + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 16, + ), + ), + ), + + // Popup properties + popupProps: PopupProps.menu( + showSearchBox: true, + searchFieldProps: TextFieldProps( + decoration: InputDecoration( + hintText: "Cari meja...", + prefixIcon: const Icon(Icons.search), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), + ), + ), + menuProps: MenuProps( + backgroundColor: Colors.white, + elevation: 8, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + itemBuilder: (context, category, isSelected) { + return Container( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), + decoration: BoxDecoration( + color: isSelected + ? Colors.blue.shade50 + : Colors.transparent, + border: Border( + bottom: BorderSide( + color: Colors.grey.shade100, + width: 0.5, + ), + ), + ), + child: Row( + children: [ + Container( + width: 8, + height: 8, + decoration: BoxDecoration( + color: isSelected + ? Colors.blue.shade600 + : Colors.grey.shade400, + shape: BoxShape.circle, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Text( + category.tableName, + style: TextStyle( + fontSize: 14, + fontWeight: isSelected + ? FontWeight.w600 + : FontWeight.w500, + color: isSelected + ? Colors.blue.shade700 + : Colors.black87, + ), + ), + ), + if (isSelected) + Icon( + Icons.check, + color: Colors.blue.shade600, + size: 18, + ), + ], + ), + ); + }, + emptyBuilder: (context, searchEntry) { + return Container( + padding: const EdgeInsets.all(20), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.search_off, + color: Colors.grey.shade400, + size: 48, + ), + const SizedBox(height: 12), + Text( + searchEntry.isEmpty + ? "Tidak ada meja tersedia" + : "Tidak ditemukan meja dengan '${searchEntry}'", + style: TextStyle( + color: Colors.grey.shade600, + fontSize: 14, + ), + textAlign: TextAlign.center, + ), + ], + ), + ); + }, + ), + + // Item as string (for search functionality) + itemAsString: (Table table) => table.tableName, + + // Comparison function + compareFn: (Table? item1, Table? item2) { + return item1?.id == item2?.id; + }, + + // On changed callback + onChanged: (Table? selectedTable) { + context.read().add( + CheckoutFormEvent.updateTable(selectedTable), + ); + }, + + // Validator (optional) + validator: (Table? value) { + if (value == null) { + return "Meja harus dipilih"; + } + return null; + }, + ); + }, + ), + SpaceHeight(24), + BlocBuilder( + builder: (context, orderState) { + return AppElevatedButton.filled( + label: 'Simpan', + isLoading: orderState.isCreating, + onPressed: () { + context.read().add( + OrderFormEvent.createOrder( + items: state.items, + orderType: state.orderType, + table: state.table, + ), + ); + }, + ); + }, + ), + ], + ), + ); + }, + ); + } +} diff --git a/lib/presentation/components/dialog/order/order_save_dialog.dart b/lib/presentation/components/dialog/order/order_save_dialog.dart new file mode 100644 index 0000000..a9cc49a --- /dev/null +++ b/lib/presentation/components/dialog/order/order_save_dialog.dart @@ -0,0 +1,98 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/material.dart' hide Table; + +import '../../../../application/checkout/checkout_form/checkout_form_bloc.dart'; +import '../../../../common/theme/theme.dart'; +import '../../../../common/types/order_type.dart'; +import '../../../../domain/table/table.dart'; +import '../../spaces/space.dart'; +import '../dialog.dart'; +import 'order_pay_later_dialog.dart'; + +class OrderSaveDialog extends StatelessWidget { + final CheckoutFormState checkoutState; + final List
tables; + const OrderSaveDialog({ + super.key, + required this.checkoutState, + required this.tables, + }); + + @override + Widget build(BuildContext context) { + return CustomModalDialog( + title: 'Pilih Aksi', + subtitle: 'Lanjutkan proses pesanan atau simpan untuk nanti.', + contentPadding: const EdgeInsets.symmetric( + horizontal: 16.0, + vertical: 24.0, + ), + child: Column( + children: [ + _item( + icon: Icons.schedule_outlined, + title: 'Bayar Nanti', + subtitle: 'Simpan pesanan dan bayar nanti', + onTap: () { + context.maybePop(); + + if (checkoutState.orderType == OrderType.dineIn) { + Future.delayed(Duration(milliseconds: 100), () { + showDialog( + context: context, + builder: (context) => OrderPayLaterDialog(tables: tables), + ); + }); + } + }, + ), + SpaceHeight(16.0), + _item( + icon: Icons.shopping_cart_checkout_outlined, + title: 'Tambahkan Pesanan', + subtitle: 'Tambah item ke daftar pesanan', + onTap: () {}, + ), + ], + ), + ); + } + + Widget _item({ + required IconData icon, + required String title, + required String subtitle, + required Function() onTap, + }) { + return GestureDetector( + onTap: onTap, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0), + decoration: BoxDecoration( + color: AppColor.white, + borderRadius: BorderRadius.circular(8.0), + border: Border.all(color: AppColor.border, width: 1.0), + ), + child: Row( + children: [ + Icon(icon, color: AppColor.primary, size: 26.0), + SpaceWidth(12.0), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: AppStyle.md.copyWith(fontWeight: FontWeight.w600), + ), + Text( + subtitle, + style: AppStyle.md.copyWith(color: AppColor.textSecondary), + ), + ], + ), + ], + ), + ), + ); + } +} diff --git a/lib/presentation/pages/checkout/checkout_page.dart b/lib/presentation/pages/checkout/checkout_page.dart index 49a981a..7cae704 100644 --- a/lib/presentation/pages/checkout/checkout_page.dart +++ b/lib/presentation/pages/checkout/checkout_page.dart @@ -5,6 +5,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import '../../../application/checkout/checkout_form/checkout_form_bloc.dart'; import '../../../application/order/order_form/order_form_bloc.dart'; import '../../../application/payment_method/payment_method_loader/payment_method_loader_bloc.dart'; +import '../../../application/table/table_loader/table_loader_bloc.dart'; import '../../../common/theme/theme.dart'; import '../../../injection.dart'; import '../../components/spaces/space.dart'; @@ -19,23 +20,48 @@ class CheckoutPage extends StatelessWidget implements AutoRouteWrapper { @override Widget build(BuildContext context) { - return BlocListener( - listenWhen: (previous, current) => - previous.failureOrCreateOrder != current.failureOrCreateOrder, - listener: (context, state) { - state.failureOrCreateOrder.fold(() {}, (either) { - either.fold((f) => AppFlushbar.showOrderFailureToast(context, f), ( - order, - ) { - if (context.mounted) { - context.read().add( - CheckoutFormEvent.started([]), + return MultiBlocListener( + listeners: [ + BlocListener( + listenWhen: (previous, current) => + previous.failureOrCreateOrderWithPayment != + current.failureOrCreateOrderWithPayment, + listener: (context, state) { + state.failureOrCreateOrderWithPayment.fold(() {}, (either) { + either.fold( + (f) => AppFlushbar.showOrderFailureToast(context, f), + (order) { + if (context.mounted) { + context.read().add( + CheckoutFormEvent.started([]), + ); + context.router.replace(SuccessOrderRoute(order: order)); + } + }, ); - context.router.replace(SuccessOrderRoute(order: order)); - } - }); - }); - }, + }); + }, + ), + BlocListener( + listenWhen: (previous, current) => + previous.failureOrCreateOrder != current.failureOrCreateOrder, + listener: (context, state) { + state.failureOrCreateOrder.fold(() {}, (either) { + either.fold( + (f) => AppFlushbar.showOrderFailureToast(context, f), + (order) { + if (context.mounted) { + context.read().add( + CheckoutFormEvent.started([]), + ); + context.router.replace(SuccessOrderRoute(order: order)); + } + }, + ); + }); + }, + ), + ], child: SafeArea( child: Hero( tag: 'checkout_screen', @@ -80,10 +106,18 @@ class CheckoutPage extends StatelessWidget implements AutoRouteWrapper { } @override - Widget wrappedRoute(BuildContext context) => BlocProvider( - create: (context) => - getIt() - ..add(PaymentMethodLoaderEvent.fetched(isRefresh: true)), + Widget wrappedRoute(BuildContext context) => MultiBlocProvider( + providers: [ + BlocProvider( + create: (context) => + getIt() + ..add(PaymentMethodLoaderEvent.fetched(isRefresh: true)), + ), + BlocProvider( + create: (context) => getIt() + ..add(TableLoaderEvent.fetched(isRefresh: true, status: 'available')), + ), + ], child: this, ); } diff --git a/lib/presentation/pages/checkout/widgets/checkout_right_panel.dart b/lib/presentation/pages/checkout/widgets/checkout_right_panel.dart index 216ed06..cdb05c4 100644 --- a/lib/presentation/pages/checkout/widgets/checkout_right_panel.dart +++ b/lib/presentation/pages/checkout/widgets/checkout_right_panel.dart @@ -1,14 +1,16 @@ import 'package:auto_route/auto_route.dart'; -import 'package:flutter/widgets.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../../../../application/checkout/checkout_form/checkout_form_bloc.dart'; import '../../../../application/order/order_form/order_form_bloc.dart'; import '../../../../application/payment_method/payment_method_loader/payment_method_loader_bloc.dart'; +import '../../../../application/table/table_loader/table_loader_bloc.dart'; import '../../../../common/extension/extension.dart'; import '../../../../common/theme/theme.dart'; import '../../../components/button/button.dart'; import '../../../components/card/payment_card.dart'; +import '../../../components/dialog/order/order_save_dialog.dart'; import '../../../components/error/payment_method_error_state_widget.dart'; import '../../../components/field/field.dart'; import '../../../components/loader/loader_with_text.dart'; @@ -276,36 +278,37 @@ class _CheckoutRightPanelState extends State { ), SpaceWidth(12), Expanded( - child: AppElevatedButton.filled( - onPressed: () { - if (customerController.text == '') { - AppFlushbar.showError( - context, - 'Pilih Pelanggan terlebih dahulu', - ); - return; - } + child: BlocBuilder( + builder: (context, tableState) { + return AppElevatedButton.filled( + isLoading: tableState.isFetching, + onPressed: () { + if (customerController.text == '') { + AppFlushbar.showError( + context, + 'Pilih Pelanggan terlebih dahulu', + ); + return; + } - // showDialog( - // context: context, - // builder: (dcontext) => SaveDialog( - // selectedTable: widget.table, - // customerName: customerController.text, - // items: items, - // orderType: orderType, - // customer: selectedCustomer, - // deliveryModel: delivery, - // ), - // ); + showDialog( + context: context, + builder: (dcontext) => OrderSaveDialog( + checkoutState: widget.checkoutState, + tables: tableState.tables, + ), + ); + }, + label: 'Simpan', + ); }, - label: 'Simpan', ), ), SpaceWidth(12), Expanded( child: AppElevatedButton.filled( - isLoading: orderState.isCreating, - onPressed: orderState.isCreating + isLoading: orderState.isCreatingWithPayment, + onPressed: orderState.isCreatingWithPayment ? null : () { if (customerController.text == '') {