From 5d74ed0bed36c5fa5cf2390155215399c6e0d347 Mon Sep 17 00:00:00 2001 From: efrilm Date: Sun, 3 Aug 2025 20:46:57 +0700 Subject: [PATCH] feat: create payment --- .../datasources/order_remote_datasource.dart | 40 +- lib/data/models/request/payment_request.dart | 80 ++ .../response/payment_response_model.dart | 95 ++ lib/main.dart | 4 + .../blocs/order_loader/order_loader_bloc.dart | 4 +- .../blocs/payment_form/payment_form_bloc.dart | 33 + .../payment_form_bloc.freezed.dart | 845 ++++++++++++++++++ .../payment_form/payment_form_event.dart | 6 + .../payment_form/payment_form_state.dart | 9 + .../sales/dialog/payment_dialog.dart | 397 ++++++++ lib/presentation/sales/pages/sales_page.dart | 8 +- .../success/pages/success_payment_page.dart | 164 ++++ 12 files changed, 1678 insertions(+), 7 deletions(-) create mode 100644 lib/data/models/request/payment_request.dart create mode 100644 lib/data/models/response/payment_response_model.dart create mode 100644 lib/presentation/sales/blocs/payment_form/payment_form_bloc.dart create mode 100644 lib/presentation/sales/blocs/payment_form/payment_form_bloc.freezed.dart create mode 100644 lib/presentation/sales/blocs/payment_form/payment_form_event.dart create mode 100644 lib/presentation/sales/blocs/payment_form/payment_form_state.dart create mode 100644 lib/presentation/sales/dialog/payment_dialog.dart create mode 100644 lib/presentation/success/pages/success_payment_page.dart diff --git a/lib/data/datasources/order_remote_datasource.dart b/lib/data/datasources/order_remote_datasource.dart index 599659b..2bc3fde 100644 --- a/lib/data/datasources/order_remote_datasource.dart +++ b/lib/data/datasources/order_remote_datasource.dart @@ -6,8 +6,10 @@ 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/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_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'; @@ -225,6 +227,40 @@ class OrderRemoteDatasource { } } + Future> createPayment( + PaymentRequestModel orderModel) async { + final authData = await AuthLocalDataSource().getAuthData(); + final url = '${Variables.baseUrl}/api/v1/payments'; + + 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 Right(PaymentSuccessResponseModel.fromMap(response.data)); + } else { + return const Left('Gagal membuat pembayaran'); + } + } 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'); + } + } + Future> getOrder( {int page = 1, int limit = Variables.defaultLimit, @@ -248,11 +284,7 @@ class OrderRemoteDatasource { ), ); - log("📥 HTTP Status Code: ${response.statusCode}"); - log("📥 Response Body: ${response.data}"); - if (response.statusCode == 200) { - log("✅ getOrderByRangeDate API call successful"); return Right(OrderResponseModel.fromMap(response.data)); } else { log("❌ getOrderByRangeDate API call failed - Status: ${response.statusCode}"); diff --git a/lib/data/models/request/payment_request.dart b/lib/data/models/request/payment_request.dart new file mode 100644 index 0000000..8386f1a --- /dev/null +++ b/lib/data/models/request/payment_request.dart @@ -0,0 +1,80 @@ +import 'dart:convert'; + +class PaymentRequestModel { + final String? orderId; + final String? paymentMethodId; + final int? amount; + final String? transactionId; + final int? splitNumber; + final int? splitTotal; + final String? splitDescription; + final List? paymentOrderItems; + + PaymentRequestModel({ + this.orderId, + this.paymentMethodId, + this.amount, + this.transactionId, + this.splitNumber, + this.splitTotal, + this.splitDescription, + this.paymentOrderItems, + }); + + factory PaymentRequestModel.fromJson(String str) => + PaymentRequestModel.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory PaymentRequestModel.fromMap(Map json) => + PaymentRequestModel( + orderId: json["order_id"], + paymentMethodId: json["payment_method_id"], + amount: json["amount"]?.toDouble(), + transactionId: json["transaction_id"], + splitNumber: json["split_number"], + splitTotal: json["split_total"], + splitDescription: json["split_description"], + paymentOrderItems: json["payment_order_items"] == null + ? [] + : List.from(json["payment_order_items"] + .map((x) => PaymentOrderItemModel.fromMap(x))), + ); + + Map toMap() => { + "order_id": orderId, + "payment_method_id": paymentMethodId, + "amount": amount, + "transaction_id": transactionId, + "split_number": splitNumber, + "split_total": splitTotal, + "split_description": splitDescription, + "payment_order_items": paymentOrderItems == null + ? [] + : List.from(paymentOrderItems!.map((x) => x.toMap())), + }; +} + +class PaymentOrderItemModel { + final String? orderItemId; + final int? amount; + + PaymentOrderItemModel({ + this.orderItemId, + this.amount, + }); + + factory PaymentOrderItemModel.fromJson(String str) => + PaymentOrderItemModel.fromMap(json.decode(str)); + + factory PaymentOrderItemModel.fromMap(Map json) => + PaymentOrderItemModel( + orderItemId: json["order_item_id"], + amount: json["amount"]?.toDouble(), + ); + + Map toMap() => { + "order_item_id": orderItemId, + "amount": amount, + }; +} diff --git a/lib/data/models/response/payment_response_model.dart b/lib/data/models/response/payment_response_model.dart new file mode 100644 index 0000000..bfaf3ed --- /dev/null +++ b/lib/data/models/response/payment_response_model.dart @@ -0,0 +1,95 @@ +import 'dart:convert'; + +class PaymentSuccessResponseModel { + final bool? success; + final PaymentData? data; + final dynamic errors; + + PaymentSuccessResponseModel({ + this.success, + this.data, + this.errors, + }); + + factory PaymentSuccessResponseModel.fromJson(String str) => + PaymentSuccessResponseModel.fromMap(json.decode(str)); + + String toJson() => json.encode(toMap()); + + factory PaymentSuccessResponseModel.fromMap(Map json) => + PaymentSuccessResponseModel( + success: json["success"], + data: json["data"] == null ? null : PaymentData.fromMap(json["data"]), + errors: json["errors"], + ); + + Map toMap() => { + "success": success, + "data": data?.toMap(), + "errors": errors, + }; +} + +class PaymentData { + final String? id; + final String? orderId; + final String? paymentMethodId; + final int? amount; + final String? status; + final String? transactionId; + final int? splitNumber; + final int? splitTotal; + final String? splitDescription; + final int? refundAmount; + final DateTime? createdAt; + final DateTime? updatedAt; + + PaymentData({ + this.id, + this.orderId, + this.paymentMethodId, + this.amount, + this.status, + this.transactionId, + this.splitNumber, + this.splitTotal, + this.splitDescription, + this.refundAmount, + this.createdAt, + this.updatedAt, + }); + + factory PaymentData.fromMap(Map json) => PaymentData( + id: json["id"], + orderId: json["order_id"], + paymentMethodId: json["payment_method_id"], + amount: json["amount"], + status: json["status"], + transactionId: json["transaction_id"], + splitNumber: json["split_number"], + splitTotal: json["split_total"], + splitDescription: json["split_description"], + refundAmount: json["refund_amount"], + createdAt: json["created_at"] == null + ? null + : DateTime.parse(json["created_at"]), + updatedAt: json["updated_at"] == null + ? null + : DateTime.parse(json["updated_at"]), + ); + + Map toMap() => { + "id": id, + "order_id": orderId, + "payment_method_id": paymentMethodId, + "amount": amount, + "status": status, + "transaction_id": transactionId, + "split_number": splitNumber, + "split_total": splitTotal, + "split_description": splitDescription, + "refund_amount": refundAmount, + "created_at": createdAt?.toIso8601String(), + "updated_at": updatedAt?.toIso8601String(), + }; +} diff --git a/lib/main.dart b/lib/main.dart index b5f862d..e689334 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -8,6 +8,7 @@ import 'package:enaklo_pos/presentation/home/bloc/order_form/order_form_bloc.dar import 'package:enaklo_pos/presentation/home/bloc/outlet_loader/outlet_loader_bloc.dart'; import 'package:enaklo_pos/presentation/home/bloc/product_loader/product_loader_bloc.dart'; import 'package:enaklo_pos/presentation/sales/blocs/order_loader/order_loader_bloc.dart'; +import 'package:enaklo_pos/presentation/sales/blocs/payment_form/payment_form_bloc.dart'; import 'package:flutter/material.dart'; import 'package:enaklo_pos/data/datasources/auth_local_datasource.dart'; import 'package:enaklo_pos/data/datasources/auth_remote_datasource.dart'; @@ -243,6 +244,9 @@ class _MyAppState extends State { BlocProvider( create: (context) => CustomerFormBloc(CustomerRemoteDataSource()), ), + BlocProvider( + create: (context) => PaymentFormBloc(OrderRemoteDatasource()), + ), ], child: MaterialApp( debugShowCheckedModeBanner: false, diff --git a/lib/presentation/sales/blocs/order_loader/order_loader_bloc.dart b/lib/presentation/sales/blocs/order_loader/order_loader_bloc.dart index 08701c0..ce4b95a 100644 --- a/lib/presentation/sales/blocs/order_loader/order_loader_bloc.dart +++ b/lib/presentation/sales/blocs/order_loader/order_loader_bloc.dart @@ -13,8 +13,8 @@ class OrderLoaderBloc extends Bloc { : super(OrderLoaderState.initial()) { on<_GetByStatus>((event, emit) async { emit(const _Loading()); - final result = - await _orderRemoteDatasource.getOrder(status: event.status); + final result = await _orderRemoteDatasource.getOrder( + status: event.status, limit: 20); result.fold( (l) => emit(_Error(l)), (r) => emit(_Loaded( diff --git a/lib/presentation/sales/blocs/payment_form/payment_form_bloc.dart b/lib/presentation/sales/blocs/payment_form/payment_form_bloc.dart new file mode 100644 index 0000000..f491395 --- /dev/null +++ b/lib/presentation/sales/blocs/payment_form/payment_form_bloc.dart @@ -0,0 +1,33 @@ +import 'package:bloc/bloc.dart'; +import 'package:enaklo_pos/data/datasources/order_remote_datasource.dart'; +import 'package:enaklo_pos/data/models/request/payment_request.dart'; +import 'package:enaklo_pos/data/models/response/payment_response_model.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'payment_form_event.dart'; +part 'payment_form_state.dart'; +part 'payment_form_bloc.freezed.dart'; + +class PaymentFormBloc extends Bloc { + final OrderRemoteDatasource _orderRemoteDatasource; + PaymentFormBloc(this._orderRemoteDatasource) + : super(PaymentFormState.initial()) { + on<_Create>( + (event, emit) async { + emit(const _Loading()); + + try { + final result = + await _orderRemoteDatasource.createPayment(event.payment); + + result.fold( + (error) => emit(_Error(error)), + (success) => emit(_Success(success.data!)), + ); + } catch (e) { + emit(_Error("Failed to create payment: $e")); + } + }, + ); + } +} 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 new file mode 100644 index 0000000..a43f786 --- /dev/null +++ b/lib/presentation/sales/blocs/payment_form/payment_form_bloc.freezed.dart @@ -0,0 +1,845 @@ +// 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 'payment_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 _$PaymentFormEvent { + PaymentRequestModel get payment => throw _privateConstructorUsedError; + @optionalTypeArgs + TResult when({ + required TResult Function(PaymentRequestModel payment) create, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(PaymentRequestModel payment)? create, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(PaymentRequestModel payment)? 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 PaymentFormEvent + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $PaymentFormEventCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $PaymentFormEventCopyWith<$Res> { + factory $PaymentFormEventCopyWith( + PaymentFormEvent value, $Res Function(PaymentFormEvent) then) = + _$PaymentFormEventCopyWithImpl<$Res, PaymentFormEvent>; + @useResult + $Res call({PaymentRequestModel payment}); +} + +/// @nodoc +class _$PaymentFormEventCopyWithImpl<$Res, $Val extends PaymentFormEvent> + implements $PaymentFormEventCopyWith<$Res> { + _$PaymentFormEventCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of PaymentFormEvent + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? payment = null, + }) { + return _then(_value.copyWith( + payment: null == payment + ? _value.payment + : payment // ignore: cast_nullable_to_non_nullable + as PaymentRequestModel, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$CreateImplCopyWith<$Res> + implements $PaymentFormEventCopyWith<$Res> { + factory _$$CreateImplCopyWith( + _$CreateImpl value, $Res Function(_$CreateImpl) then) = + __$$CreateImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({PaymentRequestModel payment}); +} + +/// @nodoc +class __$$CreateImplCopyWithImpl<$Res> + extends _$PaymentFormEventCopyWithImpl<$Res, _$CreateImpl> + implements _$$CreateImplCopyWith<$Res> { + __$$CreateImplCopyWithImpl( + _$CreateImpl _value, $Res Function(_$CreateImpl) _then) + : super(_value, _then); + + /// Create a copy of PaymentFormEvent + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? payment = null, + }) { + return _then(_$CreateImpl( + null == payment + ? _value.payment + : payment // ignore: cast_nullable_to_non_nullable + as PaymentRequestModel, + )); + } +} + +/// @nodoc + +class _$CreateImpl implements _Create { + const _$CreateImpl(this.payment); + + @override + final PaymentRequestModel payment; + + @override + String toString() { + return 'PaymentFormEvent.create(payment: $payment)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$CreateImpl && + (identical(other.payment, payment) || other.payment == payment)); + } + + @override + int get hashCode => Object.hash(runtimeType, payment); + + /// Create a copy of PaymentFormEvent + /// 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(PaymentRequestModel payment) create, + }) { + return create(payment); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(PaymentRequestModel payment)? create, + }) { + return create?.call(payment); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(PaymentRequestModel payment)? create, + required TResult orElse(), + }) { + if (create != null) { + return create(payment); + } + 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 PaymentFormEvent { + const factory _Create(final PaymentRequestModel payment) = _$CreateImpl; + + @override + PaymentRequestModel get payment; + + /// Create a copy of PaymentFormEvent + /// 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 _$PaymentFormState { + @optionalTypeArgs + TResult when({ + required TResult Function() initial, + required TResult Function() loading, + required TResult Function(PaymentData data) success, + required TResult Function(String message) error, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? initial, + TResult? Function()? loading, + TResult? Function(PaymentData data)? success, + TResult? Function(String message)? error, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? initial, + TResult Function()? loading, + TResult Function(PaymentData data)? 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 $PaymentFormStateCopyWith<$Res> { + factory $PaymentFormStateCopyWith( + PaymentFormState value, $Res Function(PaymentFormState) then) = + _$PaymentFormStateCopyWithImpl<$Res, PaymentFormState>; +} + +/// @nodoc +class _$PaymentFormStateCopyWithImpl<$Res, $Val extends PaymentFormState> + implements $PaymentFormStateCopyWith<$Res> { + _$PaymentFormStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of PaymentFormState + /// 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 _$PaymentFormStateCopyWithImpl<$Res, _$InitialImpl> + implements _$$InitialImplCopyWith<$Res> { + __$$InitialImplCopyWithImpl( + _$InitialImpl _value, $Res Function(_$InitialImpl) _then) + : super(_value, _then); + + /// Create a copy of PaymentFormState + /// with the given fields replaced by the non-null parameter values. +} + +/// @nodoc + +class _$InitialImpl implements _Initial { + const _$InitialImpl(); + + @override + String toString() { + return 'PaymentFormState.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(PaymentData data) success, + required TResult Function(String message) error, + }) { + return initial(); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? initial, + TResult? Function()? loading, + TResult? Function(PaymentData data)? success, + TResult? Function(String message)? error, + }) { + return initial?.call(); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? initial, + TResult Function()? loading, + TResult Function(PaymentData data)? 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 PaymentFormState { + const factory _Initial() = _$InitialImpl; +} + +/// @nodoc +abstract class _$$LoadingImplCopyWith<$Res> { + factory _$$LoadingImplCopyWith( + _$LoadingImpl value, $Res Function(_$LoadingImpl) then) = + __$$LoadingImplCopyWithImpl<$Res>; +} + +/// @nodoc +class __$$LoadingImplCopyWithImpl<$Res> + extends _$PaymentFormStateCopyWithImpl<$Res, _$LoadingImpl> + implements _$$LoadingImplCopyWith<$Res> { + __$$LoadingImplCopyWithImpl( + _$LoadingImpl _value, $Res Function(_$LoadingImpl) _then) + : super(_value, _then); + + /// Create a copy of PaymentFormState + /// with the given fields replaced by the non-null parameter values. +} + +/// @nodoc + +class _$LoadingImpl implements _Loading { + const _$LoadingImpl(); + + @override + String toString() { + return 'PaymentFormState.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(PaymentData data) success, + required TResult Function(String message) error, + }) { + return loading(); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? initial, + TResult? Function()? loading, + TResult? Function(PaymentData data)? success, + TResult? Function(String message)? error, + }) { + return loading?.call(); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? initial, + TResult Function()? loading, + TResult Function(PaymentData data)? 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 PaymentFormState { + const factory _Loading() = _$LoadingImpl; +} + +/// @nodoc +abstract class _$$SuccessImplCopyWith<$Res> { + factory _$$SuccessImplCopyWith( + _$SuccessImpl value, $Res Function(_$SuccessImpl) then) = + __$$SuccessImplCopyWithImpl<$Res>; + @useResult + $Res call({PaymentData data}); +} + +/// @nodoc +class __$$SuccessImplCopyWithImpl<$Res> + extends _$PaymentFormStateCopyWithImpl<$Res, _$SuccessImpl> + implements _$$SuccessImplCopyWith<$Res> { + __$$SuccessImplCopyWithImpl( + _$SuccessImpl _value, $Res Function(_$SuccessImpl) _then) + : super(_value, _then); + + /// Create a copy of PaymentFormState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? data = freezed, + }) { + return _then(_$SuccessImpl( + freezed == data + ? _value.data + : data // ignore: cast_nullable_to_non_nullable + as PaymentData, + )); + } +} + +/// @nodoc + +class _$SuccessImpl implements _Success { + const _$SuccessImpl(this.data); + + @override + final PaymentData data; + + @override + String toString() { + return 'PaymentFormState.success(data: $data)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$SuccessImpl && + const DeepCollectionEquality().equals(other.data, data)); + } + + @override + int get hashCode => + Object.hash(runtimeType, const DeepCollectionEquality().hash(data)); + + /// Create a copy of PaymentFormState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$SuccessImplCopyWith<_$SuccessImpl> get copyWith => + __$$SuccessImplCopyWithImpl<_$SuccessImpl>(this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() initial, + required TResult Function() loading, + required TResult Function(PaymentData data) success, + required TResult Function(String message) error, + }) { + return success(data); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? initial, + TResult? Function()? loading, + TResult? Function(PaymentData data)? success, + TResult? Function(String message)? error, + }) { + return success?.call(data); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? initial, + TResult Function()? loading, + TResult Function(PaymentData data)? success, + TResult Function(String message)? error, + required TResult orElse(), + }) { + if (success != null) { + return success(data); + } + 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 PaymentFormState { + const factory _Success(final PaymentData data) = _$SuccessImpl; + + PaymentData get data; + + /// Create a copy of PaymentFormState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + _$$SuccessImplCopyWith<_$SuccessImpl> get copyWith => + throw _privateConstructorUsedError; +} + +/// @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 _$PaymentFormStateCopyWithImpl<$Res, _$ErrorImpl> + implements _$$ErrorImplCopyWith<$Res> { + __$$ErrorImplCopyWithImpl( + _$ErrorImpl _value, $Res Function(_$ErrorImpl) _then) + : super(_value, _then); + + /// Create a copy of PaymentFormState + /// 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 'PaymentFormState.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 PaymentFormState + /// 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(PaymentData data) success, + required TResult Function(String message) error, + }) { + return error(message); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? initial, + TResult? Function()? loading, + TResult? Function(PaymentData data)? success, + TResult? Function(String message)? error, + }) { + return error?.call(message); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? initial, + TResult Function()? loading, + TResult Function(PaymentData data)? 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 PaymentFormState { + const factory _Error(final String message) = _$ErrorImpl; + + String get message; + + /// Create a copy of PaymentFormState + /// 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/sales/blocs/payment_form/payment_form_event.dart b/lib/presentation/sales/blocs/payment_form/payment_form_event.dart new file mode 100644 index 0000000..bc34461 --- /dev/null +++ b/lib/presentation/sales/blocs/payment_form/payment_form_event.dart @@ -0,0 +1,6 @@ +part of 'payment_form_bloc.dart'; + +@freezed +class PaymentFormEvent with _$PaymentFormEvent { + const factory PaymentFormEvent.create(PaymentRequestModel payment) = _Create; +} diff --git a/lib/presentation/sales/blocs/payment_form/payment_form_state.dart b/lib/presentation/sales/blocs/payment_form/payment_form_state.dart new file mode 100644 index 0000000..db33745 --- /dev/null +++ b/lib/presentation/sales/blocs/payment_form/payment_form_state.dart @@ -0,0 +1,9 @@ +part of 'payment_form_bloc.dart'; + +@freezed +class PaymentFormState with _$PaymentFormState { + const factory PaymentFormState.initial() = _Initial; + const factory PaymentFormState.loading() = _Loading; + const factory PaymentFormState.success(PaymentData data) = _Success; + const factory PaymentFormState.error(String message) = _Error; +} diff --git a/lib/presentation/sales/dialog/payment_dialog.dart b/lib/presentation/sales/dialog/payment_dialog.dart new file mode 100644 index 0000000..951822d --- /dev/null +++ b/lib/presentation/sales/dialog/payment_dialog.dart @@ -0,0 +1,397 @@ +import 'dart:developer'; + +import 'package:enaklo_pos/core/components/buttons.dart'; +import 'package:enaklo_pos/core/components/custom_modal_dialog.dart'; +import 'package:enaklo_pos/core/components/flushbar.dart'; +import 'package:enaklo_pos/core/components/spaces.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/int_ext.dart'; +import 'package:enaklo_pos/core/extensions/string_ext.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_methods_response_model.dart'; +import 'package:enaklo_pos/presentation/home/bloc/payment_methods/payment_methods_bloc.dart'; +import 'package:enaklo_pos/presentation/sales/blocs/payment_form/payment_form_bloc.dart'; +import 'package:enaklo_pos/presentation/success/pages/success_payment_page.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class PaymentDialog extends StatefulWidget { + final Order order; + const PaymentDialog({super.key, required this.order}); + + @override + State createState() => _PaymentDialogState(); +} + +class _PaymentDialogState extends State { + PaymentMethod? selectedPaymentMethod; + final totalPriceController = TextEditingController(); + int priceValue = 0; + int uangPas = 0; + int uangPas2 = 0; + int uangPas3 = 0; + + init() { + setState(() { + uangPas = widget.order.totalAmount ?? 0; + uangPas2 = 50000; + uangPas3 = 100000; + }); + } + + @override + void initState() { + super.initState(); + context + .read() + .add(PaymentMethodsEvent.fetchPaymentMethods()); + init(); + } + + @override + void dispose() { + super.dispose(); + totalPriceController.dispose(); + } + + @override + Widget build(BuildContext context) { + return CustomModalDialog( + title: 'Pembayaran', + subtitle: 'Silahkan lakukan pembayaran', + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: AppColors.grey, + ), + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Metode Pembayaran', + style: TextStyle( + color: AppColors.black, + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + const SpaceHeight(12.0), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('No. Pesanan'), + Text( + widget.order.orderNumber ?? "", + style: const TextStyle(fontWeight: FontWeight.bold), + ), + ], + ), + const SpaceHeight(6), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('Tagihan'), + Text( + (widget.order.totalAmount ?? 0).currencyFormatRpV2, + style: const TextStyle(fontWeight: FontWeight.bold), + ), + ], + ), + ], + ), + ), + Container( + padding: const EdgeInsets.all(16), + width: double.infinity, + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: AppColors.grey, + width: 1.0, + ), + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Metode Pembayaran', + style: TextStyle( + color: AppColors.black, + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + const SpaceHeight(12.0), + BlocBuilder( + builder: (context, state) { + return state.maybeWhen( + orElse: () => const Center( + child: CircularProgressIndicator(), + ), + loading: () => const Center( + child: Column( + children: [ + CircularProgressIndicator(), + SizedBox(height: 8.0), + Text('Loading payment methods...'), + ], + ), + ), + error: (message) => Column( + children: [ + Center( + child: + Text('Error loading payment methods: $message'), + ), + const SpaceHeight(16.0), + Button.filled( + onPressed: () { + context.read().add( + PaymentMethodsEvent.fetchPaymentMethods()); + }, + label: 'Retry', + ), + ], + ), + loaded: (paymentMethods) { + log("Loaded ${paymentMethods.length} payment methods"); + paymentMethods.forEach((method) { + log("Payment method: ${method.name} (ID: ${method.id})"); + }); + if (paymentMethods.isEmpty) { + return Column( + children: [ + const Center( + child: Text('No payment methods available'), + ), + const SpaceHeight(16.0), + Button.filled( + onPressed: () { + context.read().add( + PaymentMethodsEvent + .fetchPaymentMethods()); + }, + label: 'Retry', + ), + ], + ); + } + + // Set default selected payment method if none selected or if current selection is not in the list + if (selectedPaymentMethod == null || + !paymentMethods.any((method) => + method.id == selectedPaymentMethod?.id)) { + selectedPaymentMethod = paymentMethods.first; + } + + return Wrap( + spacing: 12.0, + runSpacing: 8.0, + children: paymentMethods.map((method) { + final isSelected = + selectedPaymentMethod?.id == method.id; + return GestureDetector( + onTap: () { + setState(() { + selectedPaymentMethod = method; + }); + }, + child: Container( + height: 60, + width: 80, + alignment: Alignment.center, + padding: const EdgeInsets.all(8.0), + decoration: BoxDecoration( + color: isSelected + ? AppColors.primary + : AppColors.white, + border: Border.all( + color: AppColors.primary, + width: 1.0, + ), + borderRadius: BorderRadius.circular(8.0), + ), + child: Text( + method.name ?? "", + style: TextStyle( + color: isSelected + ? AppColors.white + : AppColors.primary, + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ), + ), + ); + }).toList(), + ); + }, + ); + }, + ), + ], + ), + ), + if (selectedPaymentMethod?.type == "cash") + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: AppColors.grey, + width: 1.0, + ), + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Total Bayar', + style: TextStyle( + color: AppColors.black, + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + const SpaceHeight(8.0), + TextFormField( + controller: totalPriceController, + keyboardType: TextInputType.number, + decoration: InputDecoration( + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8.0), + ), + hintText: 'Total harga', + ), + onChanged: (value) { + priceValue = value.toIntegerFromText; + final int newValue = value.toIntegerFromText; + totalPriceController.text = newValue.currencyFormatRp; + totalPriceController.selection = + TextSelection.fromPosition(TextPosition( + offset: totalPriceController.text.length)); + }, + ), + const SpaceHeight(20.0), + 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, + ), + ], + ), + ), + ], + ), + ), + Padding( + padding: const EdgeInsets.symmetric( + vertical: 24, + horizontal: 16, + ), + child: BlocListener( + listener: (context, state) { + state.maybeWhen( + orElse: () {}, + success: (data) { + context.pushReplacement(SuccessPaymentPage( + payment: data, + )); + }, + error: (message) { + AppFlushbar.showError(context, message); + }, + ); + }, + child: BlocBuilder( + builder: (context, state) { + return state.maybeWhen( + orElse: () => Button.filled( + onPressed: () { + if (selectedPaymentMethod == null) { + AppFlushbar.showError(context, + 'Pilih metode pembayaran terlebih dahulu'); + return; + } + + if (selectedPaymentMethod?.type == "cash") { + if (priceValue == 0) { + AppFlushbar.showError( + context, 'Total bayar tidak boleh 0'); + return; + } + } + + final request = PaymentRequestModel( + amount: widget.order.totalAmount ?? 0, + orderId: widget.order.id, + paymentMethodId: selectedPaymentMethod?.id, + splitDescription: '', + splitNumber: 1, + splitTotal: 1, + transactionId: '', + paymentOrderItems: widget.order.orderItems + ?.map((item) => PaymentOrderItemModel( + orderItemId: item.id, + amount: item.totalPrice, + )) + .toList()); + + context + .read() + .add(PaymentFormEvent.create(request)); + }, + label: 'Bayar', + ), + loading: () => Center( + child: const CircularProgressIndicator(), + ), + ); + }, + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/presentation/sales/pages/sales_page.dart b/lib/presentation/sales/pages/sales_page.dart index 69b9b7a..610745e 100644 --- a/lib/presentation/sales/pages/sales_page.dart +++ b/lib/presentation/sales/pages/sales_page.dart @@ -3,6 +3,7 @@ import 'package:enaklo_pos/core/components/spaces.dart'; import 'package:enaklo_pos/data/models/response/order_response_model.dart'; import 'package:enaklo_pos/presentation/sales/blocs/day_sales/day_sales_bloc.dart'; import 'package:enaklo_pos/presentation/sales/blocs/order_loader/order_loader_bloc.dart'; +import 'package:enaklo_pos/presentation/sales/dialog/payment_dialog.dart'; import 'package:enaklo_pos/presentation/sales/widgets/sales_detail.dart'; import 'package:enaklo_pos/presentation/sales/widgets/sales_list_order.dart'; import 'package:enaklo_pos/presentation/sales/widgets/sales_order_information.dart'; @@ -171,7 +172,12 @@ class _SalesPageState extends State { ), SpaceWidth(8), Button.outlined( - onPressed: () {}, + onPressed: () => showDialog( + context: context, + builder: (context) => PaymentDialog( + order: orderDetail!, + ), + ), label: 'Bayar', icon: Icon(Icons.payment), ), diff --git a/lib/presentation/success/pages/success_payment_page.dart b/lib/presentation/success/pages/success_payment_page.dart new file mode 100644 index 0000000..6346f08 --- /dev/null +++ b/lib/presentation/success/pages/success_payment_page.dart @@ -0,0 +1,164 @@ +import 'package:enaklo_pos/core/components/buttons.dart'; +import 'package:enaklo_pos/core/components/dashed_divider.dart'; +import 'package:enaklo_pos/core/components/spaces.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/payment_response_model.dart'; +import 'package:enaklo_pos/presentation/home/pages/dashboard_page.dart'; +import 'package:flutter/material.dart'; + +class SuccessPaymentPage extends StatelessWidget { + final PaymentData payment; + const SuccessPaymentPage({super.key, required this.payment}); + + @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( + 'Pembayaran!', + style: const TextStyle( + fontSize: 18, fontWeight: FontWeight.bold), + ), + Text('Pembayaran berhasil dilalukan', + style: const TextStyle(fontSize: 14)), + ], + ), + ), + DashedDivider( + color: AppColors.grey, + ), + SpaceHeight(24), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Icon( + Icons.check_circle_outline, + size: 64, + color: Colors.green, + ), + ), + Spacer(), + 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( + 'Waktu', + ), + Text( + (payment.createdAt ?? DateTime.now()) + .toFormattedDate3(), + style: const TextStyle(fontWeight: FontWeight.bold), + ), + ], + ), + ], + ), + ), + 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( + (payment.amount ?? 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.pushReplacement(DashboardPage()), + label: 'Kembali', + height: 44, + ), + ), + SpaceWidth(12), + Expanded( + child: Button.filled( + onPressed: () {}, + label: 'Cetak', + icon: Icon( + Icons.print, + color: AppColors.white, + ), + height: 44, + ), + ), + ], + ), + ), + ], + ), + ), + ), + ); + } +}