From 87a62a287a487a2e6d3e7b168f5e42754495fdf8 Mon Sep 17 00:00:00 2001 From: efrilm Date: Sun, 3 Aug 2025 12:26:47 +0700 Subject: [PATCH] feat: save order --- .../datasources/order_remote_datasource.dart | 65 +- lib/main.dart | 4 + .../home/bloc/order_form/order_form_bloc.dart | 59 ++ .../order_form/order_form_bloc.freezed.dart | 900 ++++++++++++++++++ .../bloc/order_form/order_form_event.dart | 11 + .../bloc/order_form/order_form_state.dart | 9 + .../home/dialog/payment_save_dialog.dart | 34 +- lib/presentation/home/dialog/save_dialog.dart | 21 +- .../home/models/order_request.dart | 84 ++ .../home/pages/confirm_payment_page.dart | 199 ++-- lib/presentation/home/pages/home_page.dart | 3 +- .../home/widgets/home_right_title.dart | 2 +- .../table/widgets/table_widget.dart | 66 +- 13 files changed, 1340 insertions(+), 117 deletions(-) create mode 100644 lib/presentation/home/bloc/order_form/order_form_bloc.dart create mode 100644 lib/presentation/home/bloc/order_form/order_form_bloc.freezed.dart create mode 100644 lib/presentation/home/bloc/order_form/order_form_event.dart create mode 100644 lib/presentation/home/bloc/order_form/order_form_state.dart create mode 100644 lib/presentation/home/models/order_request.dart diff --git a/lib/data/datasources/order_remote_datasource.dart b/lib/data/datasources/order_remote_datasource.dart index 22a85d7..b083b0a 100644 --- a/lib/data/datasources/order_remote_datasource.dart +++ b/lib/data/datasources/order_remote_datasource.dart @@ -1,17 +1,21 @@ +import 'dart:convert'; import 'dart:developer'; -import 'dart:convert'; - import 'package:dartz/dartz.dart'; +import 'package:dio/dio.dart'; import 'package:enaklo_pos/core/constants/variables.dart'; +import 'package:enaklo_pos/core/network/dio_client.dart'; import 'package:enaklo_pos/data/datasources/auth_local_datasource.dart'; import 'package:enaklo_pos/data/models/response/order_remote_datasource.dart'; import 'package:enaklo_pos/data/models/response/payment_method_response_model.dart'; import 'package:enaklo_pos/data/models/response/summary_response_model.dart'; import 'package:enaklo_pos/presentation/home/models/order_model.dart'; +import 'package:enaklo_pos/presentation/home/models/order_request.dart'; import 'package:http/http.dart' as http; class OrderRemoteDatasource { + final Dio dio = DioClient.instance; + //save order to remote server Future saveOrder(OrderModel orderModel) async { final authData = await AuthLocalDataSource().getAuthData(); @@ -28,11 +32,11 @@ class OrderRemoteDatasource { 'Content-Type': 'application/json', }, ); - + print("📥 HTTP Status Code: ${response.statusCode}"); print("📥 Response Body: ${response.body}"); print("📥 Response Headers: ${response.headers}"); - + if (response.statusCode == 200) { print("✅ API call successful - Order saved to server"); return true; @@ -69,7 +73,8 @@ class OrderRemoteDatasource { print("✅ getOrderByRangeDate API call successful"); return Right(OrderResponseModel.fromJson(response.body)); } else { - print("❌ getOrderByRangeDate API call failed - Status: ${response.statusCode}"); + print( + "❌ getOrderByRangeDate API call failed - Status: ${response.statusCode}"); print("❌ Error Response: ${response.body}"); return const Left("Failed Load Data"); } @@ -102,7 +107,8 @@ class OrderRemoteDatasource { print("✅ getSummaryByRangeDate API call successful"); return Right(SummaryResponseModel.fromJson(response.body)); } else { - print("❌ getSummaryByRangeDate API call failed - Status: ${response.statusCode}"); + print( + "❌ getSummaryByRangeDate API call failed - Status: ${response.statusCode}"); print("❌ Error Response: ${response.body}"); return const Left("Failed Load Data"); } @@ -112,7 +118,8 @@ class OrderRemoteDatasource { } } - Future> getPaymentMethodByRangeDate( + Future> + getPaymentMethodByRangeDate( String startDate, String endDate, ) async { @@ -134,7 +141,8 @@ class OrderRemoteDatasource { print("✅ getPaymentMethodByRangeDate API call successful"); return Right(PaymentMethodResponseModel.fromJson(response.body)); } else { - print("❌ getPaymentMethodByRangeDate API call failed - Status: ${response.statusCode}"); + print( + "❌ getPaymentMethodByRangeDate API call failed - Status: ${response.statusCode}"); print("❌ Error Response: ${response.body}"); return const Left("Failed Load Payment Method Data"); } @@ -162,16 +170,17 @@ class OrderRemoteDatasource { 'order_items': orderItems, }), ); - + print("📥 Add Order Items HTTP Status Code: ${response.statusCode}"); print("📥 Add Order Items Response Body: ${response.body}"); print("📥 Add Order Items Response Headers: ${response.headers}"); - + if (response.statusCode == 200) { print("✅ addOrderItems API call successful"); return const Right(true); } else { - print("❌ addOrderItems API call failed - Status: ${response.statusCode}"); + print( + "❌ addOrderItems API call failed - Status: ${response.statusCode}"); print("❌ Error Response: ${response.body}"); return Left("Failed to add order items: ${response.body}"); } @@ -180,4 +189,38 @@ class OrderRemoteDatasource { return Left("Failed: $e"); } } + + // New Api + Future> createOrder(OrderRequestModel orderModel) 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) { + return const Right(true); + } 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/main.dart b/lib/main.dart index e597a80..7ff14d5 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,5 +1,6 @@ import 'dart:developer'; import 'package:enaklo_pos/core/constants/theme.dart'; +import 'package:enaklo_pos/presentation/home/bloc/order_form/order_form_bloc.dart'; import 'package:enaklo_pos/presentation/home/bloc/product_loader/product_loader_bloc.dart'; import 'package:flutter/material.dart'; import 'package:enaklo_pos/data/datasources/auth_local_datasource.dart'; @@ -221,6 +222,9 @@ class _MyAppState extends State { BlocProvider( create: (context) => ProductLoaderBloc(ProductRemoteDatasource()), ), + BlocProvider( + create: (context) => OrderFormBloc(OrderRemoteDatasource()), + ), ], child: MaterialApp( debugShowCheckedModeBanner: false, diff --git a/lib/presentation/home/bloc/order_form/order_form_bloc.dart b/lib/presentation/home/bloc/order_form/order_form_bloc.dart new file mode 100644 index 0000000..5825b26 --- /dev/null +++ b/lib/presentation/home/bloc/order_form/order_form_bloc.dart @@ -0,0 +1,59 @@ +import 'dart:developer'; + +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/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'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'order_form_event.dart'; +part 'order_form_state.dart'; +part 'order_form_bloc.freezed.dart'; + +class OrderFormBloc extends Bloc { + final OrderRemoteDatasource _orderRemoteDatasource; + OrderFormBloc(this._orderRemoteDatasource) : super(OrderFormState.initial()) { + on<_Create>( + (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.createOrder(orderItems); + + result.fold( + (error) => emit(_Error(error)), + (success) => emit(const _Success()), + ); + } catch (e) { + log("Error in AddOrderItemsBloc: $e"); + emit(_Error("Failed to add order items: $e")); + } + }, + ); + } +} 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 new file mode 100644 index 0000000..b63898c --- /dev/null +++ b/lib/presentation/home/bloc/order_form/order_form_bloc.freezed.dart @@ -0,0 +1,900 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'order_form_bloc.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +/// @nodoc +mixin _$OrderFormEvent { + List get items => throw _privateConstructorUsedError; + String get customerName => throw _privateConstructorUsedError; + OrderType get orderType => throw _privateConstructorUsedError; + String get tableNumber => throw _privateConstructorUsedError; + @optionalTypeArgs + TResult when({ + required TResult Function(List items, String customerName, + OrderType orderType, String tableNumber) + create, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(List items, String customerName, + OrderType orderType, String tableNumber)? + create, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(List items, String customerName, + OrderType orderType, String tableNumber)? + create, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult map({ + required TResult Function(_Create value) create, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_Create value)? create, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_Create value)? create, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; + + /// Create a copy of OrderFormEvent + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $OrderFormEventCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $OrderFormEventCopyWith<$Res> { + factory $OrderFormEventCopyWith( + OrderFormEvent value, $Res Function(OrderFormEvent) then) = + _$OrderFormEventCopyWithImpl<$Res, OrderFormEvent>; + @useResult + $Res call( + {List items, + String customerName, + OrderType orderType, + String tableNumber}); +} + +/// @nodoc +class _$OrderFormEventCopyWithImpl<$Res, $Val extends OrderFormEvent> + implements $OrderFormEventCopyWith<$Res> { + _$OrderFormEventCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _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, + }) { + return _then(_value.copyWith( + 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, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$CreateImplCopyWith<$Res> + implements $OrderFormEventCopyWith<$Res> { + factory _$$CreateImplCopyWith( + _$CreateImpl value, $Res Function(_$CreateImpl) then) = + __$$CreateImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {List items, + String customerName, + OrderType orderType, + String tableNumber}); +} + +/// @nodoc +class __$$CreateImplCopyWithImpl<$Res> + extends _$OrderFormEventCopyWithImpl<$Res, _$CreateImpl> + implements _$$CreateImplCopyWith<$Res> { + __$$CreateImplCopyWithImpl( + _$CreateImpl _value, $Res Function(_$CreateImpl) _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, + }) { + return _then(_$CreateImpl( + 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, + )); + } +} + +/// @nodoc + +class _$CreateImpl implements _Create { + const _$CreateImpl( + {required final List items, + required this.customerName, + required this.orderType, + required this.tableNumber}) + : _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 + String toString() { + return 'OrderFormEvent.create(items: $items, customerName: $customerName, orderType: $orderType, tableNumber: $tableNumber)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$CreateImpl && + 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)); + } + + @override + int get hashCode => Object.hash( + runtimeType, + const DeepCollectionEquality().hash(_items), + customerName, + orderType, + tableNumber); + + /// 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') + _$$CreateImplCopyWith<_$CreateImpl> get copyWith => + __$$CreateImplCopyWithImpl<_$CreateImpl>(this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function(List items, String customerName, + OrderType orderType, String tableNumber) + create, + }) { + return create(items, customerName, orderType, tableNumber); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(List items, String customerName, + OrderType orderType, String tableNumber)? + create, + }) { + return create?.call(items, customerName, orderType, tableNumber); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(List items, String customerName, + OrderType orderType, String tableNumber)? + create, + required TResult orElse(), + }) { + if (create != null) { + return create(items, customerName, orderType, tableNumber); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_Create value) create, + }) { + return create(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_Create value)? create, + }) { + return create?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_Create value)? create, + required TResult orElse(), + }) { + if (create != null) { + return create(this); + } + return orElse(); + } +} + +abstract class _Create implements OrderFormEvent { + const factory _Create( + {required final List items, + required final String customerName, + required final OrderType orderType, + required final String tableNumber}) = _$CreateImpl; + + @override + List get items; + @override + String get customerName; + @override + OrderType get orderType; + @override + String get tableNumber; + + /// Create a copy of OrderFormEvent + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$CreateImplCopyWith<_$CreateImpl> get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +mixin _$OrderFormState { + @optionalTypeArgs + TResult when({ + required TResult Function() initial, + required TResult Function() loading, + required TResult Function() success, + required TResult Function(String message) error, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? initial, + TResult? Function()? loading, + TResult? Function()? success, + TResult? Function(String message)? error, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? initial, + TResult Function()? loading, + TResult Function()? success, + TResult Function(String message)? error, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult map({ + required TResult Function(_Initial value) initial, + required TResult Function(_Loading value) loading, + required TResult Function(_Success value) success, + required TResult Function(_Error value) error, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_Initial value)? initial, + TResult? Function(_Loading value)? loading, + TResult? Function(_Success value)? success, + TResult? Function(_Error value)? error, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_Initial value)? initial, + TResult Function(_Loading value)? loading, + TResult Function(_Success value)? success, + TResult Function(_Error value)? error, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $OrderFormStateCopyWith<$Res> { + factory $OrderFormStateCopyWith( + OrderFormState value, $Res Function(OrderFormState) then) = + _$OrderFormStateCopyWithImpl<$Res, OrderFormState>; +} + +/// @nodoc +class _$OrderFormStateCopyWithImpl<$Res, $Val extends OrderFormState> + implements $OrderFormStateCopyWith<$Res> { + _$OrderFormStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of OrderFormState + /// with the given fields replaced by the non-null parameter values. +} + +/// @nodoc +abstract class _$$InitialImplCopyWith<$Res> { + factory _$$InitialImplCopyWith( + _$InitialImpl value, $Res Function(_$InitialImpl) then) = + __$$InitialImplCopyWithImpl<$Res>; +} + +/// @nodoc +class __$$InitialImplCopyWithImpl<$Res> + extends _$OrderFormStateCopyWithImpl<$Res, _$InitialImpl> + implements _$$InitialImplCopyWith<$Res> { + __$$InitialImplCopyWithImpl( + _$InitialImpl _value, $Res Function(_$InitialImpl) _then) + : super(_value, _then); + + /// Create a copy of OrderFormState + /// with the given fields replaced by the non-null parameter values. +} + +/// @nodoc + +class _$InitialImpl implements _Initial { + const _$InitialImpl(); + + @override + String toString() { + return 'OrderFormState.initial()'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && other is _$InitialImpl); + } + + @override + int get hashCode => runtimeType.hashCode; + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() initial, + required TResult Function() loading, + required TResult Function() success, + required TResult Function(String message) error, + }) { + return initial(); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? initial, + TResult? Function()? loading, + TResult? Function()? success, + TResult? Function(String message)? error, + }) { + return initial?.call(); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? initial, + TResult Function()? loading, + TResult Function()? success, + TResult Function(String message)? error, + required TResult orElse(), + }) { + if (initial != null) { + return initial(); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_Initial value) initial, + required TResult Function(_Loading value) loading, + required TResult Function(_Success value) success, + required TResult Function(_Error value) error, + }) { + return initial(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_Initial value)? initial, + TResult? Function(_Loading value)? loading, + TResult? Function(_Success value)? success, + TResult? Function(_Error value)? error, + }) { + return initial?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_Initial value)? initial, + TResult Function(_Loading value)? loading, + TResult Function(_Success value)? success, + TResult Function(_Error value)? error, + required TResult orElse(), + }) { + if (initial != null) { + return initial(this); + } + return orElse(); + } +} + +abstract class _Initial implements OrderFormState { + const factory _Initial() = _$InitialImpl; +} + +/// @nodoc +abstract class _$$LoadingImplCopyWith<$Res> { + factory _$$LoadingImplCopyWith( + _$LoadingImpl value, $Res Function(_$LoadingImpl) then) = + __$$LoadingImplCopyWithImpl<$Res>; +} + +/// @nodoc +class __$$LoadingImplCopyWithImpl<$Res> + extends _$OrderFormStateCopyWithImpl<$Res, _$LoadingImpl> + implements _$$LoadingImplCopyWith<$Res> { + __$$LoadingImplCopyWithImpl( + _$LoadingImpl _value, $Res Function(_$LoadingImpl) _then) + : super(_value, _then); + + /// Create a copy of OrderFormState + /// with the given fields replaced by the non-null parameter values. +} + +/// @nodoc + +class _$LoadingImpl implements _Loading { + const _$LoadingImpl(); + + @override + String toString() { + return 'OrderFormState.loading()'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && other is _$LoadingImpl); + } + + @override + int get hashCode => runtimeType.hashCode; + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() initial, + required TResult Function() loading, + required TResult Function() success, + required TResult Function(String message) error, + }) { + return loading(); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? initial, + TResult? Function()? loading, + TResult? Function()? success, + TResult? Function(String message)? error, + }) { + return loading?.call(); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? initial, + TResult Function()? loading, + TResult Function()? success, + TResult Function(String message)? error, + required TResult orElse(), + }) { + if (loading != null) { + return loading(); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_Initial value) initial, + required TResult Function(_Loading value) loading, + required TResult Function(_Success value) success, + required TResult Function(_Error value) error, + }) { + return loading(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_Initial value)? initial, + TResult? Function(_Loading value)? loading, + TResult? Function(_Success value)? success, + TResult? Function(_Error value)? error, + }) { + return loading?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_Initial value)? initial, + TResult Function(_Loading value)? loading, + TResult Function(_Success value)? success, + TResult Function(_Error value)? error, + required TResult orElse(), + }) { + if (loading != null) { + return loading(this); + } + return orElse(); + } +} + +abstract class _Loading implements OrderFormState { + const factory _Loading() = _$LoadingImpl; +} + +/// @nodoc +abstract class _$$SuccessImplCopyWith<$Res> { + factory _$$SuccessImplCopyWith( + _$SuccessImpl value, $Res Function(_$SuccessImpl) then) = + __$$SuccessImplCopyWithImpl<$Res>; +} + +/// @nodoc +class __$$SuccessImplCopyWithImpl<$Res> + extends _$OrderFormStateCopyWithImpl<$Res, _$SuccessImpl> + implements _$$SuccessImplCopyWith<$Res> { + __$$SuccessImplCopyWithImpl( + _$SuccessImpl _value, $Res Function(_$SuccessImpl) _then) + : super(_value, _then); + + /// Create a copy of OrderFormState + /// with the given fields replaced by the non-null parameter values. +} + +/// @nodoc + +class _$SuccessImpl implements _Success { + const _$SuccessImpl(); + + @override + String toString() { + return 'OrderFormState.success()'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && other is _$SuccessImpl); + } + + @override + int get hashCode => runtimeType.hashCode; + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() initial, + required TResult Function() loading, + required TResult Function() success, + required TResult Function(String message) error, + }) { + return success(); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? initial, + TResult? Function()? loading, + TResult? Function()? success, + TResult? Function(String message)? error, + }) { + return success?.call(); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? initial, + TResult Function()? loading, + TResult Function()? success, + TResult Function(String message)? error, + required TResult orElse(), + }) { + if (success != null) { + return success(); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_Initial value) initial, + required TResult Function(_Loading value) loading, + required TResult Function(_Success value) success, + required TResult Function(_Error value) error, + }) { + return success(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_Initial value)? initial, + TResult? Function(_Loading value)? loading, + TResult? Function(_Success value)? success, + TResult? Function(_Error value)? error, + }) { + return success?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_Initial value)? initial, + TResult Function(_Loading value)? loading, + TResult Function(_Success value)? success, + TResult Function(_Error value)? error, + required TResult orElse(), + }) { + if (success != null) { + return success(this); + } + return orElse(); + } +} + +abstract class _Success implements OrderFormState { + const factory _Success() = _$SuccessImpl; +} + +/// @nodoc +abstract class _$$ErrorImplCopyWith<$Res> { + factory _$$ErrorImplCopyWith( + _$ErrorImpl value, $Res Function(_$ErrorImpl) then) = + __$$ErrorImplCopyWithImpl<$Res>; + @useResult + $Res call({String message}); +} + +/// @nodoc +class __$$ErrorImplCopyWithImpl<$Res> + extends _$OrderFormStateCopyWithImpl<$Res, _$ErrorImpl> + implements _$$ErrorImplCopyWith<$Res> { + __$$ErrorImplCopyWithImpl( + _$ErrorImpl _value, $Res Function(_$ErrorImpl) _then) + : super(_value, _then); + + /// Create a copy of OrderFormState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? message = null, + }) { + return _then(_$ErrorImpl( + null == message + ? _value.message + : message // ignore: cast_nullable_to_non_nullable + as String, + )); + } +} + +/// @nodoc + +class _$ErrorImpl implements _Error { + const _$ErrorImpl(this.message); + + @override + final String message; + + @override + String toString() { + return 'OrderFormState.error(message: $message)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$ErrorImpl && + (identical(other.message, message) || other.message == message)); + } + + @override + int get hashCode => Object.hash(runtimeType, message); + + /// Create a copy of OrderFormState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$ErrorImplCopyWith<_$ErrorImpl> get copyWith => + __$$ErrorImplCopyWithImpl<_$ErrorImpl>(this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() initial, + required TResult Function() loading, + required TResult Function() success, + required TResult Function(String message) error, + }) { + return error(message); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? initial, + TResult? Function()? loading, + TResult? Function()? success, + TResult? Function(String message)? error, + }) { + return error?.call(message); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? initial, + TResult Function()? loading, + TResult Function()? success, + TResult Function(String message)? error, + required TResult orElse(), + }) { + if (error != null) { + return error(message); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_Initial value) initial, + required TResult Function(_Loading value) loading, + required TResult Function(_Success value) success, + required TResult Function(_Error value) error, + }) { + return error(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_Initial value)? initial, + TResult? Function(_Loading value)? loading, + TResult? Function(_Success value)? success, + TResult? Function(_Error value)? error, + }) { + return error?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_Initial value)? initial, + TResult Function(_Loading value)? loading, + TResult Function(_Success value)? success, + TResult Function(_Error value)? error, + required TResult orElse(), + }) { + if (error != null) { + return error(this); + } + return orElse(); + } +} + +abstract class _Error implements OrderFormState { + const factory _Error(final String message) = _$ErrorImpl; + + String get message; + + /// Create a copy of OrderFormState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + _$$ErrorImplCopyWith<_$ErrorImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/presentation/home/bloc/order_form/order_form_event.dart b/lib/presentation/home/bloc/order_form/order_form_event.dart new file mode 100644 index 0000000..b55cf25 --- /dev/null +++ b/lib/presentation/home/bloc/order_form/order_form_event.dart @@ -0,0 +1,11 @@ +part of 'order_form_bloc.dart'; + +@freezed +class OrderFormEvent with _$OrderFormEvent { + const factory OrderFormEvent.create({ + required List items, + required String customerName, + required OrderType orderType, + required String tableNumber, + }) = _Create; +} diff --git a/lib/presentation/home/bloc/order_form/order_form_state.dart b/lib/presentation/home/bloc/order_form/order_form_state.dart new file mode 100644 index 0000000..ddea79e --- /dev/null +++ b/lib/presentation/home/bloc/order_form/order_form_state.dart @@ -0,0 +1,9 @@ +part of 'order_form_bloc.dart'; + +@freezed +class OrderFormState with _$OrderFormState { + const factory OrderFormState.initial() = _Initial; + const factory OrderFormState.loading() = _Loading; + const factory OrderFormState.success() = _Success; + const factory OrderFormState.error(String message) = _Error; +} diff --git a/lib/presentation/home/dialog/payment_save_dialog.dart b/lib/presentation/home/dialog/payment_save_dialog.dart index 423dd48..c7fc5ee 100644 --- a/lib/presentation/home/dialog/payment_save_dialog.dart +++ b/lib/presentation/home/dialog/payment_save_dialog.dart @@ -5,11 +5,23 @@ import 'package:enaklo_pos/core/constants/colors.dart'; import 'package:enaklo_pos/core/extensions/build_context_ext.dart'; import 'package:enaklo_pos/data/models/response/table_model.dart'; import 'package:enaklo_pos/presentation/home/bloc/get_table_status/get_table_status_bloc.dart'; +import 'package:enaklo_pos/presentation/home/bloc/order_form/order_form_bloc.dart'; +import 'package:enaklo_pos/presentation/home/models/order_type.dart'; +import 'package:enaklo_pos/presentation/home/models/product_quantity.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class PaymentSaveDialog extends StatefulWidget { - const PaymentSaveDialog({super.key}); + final TableModel? selectedTable; + final String customerName; + final OrderType orderType; + final List items; + const PaymentSaveDialog( + {super.key, + required this.selectedTable, + required this.customerName, + required this.orderType, + required this.items}); @override State createState() => _PaymentSaveDialogState(); @@ -17,6 +29,15 @@ class PaymentSaveDialog extends StatefulWidget { class _PaymentSaveDialogState extends State { TableModel? selectTable; + + @override + void initState() { + super.initState(); + if (widget.selectedTable != null) { + selectTable = widget.selectedTable; + } + } + @override Widget build(BuildContext context) { return CustomModalDialog( @@ -112,7 +133,16 @@ class _PaymentSaveDialogState extends State { ), SpaceHeight(24), Button.filled( - onPressed: () {}, + onPressed: () { + context.read().add( + OrderFormEvent.create( + items: widget.items, + customerName: widget.customerName, + orderType: widget.orderType, + tableNumber: selectTable!.tableName.toString(), + ), + ); + }, label: "Simpan", ), ], diff --git a/lib/presentation/home/dialog/save_dialog.dart b/lib/presentation/home/dialog/save_dialog.dart index aefd6e0..99aab06 100644 --- a/lib/presentation/home/dialog/save_dialog.dart +++ b/lib/presentation/home/dialog/save_dialog.dart @@ -1,12 +1,25 @@ import 'package:enaklo_pos/core/components/custom_modal_dialog.dart'; import 'package:enaklo_pos/core/components/spaces.dart'; import 'package:enaklo_pos/core/constants/colors.dart'; +import 'package:enaklo_pos/data/models/response/table_model.dart'; import 'package:enaklo_pos/presentation/home/dialog/payment_add_order_dialog.dart'; import 'package:enaklo_pos/presentation/home/dialog/payment_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:flutter/material.dart'; class SaveDialog extends StatelessWidget { - const SaveDialog({super.key}); + final TableModel? selectedTable; + final String customerName; + final OrderType orderType; + final List items; + + const SaveDialog( + {super.key, + required this.selectedTable, + required this.customerName, + required this.orderType, + required this.items}); @override Widget build(BuildContext context) { @@ -23,7 +36,11 @@ class SaveDialog extends StatelessWidget { subtitle: 'Simpan pesanan dan bayar nanti', onTap: () => showDialog( context: context, - builder: (context) => const PaymentSaveDialog(), + builder: (context) => PaymentSaveDialog( + selectedTable: selectedTable, + customerName: customerName, + orderType: orderType, + items: items), ), ), SpaceHeight(16.0), diff --git a/lib/presentation/home/models/order_request.dart b/lib/presentation/home/models/order_request.dart new file mode 100644 index 0000000..98f5b0f --- /dev/null +++ b/lib/presentation/home/models/order_request.dart @@ -0,0 +1,84 @@ +import 'dart:convert'; + +class OrderRequestModel { + final String? outletId; + final String? userId; + final String? tableNumber; + final String? orderType; + final String? notes; + final List? orderItems; + final String? customerName; + + OrderRequestModel({ + this.outletId, + this.userId, + this.tableNumber, + this.orderType, + this.notes, + this.orderItems, + this.customerName, + }); + + factory OrderRequestModel.fromJson(String str) => + OrderRequestModel.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory OrderRequestModel.fromMap(Map json) => + OrderRequestModel( + outletId: json["outlet_id"], + userId: json["user_id"], + tableNumber: json["table_number"], + orderType: json["order_type"], + notes: json["notes"], + orderItems: json["order_items"] == null + ? [] + : List.from( + json["order_items"].map((x) => OrderItemRequest.fromMap(x))), + customerName: json["customer_name"], + ); + + Map toMap() => { + "outlet_id": outletId, + "user_id": userId, + "table_number": tableNumber, + "order_type": orderType, + "notes": notes, + "order_items": orderItems == null + ? [] + : List.from(orderItems!.map((x) => x.toMap())), + "customer_name": customerName, + }; +} + +class OrderItemRequest { + final String? productId; + final int? quantity; + final int? unitPrice; + final String? notes; + + OrderItemRequest({ + this.productId, + this.quantity, + this.unitPrice, + this.notes, + }); + + factory OrderItemRequest.fromJson(String str) => + OrderItemRequest.fromMap(json.decode(str)); + + factory OrderItemRequest.fromMap(Map json) => + OrderItemRequest( + productId: json["product_id"], + quantity: json["quantity"], + unitPrice: json["unit_price"]?.toDouble(), + notes: json["notes"], + ); + + Map toMap() => { + "product_id": productId, + "quantity": quantity, + "unit_price": unitPrice, + "notes": notes, + }; +} diff --git a/lib/presentation/home/pages/confirm_payment_page.dart b/lib/presentation/home/pages/confirm_payment_page.dart index b0257d4..944fc7d 100644 --- a/lib/presentation/home/pages/confirm_payment_page.dart +++ b/lib/presentation/home/pages/confirm_payment_page.dart @@ -2,7 +2,10 @@ import 'dart:developer'; import 'package:enaklo_pos/core/components/dashed_divider.dart'; +import 'package:enaklo_pos/core/extensions/build_context_ext.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:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -116,6 +119,7 @@ class _ConfirmPaymentPageState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ ConfirmPaymentTitle( + isBack: false, title: 'Konfirmasi', subtitle: widget.isTable ? 'Orders Table ${widget.table?.tableName}' @@ -769,48 +773,51 @@ class _ConfirmPaymentPageState extends State { const SpaceHeight(20.0), BlocBuilder( builder: (context, state) { - return Row( - children: [ - Button.outlined( - width: 150.0, - onPressed: () { - totalPriceController.text = - uangPas - .toString() - .currencyFormatRpV2; - priceValue = uangPas; - }, - label: 'UANG PAS', - ), - const SpaceWidth(20.0), - Button.outlined( - width: 150.0, - onPressed: () { - totalPriceController.text = - uangPas2 - .toString() - .currencyFormatRpV2; - priceValue = uangPas2; - }, - label: uangPas2 - .toString() - .currencyFormatRpV2, - ), - const SpaceWidth(20.0), - Button.outlined( - width: 150.0, - onPressed: () { - totalPriceController.text = - uangPas3 - .toString() - .currencyFormatRpV2; - priceValue = uangPas3; - }, - label: uangPas3 - .toString() - .currencyFormatRpV2, - ), - ], + return SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: [ + Button.outlined( + width: 150.0, + onPressed: () { + totalPriceController.text = + uangPas + .toString() + .currencyFormatRpV2; + priceValue = uangPas; + }, + label: 'UANG PAS', + ), + const SpaceWidth(20.0), + Button.outlined( + width: 150.0, + onPressed: () { + totalPriceController.text = + uangPas2 + .toString() + .currencyFormatRpV2; + priceValue = uangPas2; + }, + label: uangPas2 + .toString() + .currencyFormatRpV2, + ), + const SpaceWidth(20.0), + Button.outlined( + width: 150.0, + onPressed: () { + totalPriceController.text = + uangPas3 + .toString() + .currencyFormatRpV2; + priceValue = uangPas3; + }, + label: uangPas3 + .toString() + .currencyFormatRpV2, + ), + ], + ), ); }, ), @@ -821,43 +828,83 @@ class _ConfirmPaymentPageState extends State { ), ), ), - Container( - padding: EdgeInsets.all(16), - decoration: BoxDecoration( - color: AppColors.white, - border: Border( - top: BorderSide( - color: AppColors.background, - width: 1.0, - ), - ), - ), - child: Row( - children: [ - Expanded( - child: Button.outlined( - onPressed: () {}, - label: 'Batalkan', + BlocBuilder( + builder: (context, state) { + final orderType = state.maybeWhen( + orElse: () => OrderType.dineIn, + loaded: (products, + discountModel, + discount, + discountAmount, + tax, + serviceCharge, + totalQuantity, + totalPrice, + draftName, + orderType) => + orderType, + ); + + List items = state.maybeWhen( + orElse: () => [], + loaded: (products, + discountModel, + discount, + discountAmount, + tax, + serviceCharge, + totalQuantity, + totalPrice, + draftName, + orderType) => + products, + ); + + return Container( + padding: EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppColors.white, + border: Border( + top: BorderSide( + color: AppColors.background, + width: 1.0, + ), ), ), - SpaceWidth(12), - Expanded( - child: Button.filled( - onPressed: () => showDialog( - context: context, - builder: (context) => SaveDialog()), - label: 'Simpan', - ), + child: Row( + children: [ + Expanded( + child: Button.outlined( + onPressed: () => context.pop(), + label: 'Batalkan', + ), + ), + SpaceWidth(12), + Expanded( + child: Button.filled( + onPressed: () => showDialog( + context: context, + builder: (context) => SaveDialog( + selectedTable: widget.table, + customerName: + customerController.text, + items: items, + orderType: orderType, + )), + label: 'Simpan', + ), + ), + SpaceWidth(12), + Expanded( + child: Button.filled( + onPressed: () {}, + label: 'Bayar', + ), + ), + ], ), - SpaceWidth(12), - Expanded( - child: Button.filled( - onPressed: () {}, - label: 'Bayar', - ), - ), - ], - ), + ); + }, ), ], ), diff --git a/lib/presentation/home/pages/home_page.dart b/lib/presentation/home/pages/home_page.dart index 619ed30..d3f5ec7 100644 --- a/lib/presentation/home/pages/home_page.dart +++ b/lib/presentation/home/pages/home_page.dart @@ -490,7 +490,8 @@ class _HomePageState extends State { elevation: 1, onPressed: () { context.push(ConfirmPaymentPage( - isTable: widget.isTable, + isTable: + widget.table == null ? false : true, table: widget.table, )); }, diff --git a/lib/presentation/home/widgets/home_right_title.dart b/lib/presentation/home/widgets/home_right_title.dart index 4c2ee63..c3fce85 100644 --- a/lib/presentation/home/widgets/home_right_title.dart +++ b/lib/presentation/home/widgets/home_right_title.dart @@ -17,7 +17,7 @@ class HomeRightTitle extends StatelessWidget { @override Widget build(BuildContext context) { return Container( - height: context.deviceHeight * 0.1, + height: context.deviceHeight * 0.15, decoration: BoxDecoration( color: AppColors.primary, border: Border( diff --git a/lib/presentation/table/widgets/table_widget.dart b/lib/presentation/table/widgets/table_widget.dart index ea94ca8..a9684e0 100644 --- a/lib/presentation/table/widgets/table_widget.dart +++ b/lib/presentation/table/widgets/table_widget.dart @@ -67,8 +67,8 @@ class _TableWidgetState extends State { } else { // Handle occupied table click - load draft order and navigate to payment context.read().add( - CheckoutEvent.loadDraftOrder(data!), - ); + CheckoutEvent.loadDraftOrder(data!), + ); log("Data Draft Order: ${data!.toMap()}"); context.push(PaymentTablePage( table: widget.table, @@ -220,7 +220,8 @@ class _TableWidgetState extends State { builder: (context) => AlertDialog( title: Row( children: [ - Icon(Icons.warning, color: AppColors.red), + Icon(Icons.warning, + color: AppColors.red), SizedBox(width: 8), Text('Void Order?'), ], @@ -229,24 +230,33 @@ class _TableWidgetState extends State { 'Apakah anda yakin ingin membatalkan pesanan untuk meja ${widget.table.tableName}?\n\nPesanan akan dihapus secara permanen.'), actions: [ TextButton( - onPressed: () => Navigator.pop(context), - child: Text('Tidak', - style: TextStyle(color: AppColors.primary)), + onPressed: () => + Navigator.pop(context), + child: Text('Tidak', + style: TextStyle( + color: AppColors.primary)), ), - BlocListener( + BlocListener( listener: (context, state) { state.maybeWhen( orElse: () {}, success: () { context .read() - .add(const GetTableEvent.getTables()); - Navigator.pop(context); // Close void dialog - Navigator.pop(context); // Close table info dialog - ScaffoldMessenger.of(context).showSnackBar( + .add(const GetTableEvent + .getTables()); + Navigator.pop( + context); // Close void dialog + Navigator.pop( + context); // Close table info dialog + ScaffoldMessenger.of(context) + .showSnackBar( const SnackBar( - content: Text('Pesanan berhasil dibatalkan'), - backgroundColor: AppColors.primary, + content: Text( + 'Pesanan berhasil dibatalkan'), + backgroundColor: + AppColors.primary, ), ); }, @@ -260,24 +270,31 @@ class _TableWidgetState extends State { // Void the order final newTable = TableModel( id: widget.table.id, - tableName: widget.table.tableName, + tableName: + widget.table.tableName, status: 'available', orderId: 0, paymentAmount: 0, - startTime: DateTime.now().toIso8601String(), + startTime: DateTime.now() + .toIso8601String(), position: widget.table.position, ); - context.read().add( - StatusTableEvent.statusTabel(newTable), + context + .read() + .add( + StatusTableEvent + .statusTabel(newTable), ); // Remove draft order from local storage ProductLocalDatasource.instance - .removeDraftOrderById(widget.table.orderId); + .removeDraftOrderById( + widget.table.orderId); log("Voided order for table: ${widget.table.tableName}"); }, child: const Text( "Ya, Batalkan", - style: TextStyle(color: Colors.white), + style: TextStyle( + color: Colors.white), ), ), ), @@ -292,14 +309,14 @@ class _TableWidgetState extends State { ), SizedBox(width: 12), Expanded( - child: BlocConsumer( + child: BlocConsumer( listener: (context, state) { state.maybeWhen( orElse: () {}, success: () { - context - .read() - .add(const GetTableEvent.getTables()); + context.read().add( + const GetTableEvent.getTables()); context.pop(); }); }, @@ -308,7 +325,8 @@ class _TableWidgetState extends State { onPressed: () { context.pop(); context.read().add( - CheckoutEvent.loadDraftOrder(data!), + CheckoutEvent.loadDraftOrder( + data!), ); context.push(PaymentTablePage( table: widget.table,