diff --git a/lib/data/datasources/order_remote_datasource.dart b/lib/data/datasources/order_remote_datasource.dart index 2bc3fde..3acfee4 100644 --- a/lib/data/datasources/order_remote_datasource.dart +++ b/lib/data/datasources/order_remote_datasource.dart @@ -9,6 +9,7 @@ import 'package:enaklo_pos/data/datasources/auth_local_datasource.dart'; import 'package:enaklo_pos/data/models/request/payment_request.dart'; import 'package:enaklo_pos/data/models/response/order_response_model.dart'; import 'package:enaklo_pos/data/models/response/payment_method_response_model.dart'; +import 'package:enaklo_pos/data/models/response/payment_methods_response_model.dart'; import 'package:enaklo_pos/data/models/response/payment_response_model.dart'; import 'package:enaklo_pos/data/models/response/summary_response_model.dart'; import 'package:enaklo_pos/presentation/home/models/order_model.dart'; @@ -300,4 +301,58 @@ class OrderRemoteDatasource { return Left("Unexpected Error: $e"); } } + + Future> createOrderWithPayment( + OrderRequestModel orderModel, + PaymentMethod payment, + ) async { + final authData = await AuthLocalDataSource().getAuthData(); + final url = '${Variables.baseUrl}/api/v1/orders'; + + try { + final response = await dio.post( + url, + data: orderModel.toMap(), + options: Options( + headers: { + 'Authorization': 'Bearer ${authData.token}', + 'Accept': 'application/json', + 'Content-Type': 'application/json', + }, + ), + ); + + if (response.statusCode == 200) { + final data = OrderDetailResponseModel.fromMap(response.data); + final orderData = data.data; + final paymentRequest = PaymentRequestModel( + orderId: orderData?.id, + amount: orderData?.totalAmount, + paymentMethodId: payment.id, + splitDescription: '', + splitNumber: 1, + splitTotal: 1, + paymentOrderItems: orderData?.orderItems + ?.map((item) => PaymentOrderItemModel( + amount: item.totalPrice, + orderItemId: item.id, + )) + .toList(), + ); + + createPayment(paymentRequest); + return Right(data); + } else { + return const Left('Gagal membuat pesanan'); + } + } on DioException catch (e) { + final errorMessage = e.response?.data['message'] ?? 'Kesalahan jaringan'; + log("💥 Dio error: ${e.message}"); + log("💥 Dio response: ${e.response?.data}"); + return Left(errorMessage); + } catch (e) { + log("💥 Unexpected error: $e"); + return const Left('Terjadi kesalahan tak terduga'); + } + } } diff --git a/lib/presentation/home/bloc/order_form/order_form_bloc.dart b/lib/presentation/home/bloc/order_form/order_form_bloc.dart index 6d0e322..a032b8c 100644 --- a/lib/presentation/home/bloc/order_form/order_form_bloc.dart +++ b/lib/presentation/home/bloc/order_form/order_form_bloc.dart @@ -4,6 +4,7 @@ import 'package:bloc/bloc.dart'; import 'package:enaklo_pos/data/datasources/auth_local_datasource.dart'; import 'package:enaklo_pos/data/datasources/order_remote_datasource.dart'; import 'package:enaklo_pos/data/models/response/order_response_model.dart'; +import 'package:enaklo_pos/data/models/response/payment_methods_response_model.dart'; import 'package:enaklo_pos/presentation/home/models/order_request.dart'; import 'package:enaklo_pos/presentation/home/models/order_type.dart'; import 'package:enaklo_pos/presentation/home/models/product_quantity.dart'; @@ -46,6 +47,49 @@ class OrderFormBloc extends Bloc { final result = await _orderRemoteDatasource.createOrder(orderItems); + result.fold( + (error) => emit(_Error(error)), + (success) => emit(_Success(success.data!)), + ); + } catch (e) { + log("Error in AddOrderItemsBloc: $e"); + emit(_Error("Failed to add order items: $e")); + } + }, + ); + on<_CreateWithPaymentMethod>( + (event, emit) async { + emit(const _Loading()); + + try { + // Convert ProductQuantity list to the format expected by the API + + final userData = await AuthLocalDataSource().getAuthData(); + + final orderItems = OrderRequestModel( + customerName: event.customerName, + notes: '', + orderType: event.orderType.name, + tableNumber: event.tableNumber.toString(), + outletId: userData.user?.outletId, + userId: userData.user?.id, + orderItems: event.items + .map( + (productQuantity) => OrderItemRequest( + productId: productQuantity.product.id, + quantity: productQuantity.quantity, + notes: productQuantity.notes, + unitPrice: productQuantity.product.price, + ), + ) + .toList(), + ); + + final result = await _orderRemoteDatasource.createOrderWithPayment( + orderItems, + event.paymentMethod, + ); + result.fold( (error) => emit(_Error(error)), (success) => emit(_Success(success.data!)), diff --git a/lib/presentation/home/bloc/order_form/order_form_bloc.freezed.dart b/lib/presentation/home/bloc/order_form/order_form_bloc.freezed.dart index ebe3006..e9b4339 100644 --- a/lib/presentation/home/bloc/order_form/order_form_bloc.freezed.dart +++ b/lib/presentation/home/bloc/order_form/order_form_bloc.freezed.dart @@ -25,6 +25,13 @@ mixin _$OrderFormEvent { required TResult Function(List items, String customerName, OrderType orderType, String tableNumber) create, + required TResult Function( + List items, + String customerName, + OrderType orderType, + String tableNumber, + PaymentMethod paymentMethod) + createWithPayment, }) => throw _privateConstructorUsedError; @optionalTypeArgs @@ -32,6 +39,13 @@ mixin _$OrderFormEvent { TResult? Function(List items, String customerName, OrderType orderType, String tableNumber)? create, + TResult? Function( + List items, + String customerName, + OrderType orderType, + String tableNumber, + PaymentMethod paymentMethod)? + createWithPayment, }) => throw _privateConstructorUsedError; @optionalTypeArgs @@ -39,22 +53,32 @@ mixin _$OrderFormEvent { TResult Function(List items, String customerName, OrderType orderType, String tableNumber)? create, + TResult Function( + List items, + String customerName, + OrderType orderType, + String tableNumber, + PaymentMethod paymentMethod)? + createWithPayment, required TResult orElse(), }) => throw _privateConstructorUsedError; @optionalTypeArgs TResult map({ required TResult Function(_Create value) create, + required TResult Function(_CreateWithPaymentMethod value) createWithPayment, }) => throw _privateConstructorUsedError; @optionalTypeArgs TResult? mapOrNull({ TResult? Function(_Create value)? create, + TResult? Function(_CreateWithPaymentMethod value)? createWithPayment, }) => throw _privateConstructorUsedError; @optionalTypeArgs TResult maybeMap({ TResult Function(_Create value)? create, + TResult Function(_CreateWithPaymentMethod value)? createWithPayment, required TResult orElse(), }) => throw _privateConstructorUsedError; @@ -240,6 +264,13 @@ class _$CreateImpl implements _Create { required TResult Function(List items, String customerName, OrderType orderType, String tableNumber) create, + required TResult Function( + List items, + String customerName, + OrderType orderType, + String tableNumber, + PaymentMethod paymentMethod) + createWithPayment, }) { return create(items, customerName, orderType, tableNumber); } @@ -250,6 +281,13 @@ class _$CreateImpl implements _Create { TResult? Function(List items, String customerName, OrderType orderType, String tableNumber)? create, + TResult? Function( + List items, + String customerName, + OrderType orderType, + String tableNumber, + PaymentMethod paymentMethod)? + createWithPayment, }) { return create?.call(items, customerName, orderType, tableNumber); } @@ -260,6 +298,13 @@ class _$CreateImpl implements _Create { TResult Function(List items, String customerName, OrderType orderType, String tableNumber)? create, + TResult Function( + List items, + String customerName, + OrderType orderType, + String tableNumber, + PaymentMethod paymentMethod)? + createWithPayment, required TResult orElse(), }) { if (create != null) { @@ -272,6 +317,7 @@ class _$CreateImpl implements _Create { @optionalTypeArgs TResult map({ required TResult Function(_Create value) create, + required TResult Function(_CreateWithPaymentMethod value) createWithPayment, }) { return create(this); } @@ -280,6 +326,7 @@ class _$CreateImpl implements _Create { @optionalTypeArgs TResult? mapOrNull({ TResult? Function(_Create value)? create, + TResult? Function(_CreateWithPaymentMethod value)? createWithPayment, }) { return create?.call(this); } @@ -288,6 +335,7 @@ class _$CreateImpl implements _Create { @optionalTypeArgs TResult maybeMap({ TResult Function(_Create value)? create, + TResult Function(_CreateWithPaymentMethod value)? createWithPayment, required TResult orElse(), }) { if (create != null) { @@ -321,6 +369,252 @@ abstract class _Create implements OrderFormEvent { throw _privateConstructorUsedError; } +/// @nodoc +abstract class _$$CreateWithPaymentMethodImplCopyWith<$Res> + implements $OrderFormEventCopyWith<$Res> { + factory _$$CreateWithPaymentMethodImplCopyWith( + _$CreateWithPaymentMethodImpl value, + $Res Function(_$CreateWithPaymentMethodImpl) then) = + __$$CreateWithPaymentMethodImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {List items, + String customerName, + OrderType orderType, + String tableNumber, + PaymentMethod paymentMethod}); +} + +/// @nodoc +class __$$CreateWithPaymentMethodImplCopyWithImpl<$Res> + extends _$OrderFormEventCopyWithImpl<$Res, _$CreateWithPaymentMethodImpl> + implements _$$CreateWithPaymentMethodImplCopyWith<$Res> { + __$$CreateWithPaymentMethodImplCopyWithImpl( + _$CreateWithPaymentMethodImpl _value, + $Res Function(_$CreateWithPaymentMethodImpl) _then) + : super(_value, _then); + + /// Create a copy of OrderFormEvent + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? items = null, + Object? customerName = null, + Object? orderType = null, + Object? tableNumber = null, + Object? paymentMethod = freezed, + }) { + return _then(_$CreateWithPaymentMethodImpl( + items: null == items + ? _value._items + : items // ignore: cast_nullable_to_non_nullable + as List, + customerName: null == customerName + ? _value.customerName + : customerName // ignore: cast_nullable_to_non_nullable + as String, + orderType: null == orderType + ? _value.orderType + : orderType // ignore: cast_nullable_to_non_nullable + as OrderType, + tableNumber: null == tableNumber + ? _value.tableNumber + : tableNumber // ignore: cast_nullable_to_non_nullable + as String, + paymentMethod: freezed == paymentMethod + ? _value.paymentMethod + : paymentMethod // ignore: cast_nullable_to_non_nullable + as PaymentMethod, + )); + } +} + +/// @nodoc + +class _$CreateWithPaymentMethodImpl implements _CreateWithPaymentMethod { + const _$CreateWithPaymentMethodImpl( + {required final List items, + required this.customerName, + required this.orderType, + required this.tableNumber, + required this.paymentMethod}) + : _items = items; + + final List _items; + @override + List get items { + if (_items is EqualUnmodifiableListView) return _items; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_items); + } + + @override + final String customerName; + @override + final OrderType orderType; + @override + final String tableNumber; + @override + final PaymentMethod paymentMethod; + + @override + String toString() { + return 'OrderFormEvent.createWithPayment(items: $items, customerName: $customerName, orderType: $orderType, tableNumber: $tableNumber, paymentMethod: $paymentMethod)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$CreateWithPaymentMethodImpl && + const DeepCollectionEquality().equals(other._items, _items) && + (identical(other.customerName, customerName) || + other.customerName == customerName) && + (identical(other.orderType, orderType) || + other.orderType == orderType) && + (identical(other.tableNumber, tableNumber) || + other.tableNumber == tableNumber) && + const DeepCollectionEquality() + .equals(other.paymentMethod, paymentMethod)); + } + + @override + int get hashCode => Object.hash( + runtimeType, + const DeepCollectionEquality().hash(_items), + customerName, + orderType, + tableNumber, + const DeepCollectionEquality().hash(paymentMethod)); + + /// Create a copy of OrderFormEvent + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$CreateWithPaymentMethodImplCopyWith<_$CreateWithPaymentMethodImpl> + get copyWith => __$$CreateWithPaymentMethodImplCopyWithImpl< + _$CreateWithPaymentMethodImpl>(this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function(List items, String customerName, + OrderType orderType, String tableNumber) + create, + required TResult Function( + List items, + String customerName, + OrderType orderType, + String tableNumber, + PaymentMethod paymentMethod) + createWithPayment, + }) { + return createWithPayment( + items, customerName, orderType, tableNumber, paymentMethod); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(List items, String customerName, + OrderType orderType, String tableNumber)? + create, + TResult? Function( + List items, + String customerName, + OrderType orderType, + String tableNumber, + PaymentMethod paymentMethod)? + createWithPayment, + }) { + return createWithPayment?.call( + items, customerName, orderType, tableNumber, paymentMethod); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(List items, String customerName, + OrderType orderType, String tableNumber)? + create, + TResult Function( + List items, + String customerName, + OrderType orderType, + String tableNumber, + PaymentMethod paymentMethod)? + createWithPayment, + required TResult orElse(), + }) { + if (createWithPayment != null) { + return createWithPayment( + items, customerName, orderType, tableNumber, paymentMethod); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_Create value) create, + required TResult Function(_CreateWithPaymentMethod value) createWithPayment, + }) { + return createWithPayment(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_Create value)? create, + TResult? Function(_CreateWithPaymentMethod value)? createWithPayment, + }) { + return createWithPayment?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_Create value)? create, + TResult Function(_CreateWithPaymentMethod value)? createWithPayment, + required TResult orElse(), + }) { + if (createWithPayment != null) { + return createWithPayment(this); + } + return orElse(); + } +} + +abstract class _CreateWithPaymentMethod implements OrderFormEvent { + const factory _CreateWithPaymentMethod( + {required final List items, + required final String customerName, + required final OrderType orderType, + required final String tableNumber, + required final PaymentMethod paymentMethod}) = + _$CreateWithPaymentMethodImpl; + + @override + List get items; + @override + String get customerName; + @override + OrderType get orderType; + @override + String get tableNumber; + PaymentMethod get paymentMethod; + + /// Create a copy of OrderFormEvent + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$CreateWithPaymentMethodImplCopyWith<_$CreateWithPaymentMethodImpl> + get copyWith => throw _privateConstructorUsedError; +} + /// @nodoc mixin _$OrderFormState { @optionalTypeArgs diff --git a/lib/presentation/home/bloc/order_form/order_form_event.dart b/lib/presentation/home/bloc/order_form/order_form_event.dart index b55cf25..d6f0731 100644 --- a/lib/presentation/home/bloc/order_form/order_form_event.dart +++ b/lib/presentation/home/bloc/order_form/order_form_event.dart @@ -8,4 +8,11 @@ class OrderFormEvent with _$OrderFormEvent { required OrderType orderType, required String tableNumber, }) = _Create; + const factory OrderFormEvent.createWithPayment({ + required List items, + required String customerName, + required OrderType orderType, + required String tableNumber, + required PaymentMethod paymentMethod, + }) = _CreateWithPaymentMethod; } diff --git a/lib/presentation/home/pages/confirm_payment_page.dart b/lib/presentation/home/pages/confirm_payment_page.dart index 7564627..cef81ea 100644 --- a/lib/presentation/home/pages/confirm_payment_page.dart +++ b/lib/presentation/home/pages/confirm_payment_page.dart @@ -2,11 +2,14 @@ import 'dart:developer'; import 'package:enaklo_pos/core/components/dashed_divider.dart'; +import 'package:enaklo_pos/core/components/flushbar.dart'; import 'package:enaklo_pos/core/extensions/build_context_ext.dart'; +import 'package:enaklo_pos/presentation/home/bloc/order_form/order_form_bloc.dart'; import 'package:enaklo_pos/presentation/home/dialog/save_dialog.dart'; import 'package:enaklo_pos/presentation/home/models/order_type.dart'; import 'package:enaklo_pos/presentation/home/models/product_quantity.dart'; import 'package:enaklo_pos/presentation/home/widgets/confirm_payment_title.dart'; +import 'package:enaklo_pos/presentation/success/pages/success_order_page.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -895,10 +898,50 @@ class _ConfirmPaymentPageState extends State { ), ), SpaceWidth(12), - Expanded( - child: Button.filled( - onPressed: () {}, - label: 'Bayar', + BlocListener( + listener: (lcontext, state) { + state.maybeWhen( + orElse: () {}, + success: (data) { + context + .pushReplacement(SuccessOrderPage( + order: data, + )); + }, + error: (message) => AppFlushbar.showError( + context, + message, + ), + ); + }, + child: BlocBuilder( + builder: (context, state) { + return Expanded( + child: state.maybeMap( + orElse: () => Button.filled( + onPressed: () { + context.read().add( + OrderFormEvent.create( + items: items, + customerName: + customerController + .text, + orderType: orderType, + tableNumber: widget.table + ?.tableName ?? + '', + ), + ); + }, + label: 'Bayar', + ), + loading: (_) => Center( + child: CircularProgressIndicator(), + ), + ), + ); + }, ), ), ], diff --git a/lib/presentation/sales/blocs/payment_form/payment_form_bloc.freezed.dart b/lib/presentation/sales/blocs/payment_form/payment_form_bloc.freezed.dart index a43f786..77ad755 100644 --- a/lib/presentation/sales/blocs/payment_form/payment_form_bloc.freezed.dart +++ b/lib/presentation/sales/blocs/payment_form/payment_form_bloc.freezed.dart @@ -561,10 +561,10 @@ class __$$SuccessImplCopyWithImpl<$Res> @pragma('vm:prefer-inline') @override $Res call({ - Object? data = freezed, + Object? data = null, }) { return _then(_$SuccessImpl( - freezed == data + null == data ? _value.data : data // ignore: cast_nullable_to_non_nullable as PaymentData, @@ -590,12 +590,11 @@ class _$SuccessImpl implements _Success { return identical(this, other) || (other.runtimeType == runtimeType && other is _$SuccessImpl && - const DeepCollectionEquality().equals(other.data, data)); + (identical(other.data, data) || other.data == data)); } @override - int get hashCode => - Object.hash(runtimeType, const DeepCollectionEquality().hash(data)); + int get hashCode => Object.hash(runtimeType, data); /// Create a copy of PaymentFormState /// with the given fields replaced by the non-null parameter values. diff --git a/lib/presentation/success/pages/success_order_page.dart b/lib/presentation/success/pages/success_order_page.dart new file mode 100644 index 0000000..028c4ce --- /dev/null +++ b/lib/presentation/success/pages/success_order_page.dart @@ -0,0 +1,185 @@ +import 'package:enaklo_pos/core/components/components.dart'; +import 'package:enaklo_pos/core/components/dashed_divider.dart'; +import 'package:enaklo_pos/core/constants/colors.dart'; +import 'package:enaklo_pos/core/extensions/build_context_ext.dart'; +import 'package:enaklo_pos/core/extensions/date_time_ext.dart'; +import 'package:enaklo_pos/core/extensions/string_ext.dart'; +import 'package:enaklo_pos/data/models/response/order_response_model.dart'; +import 'package:enaklo_pos/presentation/home/pages/dashboard_page.dart'; +import 'package:flutter/material.dart'; + +class SuccessOrderPage extends StatelessWidget { + final Order order; + const SuccessOrderPage({super.key, required this.order}); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: AppColors.background, + body: Center( + child: Container( + width: context.deviceWidth * 0.4, + height: context.deviceHeight * 0.8, + decoration: BoxDecoration( + color: AppColors.white, + borderRadius: const BorderRadius.all(Radius.circular(12.0)), + ), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + children: [ + Text( + 'Pesanan!', + style: const TextStyle( + fontSize: 18, fontWeight: FontWeight.bold), + ), + Text('Pesanan berhasil ', + style: const TextStyle(fontSize: 14)), + ], + ), + ), + DashedDivider( + color: AppColors.grey, + ), + SpaceHeight(24), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Text( + order.metadata?['customer_name'] ?? "-", + style: const TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: AppColors.primary, + ), + textAlign: TextAlign.center, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), + Padding( + padding: const EdgeInsets.all(16.0).copyWith(top: 24), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'No. Pesanan', + ), + Text( + order.orderNumber ?? "-", + style: const TextStyle(fontWeight: FontWeight.bold), + ), + ], + ), + SpaceHeight(4), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'No. Meja', + ), + Text( + order.tableNumber ?? "-", + style: const TextStyle(fontWeight: FontWeight.bold), + ), + ], + ), + SpaceHeight(4), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Waktu', + ), + Text( + (order.createdAt ?? DateTime.now()) + .toFormattedDate3(), + style: const TextStyle(fontWeight: FontWeight.bold), + ), + ], + ), + ], + ), + ), + Spacer(), + DashedDivider( + color: AppColors.grey, + ), + Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Status Pembayaran', + ), + Text( + 'Lunas', + style: const TextStyle( + fontWeight: FontWeight.bold, + color: Colors.green, + ), + ), + ], + ), + ), + DashedDivider( + color: AppColors.grey, + ), + Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Total Pembayaran', + ), + Text( + (order.totalAmount ?? 0).toString().currencyFormatRpV2, + style: const TextStyle(fontWeight: FontWeight.bold), + ), + ], + ), + ), + DashedDivider( + color: AppColors.grey, + ), + Spacer(), + Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + children: [ + Expanded( + child: Button.outlined( + onPressed: () { + context.push(DashboardPage()); + }, + label: 'Kembali', + height: 44, + ), + ), + SpaceWidth(12), + Expanded( + child: Button.filled( + onPressed: () {}, + label: 'Cetak', + icon: Icon( + Icons.print, + color: AppColors.white, + ), + height: 44, + ), + ), + ], + ), + ), + ], + ), + ), + ), + ); + } +}