save order pay later page

This commit is contained in:
efrilm 2025-10-28 16:26:35 +07:00
parent a6e58a8cc0
commit a514a8abdb
5 changed files with 447 additions and 47 deletions

View File

@ -41,7 +41,12 @@ class OrderFormBloc extends Bloc<OrderFormEvent, OrderFormState> {
createOrderWithPayment: (e) async {
Either<OrderFailure, Order> 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<OrderFormEvent, OrderFormState> {
emit(
state.copyWith(
isCreating: false,
failureOrCreateOrder: optionOf(failureOrOrder),
isCreatingWithPayment: false,
failureOrCreateOrderWithPayment: optionOf(failureOrOrder),
),
);
},

View File

@ -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<Table> tables;
const OrderPayLaterDialog({super.key, required this.tables});
@override
Widget build(BuildContext context) {
return BlocBuilder<CheckoutFormBloc, CheckoutFormState>(
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<Table>(
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<CheckoutFormBloc>().add(
CheckoutFormEvent.updateTable(selectedTable),
);
},
// Validator (optional)
validator: (Table? value) {
if (value == null) {
return "Meja harus dipilih";
}
return null;
},
);
},
),
SpaceHeight(24),
BlocBuilder<OrderFormBloc, OrderFormState>(
builder: (context, orderState) {
return AppElevatedButton.filled(
label: 'Simpan',
isLoading: orderState.isCreating,
onPressed: () {
context.read<OrderFormBloc>().add(
OrderFormEvent.createOrder(
items: state.items,
orderType: state.orderType,
table: state.table,
),
);
},
);
},
),
],
),
);
},
);
}
}

View File

@ -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<Table> 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),
),
],
),
],
),
),
);
}
}

View File

@ -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<OrderFormBloc, OrderFormState>(
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<CheckoutFormBloc>().add(
CheckoutFormEvent.started([]),
return MultiBlocListener(
listeners: [
BlocListener<OrderFormBloc, OrderFormState>(
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<CheckoutFormBloc>().add(
CheckoutFormEvent.started([]),
);
context.router.replace(SuccessOrderRoute(order: order));
}
},
);
context.router.replace(SuccessOrderRoute(order: order));
}
});
});
},
});
},
),
BlocListener<OrderFormBloc, OrderFormState>(
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<CheckoutFormBloc>().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<PaymentMethodLoaderBloc>()
..add(PaymentMethodLoaderEvent.fetched(isRefresh: true)),
Widget wrappedRoute(BuildContext context) => MultiBlocProvider(
providers: [
BlocProvider(
create: (context) =>
getIt<PaymentMethodLoaderBloc>()
..add(PaymentMethodLoaderEvent.fetched(isRefresh: true)),
),
BlocProvider(
create: (context) => getIt<TableLoaderBloc>()
..add(TableLoaderEvent.fetched(isRefresh: true, status: 'available')),
),
],
child: this,
);
}

View File

@ -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<CheckoutRightPanel> {
),
SpaceWidth(12),
Expanded(
child: AppElevatedButton.filled(
onPressed: () {
if (customerController.text == '') {
AppFlushbar.showError(
context,
'Pilih Pelanggan terlebih dahulu',
);
return;
}
child: BlocBuilder<TableLoaderBloc, TableLoaderState>(
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 == '') {