diff --git a/lib/presentation/components/card/order_menu.dart b/lib/presentation/components/card/order_menu.dart index 8b2b15c..ac8ad8a 100644 --- a/lib/presentation/components/card/order_menu.dart +++ b/lib/presentation/components/card/order_menu.dart @@ -150,6 +150,42 @@ class _OrderMenuState extends State { ), ], ), + SpaceHeight(8.0), + SizedBox( + height: 40, + child: Row( + children: [ + Flexible( + child: TextFormField( + cursorColor: AppColor.primary, + controller: _controller, + style: const TextStyle(fontSize: 12, color: AppColor.black), + decoration: InputDecoration(hintText: 'Catatan Pesanan'), + ), + ), + const SpaceWidth(16.0), + GestureDetector( + onTap: () { + context.read().add( + CheckoutFormEvent.removeItem( + widget.data.product, + widget.data.variant, + ), + ); + }, + child: Container( + height: 40, + width: 40, + decoration: BoxDecoration( + color: AppColor.primary, + borderRadius: BorderRadius.circular(8.0), + ), + child: Icon(Icons.delete_outline, color: AppColor.white), + ), + ), + ], + ), + ), ], ); } diff --git a/lib/presentation/components/field/customer_autocomplete.dart b/lib/presentation/components/field/customer_autocomplete.dart new file mode 100644 index 0000000..76a5e87 --- /dev/null +++ b/lib/presentation/components/field/customer_autocomplete.dart @@ -0,0 +1,107 @@ +part of 'field.dart'; + +class CustomerAutocomplete extends StatefulWidget { + final TextEditingController controller; + final Customer? selectedCustomer; + final ValueChanged? onSelected; + + const CustomerAutocomplete({ + super.key, + required this.controller, + this.selectedCustomer, + this.onSelected, + }); + + @override + State createState() => _CustomerAutocompleteState(); +} + +class _CustomerAutocompleteState extends State { + final FocusNode _focusNode = FocusNode(); + @override + void initState() { + super.initState(); + context.read().add(CustomerLoaderEvent.fetched()); + } + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Pelanggan', + style: AppStyle.lg.copyWith( + color: AppColor.black, + fontWeight: FontWeight.w600, + ), + ), + const SpaceHeight(6.0), + BlocBuilder( + builder: (context, state) { + return RawAutocomplete( + textEditingController: widget.controller, + focusNode: _focusNode, + optionsBuilder: (TextEditingValue textEditingValue) { + if (textEditingValue.text.isEmpty) { + return const Iterable.empty(); + } + return state.customers.where((Customer customer) { + return customer.name.toLowerCase().contains( + textEditingValue.text.toLowerCase(), + ); + }); + }, + displayStringForOption: (Customer option) => option.name, + onSelected: (Customer selection) { + widget.controller.text = selection.name; + if (widget.onSelected != null) { + widget.onSelected!(selection); + } + }, + fieldViewBuilder: + (context, controller, focusNode, onFieldSubmitted) { + return TextFormField( + controller: controller, + focusNode: focusNode, + decoration: const InputDecoration( + hintText: 'Nama Pelanggan', + ), + onChanged: (value) { + if (widget.onSelected != null) { + widget.onSelected!(null); // reset jika ketik manual + } + }, + ); + }, + optionsViewBuilder: (context, onSelected, options) { + return Material( + elevation: 1, + borderRadius: BorderRadius.circular(8), + color: Colors.white, + child: SizedBox( + height: context.deviceHeight * 0.4, + child: ListView.separated( + padding: EdgeInsets.zero, + itemCount: options.length, + separatorBuilder: (_, __) => const Divider(height: 1), + itemBuilder: (BuildContext context, int index) { + final Customer option = options.elementAt(index); + return ListTile( + title: Text(option.name), + onTap: () { + onSelected(option); + }, + ); + }, + ), + ), + ); + }, + ); + }, + ), + ], + ); + } +} diff --git a/lib/presentation/components/field/field.dart b/lib/presentation/components/field/field.dart index 8a407f8..1dca7ac 100644 --- a/lib/presentation/components/field/field.dart +++ b/lib/presentation/components/field/field.dart @@ -1,8 +1,13 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import '../../../application/customer/customer_loader/customer_loader_bloc.dart'; +import '../../../common/extension/extension.dart'; import '../../../common/theme/theme.dart'; +import '../../../domain/customer/customer.dart'; import '../spaces/space.dart'; part 'password_text_field.dart'; part 'text_field.dart'; part 'search_text_field.dart'; +part 'customer_autocomplete.dart'; diff --git a/lib/presentation/pages/checkout/checkout_page.dart b/lib/presentation/pages/checkout/checkout_page.dart new file mode 100644 index 0000000..459261c --- /dev/null +++ b/lib/presentation/pages/checkout/checkout_page.dart @@ -0,0 +1,52 @@ +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 '../../../common/theme/theme.dart'; +import '../../components/spaces/space.dart'; +import 'widgets/checkout_left_panel.dart'; +import 'widgets/checkout_right_panel.dart'; + +@RoutePage() +class CheckoutPage extends StatelessWidget { + const CheckoutPage({super.key}); + + @override + Widget build(BuildContext context) { + return SafeArea( + child: Hero( + tag: 'checkout_screen', + child: BlocBuilder( + builder: (context, state) { + final price = state.items.fold( + 0, + (previousValue, element) => + previousValue + + (element.product.price.toInt() + + (element.variant?.priceModifier.toInt() ?? 0)) * + element.quantity, + ); + + return Scaffold( + backgroundColor: AppColor.white, + body: Row( + children: [ + Expanded( + flex: 3, + child: CheckoutLeftPanel( + checkoutState: state, + price: price, + ), + ), + SpaceWidth(2), + Expanded(flex: 3, child: CheckoutRightPanel()), + ], + ), + ); + }, + ), + ), + ); + } +} diff --git a/lib/presentation/pages/checkout/widgets/checkout_left_panel.dart b/lib/presentation/pages/checkout/widgets/checkout_left_panel.dart new file mode 100644 index 0000000..3acb220 --- /dev/null +++ b/lib/presentation/pages/checkout/widgets/checkout_left_panel.dart @@ -0,0 +1,199 @@ +import 'package:flutter/widgets.dart'; + +import '../../../../application/checkout/checkout_form/checkout_form_bloc.dart'; +import '../../../../common/extension/extension.dart'; +import '../../../../common/theme/theme.dart'; +import '../../../components/border/dashed_border.dart'; +import '../../../components/card/order_menu.dart'; +import '../../../components/page/page_title.dart'; +import '../../../components/spaces/space.dart'; + +class CheckoutLeftPanel extends StatelessWidget { + final CheckoutFormState checkoutState; + final int price; + const CheckoutLeftPanel({ + super.key, + required this.checkoutState, + required this.price, + }); + + @override + Widget build(BuildContext context) { + return Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + PageTitle( + title: 'Konfirmasi', + subtitle: checkoutState.table != null + ? 'Pesanan Meja ${checkoutState.table?.tableName ?? "-"}' + : 'Pesanan', + actionWidget: [ + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8.0), + border: Border.all(color: AppColor.primary, width: 1.0), + ), + padding: const EdgeInsets.all(8.0), + child: Text( + checkoutState.orderType.value, + style: TextStyle( + color: AppColor.primary, + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), + Container( + padding: const EdgeInsets.all(16.0).copyWith(bottom: 8), + decoration: const BoxDecoration( + border: Border( + bottom: BorderSide(color: AppColor.border, width: 1.0), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Item', + style: AppStyle.lg.copyWith( + color: AppColor.primary, + fontWeight: FontWeight.w600, + ), + ), + SpaceWidth(160), + 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, + ), + ), + ), + ], + ), + ), + Expanded( + child: ListView.separated( + shrinkWrap: true, + padding: const EdgeInsets.all(16.0).copyWith(top: 8.0), + itemBuilder: (context, index) => + OrderMenu(data: checkoutState.items[index]), + separatorBuilder: (context, index) => const SpaceHeight(12.0), + itemCount: checkoutState.items.length, + ), + ), + DashedDivider(color: AppColor.border), + Container( + padding: const EdgeInsets.all(16.0), + child: Column( + children: [ + if (checkoutState.delivery != null) ...[ + Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Pengiriman', + style: AppStyle.md.copyWith( + color: AppColor.black, + fontWeight: FontWeight.w600, + ), + ), + Text( + checkoutState.delivery?.name ?? '-', + style: AppStyle.md.copyWith( + color: AppColor.black, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + const SpaceHeight(8.0), + DashedDivider(color: AppColor.border), + const SpaceHeight(8.0), + ], + ), + ], + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Sub total', + style: AppStyle.md.copyWith( + color: AppColor.black, + fontWeight: FontWeight.w600, + ), + ), + Text( + price.currencyFormatRp, + style: AppStyle.md.copyWith( + color: AppColor.black, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + const SpaceHeight(8.0), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Pajak PB1', + style: AppStyle.md.copyWith( + color: AppColor.black, + fontWeight: FontWeight.w400, + ), + ), + Text( + '${checkoutState.tax}% (Rp. 0)', + style: AppStyle.md.copyWith( + color: AppColor.black, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + const SpaceHeight(8.0), + DashedDivider(color: AppColor.border), + const SpaceHeight(8.0), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Total', + style: AppStyle.xl.copyWith( + color: AppColor.black, + fontWeight: FontWeight.bold, + ), + ), + Text( + price.currencyFormatRp, + style: const TextStyle( + color: AppColor.primary, + fontWeight: FontWeight.bold, + fontSize: 18, + ), + ), + ], + ), + ], + ), + ), + ], + ); + } +} diff --git a/lib/presentation/pages/checkout/widgets/checkout_right_panel.dart b/lib/presentation/pages/checkout/widgets/checkout_right_panel.dart new file mode 100644 index 0000000..fca29f7 --- /dev/null +++ b/lib/presentation/pages/checkout/widgets/checkout_right_panel.dart @@ -0,0 +1,48 @@ +import 'package:flutter/widgets.dart'; + +import '../../../../common/theme/theme.dart'; +import '../../../components/field/field.dart'; +import '../../../components/page/page_title.dart'; + +class CheckoutRightPanel extends StatelessWidget { + const CheckoutRightPanel({super.key}); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + PageTitle( + title: 'Pembayaran', + isBack: false, + subtitle: 'Silahkan lakukan pembayaran', + ), + Expanded( + child: SingleChildScrollView( + child: Column( + children: [ + // Container( + // padding: const EdgeInsets.all(16), + // decoration: BoxDecoration( + // color: AppColor.white, + // border: Border( + // bottom: BorderSide(color: AppColor.border, width: 1.0), + // ), + // ), + // child: CustomerAutocomplete( + // controller: customerController, + // selectedCustomer: selectedCustomer, + // onSelected: (customer) { + // setState(() { + // selectedCustomer = customer; + // }); + // }, + // ), + // ), + ], + ), + ), + ), + ], + ); + } +} 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 3014835..4a4f30b 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,5 +1,6 @@ import 'dart:developer'; +import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -10,6 +11,7 @@ import '../../../../../components/button/button.dart'; import '../../../../../components/card/order_menu.dart'; import '../../../../../components/spaces/space.dart'; import '../../../../../components/toast/flushbar.dart'; +import '../../../../../router/app_router.gr.dart'; import 'home_right_title.dart'; class HomeRightPanel extends StatelessWidget { @@ -146,7 +148,8 @@ class HomeRightPanel extends StatelessWidget { elevation: 1, disabled: state.items.isEmpty, onPressed: () { - if (state.orderType.name == 'dineIn') { + if (state.orderType.name == 'dineIn' && + state.table == null) { AppFlushbar.showError( context, 'Mohon pilih meja terlebih dahulu', @@ -161,12 +164,7 @@ class HomeRightPanel extends StatelessWidget { ); return; } - // context.push( - // ConfirmPaymentPage( - // isTable: widget.table == null ? false : true, - // table: widget.table, - // ), - // ); + context.router.push(CheckoutRoute()); }, label: 'Lanjutkan Pembayaran', ), diff --git a/lib/presentation/router/app_router.dart b/lib/presentation/router/app_router.dart index 56181e6..78017e3 100644 --- a/lib/presentation/router/app_router.dart +++ b/lib/presentation/router/app_router.dart @@ -25,5 +25,8 @@ class AppRouter extends RootStackRouter { // Sync AutoRoute(page: SyncRoute.page), + + // Checkout + AutoRoute(page: CheckoutRoute.page), ]; } diff --git a/lib/presentation/router/app_router.gr.dart b/lib/presentation/router/app_router.gr.dart index 4be9313..a151859 100644 --- a/lib/presentation/router/app_router.gr.dart +++ b/lib/presentation/router/app_router.gr.dart @@ -10,165 +10,183 @@ // ignore_for_file: no_leading_underscores_for_library_prefixes import 'package:apskel_pos_flutter_v2/presentation/pages/auth/login/login_page.dart' - as _i3; -import 'package:apskel_pos_flutter_v2/presentation/pages/main/main_page.dart' as _i4; -import 'package:apskel_pos_flutter_v2/presentation/pages/main/pages/customer/customer_page.dart' +import 'package:apskel_pos_flutter_v2/presentation/pages/checkout/checkout_page.dart' as _i1; -import 'package:apskel_pos_flutter_v2/presentation/pages/main/pages/home/home_page.dart' - as _i2; -import 'package:apskel_pos_flutter_v2/presentation/pages/main/pages/report/report_page.dart' +import 'package:apskel_pos_flutter_v2/presentation/pages/main/main_page.dart' as _i5; -import 'package:apskel_pos_flutter_v2/presentation/pages/main/pages/setting/setting_page.dart' +import 'package:apskel_pos_flutter_v2/presentation/pages/main/pages/customer/customer_page.dart' + as _i2; +import 'package:apskel_pos_flutter_v2/presentation/pages/main/pages/home/home_page.dart' + as _i3; +import 'package:apskel_pos_flutter_v2/presentation/pages/main/pages/report/report_page.dart' as _i6; -import 'package:apskel_pos_flutter_v2/presentation/pages/main/pages/table/table_page.dart' - as _i9; -import 'package:apskel_pos_flutter_v2/presentation/pages/splash/splash_page.dart' +import 'package:apskel_pos_flutter_v2/presentation/pages/main/pages/setting/setting_page.dart' as _i7; -import 'package:apskel_pos_flutter_v2/presentation/pages/sync/sync_page.dart' +import 'package:apskel_pos_flutter_v2/presentation/pages/main/pages/table/table_page.dart' + as _i10; +import 'package:apskel_pos_flutter_v2/presentation/pages/splash/splash_page.dart' as _i8; -import 'package:auto_route/auto_route.dart' as _i10; +import 'package:apskel_pos_flutter_v2/presentation/pages/sync/sync_page.dart' + as _i9; +import 'package:auto_route/auto_route.dart' as _i11; /// generated route for -/// [_i1.CustomerPage] -class CustomerRoute extends _i10.PageRouteInfo { - const CustomerRoute({List<_i10.PageRouteInfo>? children}) +/// [_i1.CheckoutPage] +class CheckoutRoute extends _i11.PageRouteInfo { + const CheckoutRoute({List<_i11.PageRouteInfo>? children}) + : super(CheckoutRoute.name, initialChildren: children); + + static const String name = 'CheckoutRoute'; + + static _i11.PageInfo page = _i11.PageInfo( + name, + builder: (data) { + return const _i1.CheckoutPage(); + }, + ); +} + +/// generated route for +/// [_i2.CustomerPage] +class CustomerRoute extends _i11.PageRouteInfo { + const CustomerRoute({List<_i11.PageRouteInfo>? children}) : super(CustomerRoute.name, initialChildren: children); static const String name = 'CustomerRoute'; - static _i10.PageInfo page = _i10.PageInfo( + static _i11.PageInfo page = _i11.PageInfo( name, builder: (data) { - return _i10.WrappedRoute(child: const _i1.CustomerPage()); + return _i11.WrappedRoute(child: const _i2.CustomerPage()); }, ); } /// generated route for -/// [_i2.HomePage] -class HomeRoute extends _i10.PageRouteInfo { - const HomeRoute({List<_i10.PageRouteInfo>? children}) +/// [_i3.HomePage] +class HomeRoute extends _i11.PageRouteInfo { + const HomeRoute({List<_i11.PageRouteInfo>? children}) : super(HomeRoute.name, initialChildren: children); static const String name = 'HomeRoute'; - static _i10.PageInfo page = _i10.PageInfo( + static _i11.PageInfo page = _i11.PageInfo( name, builder: (data) { - return _i10.WrappedRoute(child: const _i2.HomePage()); + return _i11.WrappedRoute(child: const _i3.HomePage()); }, ); } /// generated route for -/// [_i3.LoginPage] -class LoginRoute extends _i10.PageRouteInfo { - const LoginRoute({List<_i10.PageRouteInfo>? children}) +/// [_i4.LoginPage] +class LoginRoute extends _i11.PageRouteInfo { + const LoginRoute({List<_i11.PageRouteInfo>? children}) : super(LoginRoute.name, initialChildren: children); static const String name = 'LoginRoute'; - static _i10.PageInfo page = _i10.PageInfo( + static _i11.PageInfo page = _i11.PageInfo( name, builder: (data) { - return _i10.WrappedRoute(child: const _i3.LoginPage()); + return _i11.WrappedRoute(child: const _i4.LoginPage()); }, ); } /// generated route for -/// [_i4.MainPage] -class MainRoute extends _i10.PageRouteInfo { - const MainRoute({List<_i10.PageRouteInfo>? children}) +/// [_i5.MainPage] +class MainRoute extends _i11.PageRouteInfo { + const MainRoute({List<_i11.PageRouteInfo>? children}) : super(MainRoute.name, initialChildren: children); static const String name = 'MainRoute'; - static _i10.PageInfo page = _i10.PageInfo( + static _i11.PageInfo page = _i11.PageInfo( name, builder: (data) { - return const _i4.MainPage(); + return const _i5.MainPage(); }, ); } /// generated route for -/// [_i5.ReportPage] -class ReportRoute extends _i10.PageRouteInfo { - const ReportRoute({List<_i10.PageRouteInfo>? children}) +/// [_i6.ReportPage] +class ReportRoute extends _i11.PageRouteInfo { + const ReportRoute({List<_i11.PageRouteInfo>? children}) : super(ReportRoute.name, initialChildren: children); static const String name = 'ReportRoute'; - static _i10.PageInfo page = _i10.PageInfo( + static _i11.PageInfo page = _i11.PageInfo( name, builder: (data) { - return const _i5.ReportPage(); + return const _i6.ReportPage(); }, ); } /// generated route for -/// [_i6.SettingPage] -class SettingRoute extends _i10.PageRouteInfo { - const SettingRoute({List<_i10.PageRouteInfo>? children}) +/// [_i7.SettingPage] +class SettingRoute extends _i11.PageRouteInfo { + const SettingRoute({List<_i11.PageRouteInfo>? children}) : super(SettingRoute.name, initialChildren: children); static const String name = 'SettingRoute'; - static _i10.PageInfo page = _i10.PageInfo( + static _i11.PageInfo page = _i11.PageInfo( name, builder: (data) { - return const _i6.SettingPage(); + return const _i7.SettingPage(); }, ); } /// generated route for -/// [_i7.SplashPage] -class SplashRoute extends _i10.PageRouteInfo { - const SplashRoute({List<_i10.PageRouteInfo>? children}) +/// [_i8.SplashPage] +class SplashRoute extends _i11.PageRouteInfo { + const SplashRoute({List<_i11.PageRouteInfo>? children}) : super(SplashRoute.name, initialChildren: children); static const String name = 'SplashRoute'; - static _i10.PageInfo page = _i10.PageInfo( + static _i11.PageInfo page = _i11.PageInfo( name, builder: (data) { - return const _i7.SplashPage(); + return const _i8.SplashPage(); }, ); } /// generated route for -/// [_i8.SyncPage] -class SyncRoute extends _i10.PageRouteInfo { - const SyncRoute({List<_i10.PageRouteInfo>? children}) +/// [_i9.SyncPage] +class SyncRoute extends _i11.PageRouteInfo { + const SyncRoute({List<_i11.PageRouteInfo>? children}) : super(SyncRoute.name, initialChildren: children); static const String name = 'SyncRoute'; - static _i10.PageInfo page = _i10.PageInfo( + static _i11.PageInfo page = _i11.PageInfo( name, builder: (data) { - return _i10.WrappedRoute(child: const _i8.SyncPage()); + return _i11.WrappedRoute(child: const _i9.SyncPage()); }, ); } /// generated route for -/// [_i9.TablePage] -class TableRoute extends _i10.PageRouteInfo { - const TableRoute({List<_i10.PageRouteInfo>? children}) +/// [_i10.TablePage] +class TableRoute extends _i11.PageRouteInfo { + const TableRoute({List<_i11.PageRouteInfo>? children}) : super(TableRoute.name, initialChildren: children); static const String name = 'TableRoute'; - static _i10.PageInfo page = _i10.PageInfo( + static _i11.PageInfo page = _i11.PageInfo( name, builder: (data) { - return _i10.WrappedRoute(child: const _i9.TablePage()); + return _i11.WrappedRoute(child: const _i10.TablePage()); }, ); }