feat: refund page

This commit is contained in:
efrilm 2025-08-04 23:13:52 +07:00
parent 2912a438e3
commit 1b1d01c1e8
14 changed files with 2349 additions and 32 deletions

View File

@ -527,4 +527,44 @@ class OrderRemoteDatasource {
return const Left('Terjadi kesalahan tak terduga');
}
}
Future<Either<String, bool>> refundPayment({
required String paymentId,
required String reason,
required int refundAmount,
}) async {
final authData = await AuthLocalDataSource().getAuthData();
final url = '${Variables.baseUrl}/api/v1/payments/$paymentId/refund';
try {
final response = await dio.post(
url,
data: {
'refund_amount': refundAmount,
'reason': reason,
},
options: Options(
headers: {
'Authorization': 'Bearer ${authData.token}',
'Accept': 'application/json',
'Content-Type': 'application/json',
},
),
);
if (response.statusCode == 200) {
return Right(true);
} else {
return const Left('Gagal refund');
}
} 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');
}
}
}

View File

@ -9,6 +9,7 @@ import 'package:enaklo_pos/presentation/home/bloc/current_outlet/current_outlet_
import 'package:enaklo_pos/presentation/home/bloc/order_form/order_form_bloc.dart';
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/refund/bloc/refund_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:enaklo_pos/presentation/void/bloc/void_order_bloc.dart';
@ -256,6 +257,9 @@ class _MyAppState extends State<MyApp> {
BlocProvider(
create: (context) => VoidOrderBloc(OrderRemoteDatasource()),
),
BlocProvider(
create: (context) => RefundBloc(OrderRemoteDatasource()),
),
],
child: MaterialApp(
debugShowCheckedModeBanner: false,

View File

@ -0,0 +1,40 @@
import 'package:bloc/bloc.dart';
import 'package:enaklo_pos/data/datasources/order_remote_datasource.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'refund_event.dart';
part 'refund_state.dart';
part 'refund_bloc.freezed.dart';
class RefundBloc extends Bloc<RefundEvent, RefundState> {
final OrderRemoteDatasource _orderRemoteDatasource;
RefundBloc(this._orderRemoteDatasource) : super(const RefundState.initial()) {
on<RefundEvent>((event, emit) async {
await event.when(
refundPayment: (paymentId, reason, refundAmount) =>
_onRefundPayment(paymentId, reason, refundAmount, emit),
);
});
}
Future<void> _onRefundPayment(
String paymentId,
String reason,
int refundAmount,
Emitter<RefundState> emit,
) async {
emit(const RefundState.loading());
final result = await _orderRemoteDatasource.refundPayment(
paymentId: paymentId,
reason: reason,
refundAmount: refundAmount,
);
result.fold(
(error) => emit(RefundState.error(error)),
(success) => emit(const RefundState.success()),
);
}
}

View File

@ -0,0 +1,855 @@
// 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 'refund_bloc.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(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 _$RefundEvent {
String get paymentId => throw _privateConstructorUsedError;
String get reason => throw _privateConstructorUsedError;
int get refundAmount => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function(String paymentId, String reason, int refundAmount)
refundPayment,
}) =>
throw _privateConstructorUsedError;
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function(String paymentId, String reason, int refundAmount)?
refundPayment,
}) =>
throw _privateConstructorUsedError;
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function(String paymentId, String reason, int refundAmount)?
refundPayment,
required TResult orElse(),
}) =>
throw _privateConstructorUsedError;
@optionalTypeArgs
TResult map<TResult extends Object?>({
required TResult Function(_RefundPayment value) refundPayment,
}) =>
throw _privateConstructorUsedError;
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult? Function(_RefundPayment value)? refundPayment,
}) =>
throw _privateConstructorUsedError;
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
TResult Function(_RefundPayment value)? refundPayment,
required TResult orElse(),
}) =>
throw _privateConstructorUsedError;
/// Create a copy of RefundEvent
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$RefundEventCopyWith<RefundEvent> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $RefundEventCopyWith<$Res> {
factory $RefundEventCopyWith(
RefundEvent value, $Res Function(RefundEvent) then) =
_$RefundEventCopyWithImpl<$Res, RefundEvent>;
@useResult
$Res call({String paymentId, String reason, int refundAmount});
}
/// @nodoc
class _$RefundEventCopyWithImpl<$Res, $Val extends RefundEvent>
implements $RefundEventCopyWith<$Res> {
_$RefundEventCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of RefundEvent
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? paymentId = null,
Object? reason = null,
Object? refundAmount = null,
}) {
return _then(_value.copyWith(
paymentId: null == paymentId
? _value.paymentId
: paymentId // ignore: cast_nullable_to_non_nullable
as String,
reason: null == reason
? _value.reason
: reason // ignore: cast_nullable_to_non_nullable
as String,
refundAmount: null == refundAmount
? _value.refundAmount
: refundAmount // ignore: cast_nullable_to_non_nullable
as int,
) as $Val);
}
}
/// @nodoc
abstract class _$$RefundPaymentImplCopyWith<$Res>
implements $RefundEventCopyWith<$Res> {
factory _$$RefundPaymentImplCopyWith(
_$RefundPaymentImpl value, $Res Function(_$RefundPaymentImpl) then) =
__$$RefundPaymentImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({String paymentId, String reason, int refundAmount});
}
/// @nodoc
class __$$RefundPaymentImplCopyWithImpl<$Res>
extends _$RefundEventCopyWithImpl<$Res, _$RefundPaymentImpl>
implements _$$RefundPaymentImplCopyWith<$Res> {
__$$RefundPaymentImplCopyWithImpl(
_$RefundPaymentImpl _value, $Res Function(_$RefundPaymentImpl) _then)
: super(_value, _then);
/// Create a copy of RefundEvent
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? paymentId = null,
Object? reason = null,
Object? refundAmount = null,
}) {
return _then(_$RefundPaymentImpl(
paymentId: null == paymentId
? _value.paymentId
: paymentId // ignore: cast_nullable_to_non_nullable
as String,
reason: null == reason
? _value.reason
: reason // ignore: cast_nullable_to_non_nullable
as String,
refundAmount: null == refundAmount
? _value.refundAmount
: refundAmount // ignore: cast_nullable_to_non_nullable
as int,
));
}
}
/// @nodoc
class _$RefundPaymentImpl implements _RefundPayment {
const _$RefundPaymentImpl(
{required this.paymentId,
required this.reason,
required this.refundAmount});
@override
final String paymentId;
@override
final String reason;
@override
final int refundAmount;
@override
String toString() {
return 'RefundEvent.refundPayment(paymentId: $paymentId, reason: $reason, refundAmount: $refundAmount)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$RefundPaymentImpl &&
(identical(other.paymentId, paymentId) ||
other.paymentId == paymentId) &&
(identical(other.reason, reason) || other.reason == reason) &&
(identical(other.refundAmount, refundAmount) ||
other.refundAmount == refundAmount));
}
@override
int get hashCode => Object.hash(runtimeType, paymentId, reason, refundAmount);
/// Create a copy of RefundEvent
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$RefundPaymentImplCopyWith<_$RefundPaymentImpl> get copyWith =>
__$$RefundPaymentImplCopyWithImpl<_$RefundPaymentImpl>(this, _$identity);
@override
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function(String paymentId, String reason, int refundAmount)
refundPayment,
}) {
return refundPayment(paymentId, reason, refundAmount);
}
@override
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function(String paymentId, String reason, int refundAmount)?
refundPayment,
}) {
return refundPayment?.call(paymentId, reason, refundAmount);
}
@override
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function(String paymentId, String reason, int refundAmount)?
refundPayment,
required TResult orElse(),
}) {
if (refundPayment != null) {
return refundPayment(paymentId, reason, refundAmount);
}
return orElse();
}
@override
@optionalTypeArgs
TResult map<TResult extends Object?>({
required TResult Function(_RefundPayment value) refundPayment,
}) {
return refundPayment(this);
}
@override
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult? Function(_RefundPayment value)? refundPayment,
}) {
return refundPayment?.call(this);
}
@override
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
TResult Function(_RefundPayment value)? refundPayment,
required TResult orElse(),
}) {
if (refundPayment != null) {
return refundPayment(this);
}
return orElse();
}
}
abstract class _RefundPayment implements RefundEvent {
const factory _RefundPayment(
{required final String paymentId,
required final String reason,
required final int refundAmount}) = _$RefundPaymentImpl;
@override
String get paymentId;
@override
String get reason;
@override
int get refundAmount;
/// Create a copy of RefundEvent
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$RefundPaymentImplCopyWith<_$RefundPaymentImpl> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
mixin _$RefundState {
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function() initial,
required TResult Function() loading,
required TResult Function() success,
required TResult Function(String message) error,
}) =>
throw _privateConstructorUsedError;
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function()? initial,
TResult? Function()? loading,
TResult? Function()? success,
TResult? Function(String message)? error,
}) =>
throw _privateConstructorUsedError;
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function()? initial,
TResult Function()? loading,
TResult Function()? success,
TResult Function(String message)? error,
required TResult orElse(),
}) =>
throw _privateConstructorUsedError;
@optionalTypeArgs
TResult map<TResult extends Object?>({
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 extends Object?>({
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 extends Object?>({
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 $RefundStateCopyWith<$Res> {
factory $RefundStateCopyWith(
RefundState value, $Res Function(RefundState) then) =
_$RefundStateCopyWithImpl<$Res, RefundState>;
}
/// @nodoc
class _$RefundStateCopyWithImpl<$Res, $Val extends RefundState>
implements $RefundStateCopyWith<$Res> {
_$RefundStateCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of RefundState
/// 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 _$RefundStateCopyWithImpl<$Res, _$InitialImpl>
implements _$$InitialImplCopyWith<$Res> {
__$$InitialImplCopyWithImpl(
_$InitialImpl _value, $Res Function(_$InitialImpl) _then)
: super(_value, _then);
/// Create a copy of RefundState
/// with the given fields replaced by the non-null parameter values.
}
/// @nodoc
class _$InitialImpl implements _Initial {
const _$InitialImpl();
@override
String toString() {
return 'RefundState.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<TResult extends Object?>({
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 extends Object?>({
TResult? Function()? initial,
TResult? Function()? loading,
TResult? Function()? success,
TResult? Function(String message)? error,
}) {
return initial?.call();
}
@override
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
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<TResult extends Object?>({
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 extends Object?>({
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 extends Object?>({
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 RefundState {
const factory _Initial() = _$InitialImpl;
}
/// @nodoc
abstract class _$$LoadingImplCopyWith<$Res> {
factory _$$LoadingImplCopyWith(
_$LoadingImpl value, $Res Function(_$LoadingImpl) then) =
__$$LoadingImplCopyWithImpl<$Res>;
}
/// @nodoc
class __$$LoadingImplCopyWithImpl<$Res>
extends _$RefundStateCopyWithImpl<$Res, _$LoadingImpl>
implements _$$LoadingImplCopyWith<$Res> {
__$$LoadingImplCopyWithImpl(
_$LoadingImpl _value, $Res Function(_$LoadingImpl) _then)
: super(_value, _then);
/// Create a copy of RefundState
/// with the given fields replaced by the non-null parameter values.
}
/// @nodoc
class _$LoadingImpl implements _Loading {
const _$LoadingImpl();
@override
String toString() {
return 'RefundState.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<TResult extends Object?>({
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 extends Object?>({
TResult? Function()? initial,
TResult? Function()? loading,
TResult? Function()? success,
TResult? Function(String message)? error,
}) {
return loading?.call();
}
@override
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
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<TResult extends Object?>({
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 extends Object?>({
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 extends Object?>({
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 RefundState {
const factory _Loading() = _$LoadingImpl;
}
/// @nodoc
abstract class _$$SuccessImplCopyWith<$Res> {
factory _$$SuccessImplCopyWith(
_$SuccessImpl value, $Res Function(_$SuccessImpl) then) =
__$$SuccessImplCopyWithImpl<$Res>;
}
/// @nodoc
class __$$SuccessImplCopyWithImpl<$Res>
extends _$RefundStateCopyWithImpl<$Res, _$SuccessImpl>
implements _$$SuccessImplCopyWith<$Res> {
__$$SuccessImplCopyWithImpl(
_$SuccessImpl _value, $Res Function(_$SuccessImpl) _then)
: super(_value, _then);
/// Create a copy of RefundState
/// with the given fields replaced by the non-null parameter values.
}
/// @nodoc
class _$SuccessImpl implements _Success {
const _$SuccessImpl();
@override
String toString() {
return 'RefundState.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<TResult extends Object?>({
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 extends Object?>({
TResult? Function()? initial,
TResult? Function()? loading,
TResult? Function()? success,
TResult? Function(String message)? error,
}) {
return success?.call();
}
@override
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
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<TResult extends Object?>({
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 extends Object?>({
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 extends Object?>({
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 RefundState {
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 _$RefundStateCopyWithImpl<$Res, _$ErrorImpl>
implements _$$ErrorImplCopyWith<$Res> {
__$$ErrorImplCopyWithImpl(
_$ErrorImpl _value, $Res Function(_$ErrorImpl) _then)
: super(_value, _then);
/// Create a copy of RefundState
/// 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 'RefundState.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 RefundState
/// 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<TResult extends Object?>({
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 extends Object?>({
TResult? Function()? initial,
TResult? Function()? loading,
TResult? Function()? success,
TResult? Function(String message)? error,
}) {
return error?.call(message);
}
@override
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
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<TResult extends Object?>({
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 extends Object?>({
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 extends Object?>({
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 RefundState {
const factory _Error(final String message) = _$ErrorImpl;
String get message;
/// Create a copy of RefundState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
_$$ErrorImplCopyWith<_$ErrorImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@ -0,0 +1,10 @@
part of 'refund_bloc.dart';
@freezed
class RefundEvent with _$RefundEvent {
const factory RefundEvent.refundPayment({
required String paymentId,
required String reason,
required int refundAmount,
}) = _RefundPayment;
}

View File

@ -0,0 +1,9 @@
part of 'refund_bloc.dart';
@freezed
class RefundState with _$RefundState {
const factory RefundState.initial() = _Initial;
const factory RefundState.loading() = _Loading;
const factory RefundState.success() = _Success;
const factory RefundState.error(String message) = _Error;
}

View File

@ -0,0 +1,90 @@
import 'package:enaklo_pos/core/components/spaces.dart';
import 'package:flutter/material.dart';
class RefundErrorDialog extends StatelessWidget {
final String message;
const RefundErrorDialog({super.key, required this.message});
@override
Widget build(BuildContext context) {
return Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(24),
),
child: Container(
padding: EdgeInsets.all(32),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(24),
gradient: LinearGradient(
colors: [Colors.red[50]!, Colors.white],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.red,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.red.withOpacity(0.3),
blurRadius: 20,
offset: Offset(0, 10),
),
],
),
child: Icon(
Icons.error,
color: Colors.white,
size: 40,
),
),
SpaceHeight(24),
Text(
'Error!',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.red[600],
),
),
SpaceHeight(12),
Text(
message,
style: TextStyle(
color: Colors.grey[600],
fontSize: 16,
),
textAlign: TextAlign.center,
),
SpaceHeight(32),
Container(
width: double.infinity,
child: ElevatedButton(
onPressed: () => Navigator.pop(context),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red[600],
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
padding: EdgeInsets.symmetric(vertical: 16),
),
child: Text(
'OK',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
),
),
],
),
),
);
}
}

View File

@ -0,0 +1,131 @@
import 'package:enaklo_pos/core/components/spaces.dart';
import 'package:enaklo_pos/core/constants/colors.dart';
import 'package:enaklo_pos/core/extensions/int_ext.dart';
import 'package:flutter/material.dart';
class RefundSuccessDialog extends StatelessWidget {
final int refundAmount;
final String selectedReason;
const RefundSuccessDialog(
{super.key, required this.refundAmount, required this.selectedReason});
@override
Widget build(BuildContext context) {
return Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(24),
),
child: Container(
padding: EdgeInsets.all(32),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(24),
gradient: LinearGradient(
colors: [Colors.green[50]!, Colors.white],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.green,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.green.withOpacity(0.3),
blurRadius: 20,
offset: Offset(0, 10),
),
],
),
child: Icon(
Icons.check,
color: Colors.white,
size: 40,
),
),
SpaceHeight(24),
Text(
'Refund Berhasil!',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: AppColors.primary,
),
),
SpaceHeight(12),
Text(
'Refund sebesar ${(refundAmount).currencyFormatRpV2} telah diproses.',
style: TextStyle(
color: Colors.grey[600],
fontSize: 16,
),
textAlign: TextAlign.center,
),
SpaceHeight(8),
Text(
'Alasan: $selectedReason',
style: TextStyle(
color: Colors.grey[500],
fontSize: 14,
),
textAlign: TextAlign.center,
),
SpaceHeight(32),
Row(
children: [
Expanded(
child: OutlinedButton(
onPressed: () {
Navigator.pop(context);
},
style: OutlinedButton.styleFrom(
side: BorderSide(color: AppColors.primary),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
padding: EdgeInsets.symmetric(vertical: 16),
),
child: Text(
'Print Receipt',
style: TextStyle(
color: AppColors.primary,
fontWeight: FontWeight.bold,
),
),
),
),
SizedBox(width: 16),
Expanded(
child: ElevatedButton(
onPressed: () {
Navigator.pop(context);
Navigator.pop(context);
},
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primary,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
padding: EdgeInsets.symmetric(vertical: 16),
),
child: Text(
'Selesai',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
),
),
],
),
],
),
),
);
}
}

View File

@ -0,0 +1,818 @@
import 'package:enaklo_pos/core/components/spaces.dart';
import 'package:enaklo_pos/core/constants/colors.dart';
import 'package:enaklo_pos/core/extensions/date_time_ext.dart';
import 'package:enaklo_pos/core/extensions/int_ext.dart';
import 'package:enaklo_pos/data/models/response/order_response_model.dart';
import 'package:enaklo_pos/presentation/refund/bloc/refund_bloc.dart';
import 'package:enaklo_pos/presentation/refund/dialog/refund_error_dialog.dart';
import 'package:enaklo_pos/presentation/refund/dialog/refund_success_dialog.dart';
import 'package:enaklo_pos/presentation/refund/widgets/refund_appbar.dart';
import 'package:enaklo_pos/presentation/refund/widgets/refund_info_tile.dart';
import 'package:enaklo_pos/presentation/refund/widgets/refund_order_Item_tile.dart';
import 'package:enaklo_pos/presentation/refund/widgets/refund_reason_tile.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class RefundPage extends StatefulWidget {
final Order selectedOrder;
const RefundPage({super.key, required this.selectedOrder});
@override
State<RefundPage> createState() => _RefundPageState();
}
class _RefundPageState extends State<RefundPage> with TickerProviderStateMixin {
final TextEditingController _reasonController = TextEditingController();
final TextEditingController _refundAmountController = TextEditingController();
final ScrollController _leftPanelScrollController = ScrollController();
final ScrollController _rightPanelScrollController = ScrollController();
final ScrollController _itemsScrollController = ScrollController();
String selectedReason = 'Barang Rusak';
late AnimationController _slideController;
late AnimationController _fadeController;
late AnimationController _scaleController;
late Animation<Offset> _slideAnimation;
late Animation<double> _fadeAnimation;
late Animation<double> _scaleAnimation;
final List<Map<String, dynamic>> refundReasons = [
{'value': 'Barang Rusak', 'icon': Icons.broken_image, 'color': Colors.red},
{'value': 'Salah Item', 'icon': Icons.swap_horiz, 'color': Colors.orange},
{
'value': 'Tidak Sesuai Pesanan',
'icon': Icons.error_outline,
'color': Colors.amber
},
{
'value': 'Permintaan Customer',
'icon': Icons.person,
'color': Colors.blue
},
{
'value': 'Kualitas Tidak Baik',
'icon': Icons.thumb_down,
'color': Colors.purple
},
{'value': 'Lainnya', 'icon': Icons.more_horiz, 'color': Colors.grey},
];
@override
void initState() {
super.initState();
_initializeAnimations();
_refundAmountController.text =
(widget.selectedOrder.totalAmount ?? 0).toString();
}
void _initializeAnimations() {
_slideController = AnimationController(
duration: Duration(milliseconds: 1200),
vsync: this,
);
_fadeController = AnimationController(
duration: Duration(milliseconds: 800),
vsync: this,
);
_scaleController = AnimationController(
duration: Duration(milliseconds: 600),
vsync: this,
);
_slideAnimation = Tween<Offset>(
begin: Offset(0.0, 1.0),
end: Offset.zero,
).animate(CurvedAnimation(
parent: _slideController,
curve: Curves.elasticOut,
));
_fadeAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _fadeController,
curve: Curves.easeInOut,
));
_scaleAnimation = Tween<double>(
begin: 0.8,
end: 1.0,
).animate(CurvedAnimation(
parent: _scaleController,
curve: Curves.elasticOut,
));
_fadeController.forward();
_slideController.forward();
_scaleController.forward();
}
@override
void dispose() {
_slideController.dispose();
_fadeController.dispose();
_scaleController.dispose();
_reasonController.dispose();
_refundAmountController.dispose();
_leftPanelScrollController.dispose();
_rightPanelScrollController.dispose();
_itemsScrollController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return BlocListener<RefundBloc, RefundState>(
listener: (context, state) {
state.when(
initial: () {},
loading: () {},
success: () {
_showSuccessDialog();
},
error: (message) {
_showErrorDialog(message);
},
);
},
child: Scaffold(
backgroundColor: Color(0xFFF5F7FA),
body: FadeTransition(
opacity: _fadeAnimation,
child: Column(
children: [
RefundAppbar(
order: widget.selectedOrder,
),
Expanded(
child: Padding(
padding: EdgeInsets.all(24.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Left Panel - Order Summary (Scrollable)
Expanded(
flex: 3,
child: Scrollbar(
controller: _leftPanelScrollController,
thumbVisibility: true,
child: SingleChildScrollView(
controller: _leftPanelScrollController,
child: _buildOrderSummaryPanel(),
),
),
),
SizedBox(width: 24),
// Right Panel - Refund Configuration (Scrollable)
Expanded(
flex: 4,
child: Scrollbar(
controller: _rightPanelScrollController,
thumbVisibility: true,
child: SingleChildScrollView(
controller: _rightPanelScrollController,
child: _buildRefundConfigPanel(context),
),
),
),
],
),
),
),
],
),
),
),
);
}
Widget _buildOrderSummaryPanel() {
return ScaleTransition(
scale: _scaleAnimation,
child: Column(
children: [
// Order Info Card
Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(24),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.08),
blurRadius: 30,
offset: Offset(0, 15),
),
],
),
child: Padding(
padding: EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: EdgeInsets.all(12),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.green[400]!, Colors.green[600]!],
),
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.green.withOpacity(0.3),
blurRadius: 15,
offset: Offset(0, 8),
),
],
),
child: Icon(
Icons.receipt_long,
color: Colors.white,
size: 24,
),
),
SizedBox(width: 20),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Detail Pesanan',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: AppColors.primary,
),
),
SpaceHeight(4),
Text(
(widget.selectedOrder.createdAt ?? DateTime.now())
.toFormattedDate3(),
style: TextStyle(
color: Colors.grey[600],
fontSize: 14,
),
),
],
),
),
],
),
SpaceHeight(20),
// Order Details Grid
Row(
children: [
Expanded(
child: RefundInfoTile(
title: 'Meja',
value: widget.selectedOrder.tableNumber ?? 'Takeaway',
icon: Icons.table_restaurant,
color: Colors.blue,
),
),
SizedBox(width: 16),
Expanded(
child: RefundInfoTile(
title: 'Tipe',
value: widget.selectedOrder.orderType ?? 'N/A',
icon: Icons.shopping_bag_outlined,
color: Colors.purple,
),
),
],
),
SpaceHeight(16),
Row(
children: [
Expanded(
child: RefundInfoTile(
title: 'Status',
value: widget.selectedOrder.status?.toUpperCase() ??
'N/A',
icon: Icons.check_circle_outline,
color: _getStatusColor(widget.selectedOrder.status),
),
),
SizedBox(width: 16),
Expanded(
child: RefundInfoTile(
title: 'Items',
value:
'${widget.selectedOrder.orderItems?.length ?? 0}',
icon: Icons.inventory_2_outlined,
color: Colors.orange,
),
),
],
),
],
),
),
),
SpaceHeight(24),
// Payment Summary Card
Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(24),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.08),
blurRadius: 30,
offset: Offset(0, 15),
),
],
),
child: Padding(
padding: EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.amber.withOpacity(0.2),
borderRadius: BorderRadius.circular(12),
),
child: Icon(
Icons.account_balance_wallet,
color: Colors.amber[700],
size: 24,
),
),
SizedBox(width: 16),
Text(
'Ringkasan Pembayaran',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: AppColors.primary,
),
),
],
),
SpaceHeight(24),
_buildPaymentRow(
'Subtotal', widget.selectedOrder.subtotal ?? 0),
_buildPaymentRow(
'Pajak', widget.selectedOrder.taxAmount ?? 0),
_buildPaymentRow(
'Diskon', -(widget.selectedOrder.discountAmount ?? 0)),
Padding(
padding: EdgeInsets.symmetric(vertical: 16),
child: Divider(thickness: 2, color: Colors.grey[200]),
),
_buildPaymentRow(
'Total Dibayar',
widget.selectedOrder.totalAmount ?? 0,
isTotal: true,
),
],
),
),
),
SpaceHeight(24), // Extra space for scroll
],
),
);
}
Widget _buildRefundConfigPanel(BuildContext context) {
return SlideTransition(
position: _slideAnimation,
child: Column(
children: [
// Refund Reason Card
Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(24),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.08),
blurRadius: 30,
offset: Offset(0, 15),
),
],
),
child: Padding(
padding: EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: EdgeInsets.all(12),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.red[400]!, Colors.red[600]!],
),
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.red.withOpacity(0.3),
blurRadius: 15,
offset: Offset(0, 8),
),
],
),
child: Icon(
Icons.assignment_return,
color: Colors.white,
size: 24,
),
),
SizedBox(width: 20),
Text(
'Konfigurasi Refund',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: AppColors.primary,
),
),
],
),
SpaceHeight(20),
Text(
'Pilih Alasan Refund',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Colors.grey[700],
),
),
// Reason Selection Grid
GridView.builder(
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
crossAxisSpacing: 12,
mainAxisSpacing: 12,
childAspectRatio: 2.5,
),
itemCount: refundReasons.length,
itemBuilder: (context, index) {
final reason = refundReasons[index];
final isSelected = selectedReason == reason['value'];
return RefundReasonTile(
isSelected: isSelected,
reason: reason,
onTap: () {
setState(() {
selectedReason = reason['value'];
});
},
);
},
),
if (selectedReason == 'Lainnya') ...[
SpaceHeight(24),
TextField(
controller: _reasonController,
maxLines: 3,
decoration: InputDecoration(
hintText: 'Jelaskan alasan refund secara detail...',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: BorderSide(color: Colors.grey[300]!),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide:
BorderSide(color: AppColors.primary, width: 2),
),
filled: true,
fillColor: Colors.grey[50],
contentPadding: EdgeInsets.all(20),
),
),
],
SpaceHeight(32),
// Refund Amount Input
Text(
'Jumlah Refund',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Colors.grey[700],
),
),
SpaceHeight(16),
TextField(
controller: _refundAmountController,
keyboardType: TextInputType.number,
readOnly: true,
decoration: InputDecoration(
hintText: 'Masukkan jumlah refund',
prefixText: 'Rp ',
prefixStyle: TextStyle(
color: AppColors.primary,
fontWeight: FontWeight.bold,
fontSize: 16,
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: BorderSide(color: Colors.grey[300]!),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide:
BorderSide(color: AppColors.primary, width: 2),
),
filled: true,
fillColor: Colors.grey[50],
contentPadding: EdgeInsets.all(20),
),
),
],
),
),
),
SpaceHeight(24),
// Items Display Card
Container(
height: 500, // Fixed height untuk items list
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(24),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.08),
blurRadius: 30,
offset: Offset(0, 15),
),
],
),
child: Column(
children: [
Padding(
padding: EdgeInsets.all(20),
child: Row(
children: [
Container(
padding: EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.blue.withOpacity(0.2),
borderRadius: BorderRadius.circular(12),
),
child: Icon(
Icons.list_alt,
color: Colors.blue[700],
size: 24,
),
),
SizedBox(width: 16),
Expanded(
child: Text(
'Item Pesanan',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: AppColors.primary,
),
),
),
Container(
padding:
EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: AppColors.primary.withOpacity(0.1),
borderRadius: BorderRadius.circular(20),
),
child: Text(
'${widget.selectedOrder.orderItems?.length ?? 0} item',
style: TextStyle(
color: AppColors.primary,
fontWeight: FontWeight.bold,
fontSize: 12,
),
),
),
],
),
),
// Scrollable Items List
Expanded(
child: Scrollbar(
controller: _itemsScrollController,
thumbVisibility: true,
child: ListView.builder(
controller: _itemsScrollController,
padding: EdgeInsets.symmetric(horizontal: 32),
itemCount: widget.selectedOrder.orderItems?.length ?? 0,
itemBuilder: (context, index) {
final item = widget.selectedOrder.orderItems![index];
return RefundOrderItemTile(item: item);
},
),
),
),
// Process Refund Button
Container(
padding: EdgeInsets.all(32),
decoration: BoxDecoration(
color: Colors.grey[50],
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(24),
bottomRight: Radius.circular(24),
),
),
child: BlocBuilder<RefundBloc, RefundState>(
builder: (context, state) {
final isLoading = state.maybeWhen(
loading: () => true,
orElse: () => false,
);
return Container(
width: double.infinity,
height: 64,
child: ElevatedButton(
onPressed:
isLoading ? null : () => _processRefund(context),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red[600],
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
elevation: 0,
shadowColor: Colors.red.withOpacity(0.3),
),
child: isLoading
? Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
color: Colors.white,
strokeWidth: 3,
),
),
SizedBox(width: 16),
Text(
'Memproses Refund...',
style: TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
],
)
: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.monetization_on,
color: Colors.white, size: 28),
SizedBox(width: 16),
Text(
'PROSES REFUND',
style: TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.bold,
letterSpacing: 0.5,
),
),
],
),
),
);
},
),
),
],
),
),
SpaceHeight(24), // Extra space for scroll
],
),
);
}
Widget _buildPaymentRow(String label, int amount, {bool isTotal = false}) {
return Padding(
padding: EdgeInsets.symmetric(vertical: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
label,
style: TextStyle(
fontSize: isTotal ? 18 : 16,
fontWeight: isTotal ? FontWeight.bold : FontWeight.w500,
color: isTotal ? AppColors.primary : Colors.grey[700],
),
),
Text(
amount.currencyFormatRpV2,
style: TextStyle(
fontSize: isTotal ? 18 : 16,
fontWeight: isTotal ? FontWeight.bold : FontWeight.w600,
color: isTotal ? AppColors.primary : Colors.grey[800],
),
),
],
),
);
}
Color _getStatusColor(String? status) {
switch (status?.toLowerCase()) {
case 'completed':
case 'paid':
return Colors.green;
case 'pending':
return Colors.orange;
case 'cancelled':
return Colors.red;
default:
return Colors.grey;
}
}
void _processRefund(BuildContext context) {
// Validate refund amount
final refundAmount = int.tryParse(_refundAmountController.text) ?? 0;
if (refundAmount <= 0) {
_showErrorDialog('Jumlah refund harus lebih dari 0');
return;
}
final totalAmount = widget.selectedOrder.totalAmount ?? 0;
if (refundAmount > totalAmount) {
_showErrorDialog('Jumlah refund tidak boleh melebihi total Pesanan');
return;
}
// Get reason text
String reason = selectedReason;
if (selectedReason == 'Lainnya' && _reasonController.text.isNotEmpty) {
reason = _reasonController.text;
}
// Trigger refund event
context.read<RefundBloc>().add(
RefundEvent.refundPayment(
paymentId: widget.selectedOrder.id ??
'', // Assuming order ID is payment ID
reason: reason,
refundAmount: refundAmount,
),
);
}
void _showErrorDialog(String message) {
showDialog(
context: context,
builder: (context) => RefundErrorDialog(message: message),
);
}
void _showSuccessDialog() {
final refundAmount = int.tryParse(_refundAmountController.text) ?? 0;
showDialog(
context: context,
barrierDismissible: false,
builder: (context) => RefundSuccessDialog(
selectedReason: selectedReason,
refundAmount: refundAmount,
),
);
}
}

View File

@ -0,0 +1,104 @@
import 'package:enaklo_pos/core/extensions/date_time_ext.dart';
import 'package:enaklo_pos/data/models/response/order_response_model.dart';
import 'package:flutter/material.dart';
class RefundAppbar extends StatelessWidget {
final Order order;
const RefundAppbar({super.key, required this.order});
@override
Widget build(BuildContext context) {
return Container(
height: 120,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
Color(0xFF36175E),
Color(0xFF4A2C6B),
Color(0xFF5D3F78),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
boxShadow: [
BoxShadow(
color: Color(0xFF36175E).withOpacity(0.4),
blurRadius: 30,
offset: Offset(0, 15),
),
],
),
child: SafeArea(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 24, vertical: 16),
child: Row(
children: [
Container(
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: Colors.white.withOpacity(0.3)),
),
child: IconButton(
icon: Icon(Icons.arrow_back_ios_new,
color: Colors.white, size: 24),
onPressed: () => Navigator.pop(context),
),
),
SizedBox(width: 24),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Refund Pesanan',
style: TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.bold,
letterSpacing: -0.5,
),
),
SizedBox(height: 4),
Text(
'Order #${order.orderNumber}',
style: TextStyle(
color: Colors.white.withOpacity(0.9),
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
],
),
),
Container(
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 12),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(25),
border: Border.all(color: Colors.white.withOpacity(0.3)),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.schedule, color: Colors.white, size: 18),
SizedBox(width: 8),
Text(
DateTime.now().toFormattedDate3(),
style: TextStyle(
color: Colors.white,
fontSize: 14,
fontWeight: FontWeight.w600,
),
),
],
),
),
],
),
),
),
);
}
}

View File

@ -0,0 +1,57 @@
import 'package:enaklo_pos/core/components/spaces.dart';
import 'package:flutter/material.dart';
class RefundInfoTile extends StatelessWidget {
final String title;
final String value;
final IconData icon;
final Color color;
const RefundInfoTile({
super.key,
required this.title,
required this.value,
required this.icon,
required this.color,
});
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(16),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: color.withOpacity(0.2)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(icon, color: color, size: 20),
SizedBox(width: 8),
Text(
title,
style: TextStyle(
color: Colors.grey[600],
fontSize: 12,
fontWeight: FontWeight.w500,
),
),
],
),
SpaceHeight(8),
Text(
value,
style: TextStyle(
color: Colors.grey[800],
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
],
),
);
}
}

View File

@ -0,0 +1,130 @@
import 'package:enaklo_pos/core/components/spaces.dart';
import 'package:enaklo_pos/core/constants/colors.dart';
import 'package:enaklo_pos/core/extensions/int_ext.dart';
import 'package:enaklo_pos/data/models/response/order_response_model.dart';
import 'package:flutter/material.dart';
class RefundOrderItemTile extends StatelessWidget {
final OrderItem item;
const RefundOrderItemTile({super.key, required this.item});
@override
Widget build(BuildContext context) {
return Container(
margin: EdgeInsets.only(bottom: 16),
decoration: BoxDecoration(
color: Colors.grey[50],
borderRadius: BorderRadius.circular(20),
border: Border.all(color: Colors.grey[200]!, width: 1),
),
child: Padding(
padding: EdgeInsets.all(24),
child: Row(
children: [
// Item Icon
Container(
padding: EdgeInsets.all(12),
decoration: BoxDecoration(
color: AppColors.primary.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Icon(
Icons.restaurant,
color: AppColors.primary,
size: 24,
),
),
SizedBox(width: 20),
// Item Details
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
item.productName ?? 'N/A',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 18,
color: Colors.grey[800],
),
),
if (item.productVariantName != null) ...[
SpaceHeight(4),
Text(
item.productVariantName!,
style: TextStyle(
color: Colors.grey[600],
fontSize: 14,
),
),
],
if (item.notes != null && item.notes!.isNotEmpty) ...[
SpaceHeight(8),
Container(
padding:
EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: Colors.amber.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Text(
'Catatan: ${item.notes}',
style: TextStyle(
color: Colors.amber[700],
fontSize: 12,
fontWeight: FontWeight.w500,
),
),
),
],
],
),
),
// Price & Quantity
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
(item.unitPrice ?? 0).currencyFormatRpV2,
style: TextStyle(
fontWeight: FontWeight.bold,
color: AppColors.primary,
fontSize: 16,
),
),
SpaceHeight(4),
Container(
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 4),
decoration: BoxDecoration(
color: Colors.blue.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Text(
'x${item.quantity}',
style: TextStyle(
color: Colors.blue[700],
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
),
SpaceHeight(8),
Text(
(item.totalPrice ?? 0).currencyFormatRpV2,
style: TextStyle(
color: Colors.grey[600],
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
],
),
],
),
),
);
}
}

View File

@ -0,0 +1,53 @@
import 'package:enaklo_pos/core/components/spaces.dart';
import 'package:flutter/material.dart';
class RefundReasonTile extends StatelessWidget {
final bool isSelected;
final Map<String, dynamic> reason;
final Function() onTap;
const RefundReasonTile({
super.key,
required this.isSelected,
required this.reason,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: AnimatedContainer(
duration: Duration(milliseconds: 300),
decoration: BoxDecoration(
color:
isSelected ? reason['color'].withOpacity(0.2) : Colors.grey[100],
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: isSelected ? reason['color'] : Colors.transparent,
width: 2,
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
reason['icon'],
color: isSelected ? reason['color'] : Colors.grey[600],
size: 20,
),
SpaceHeight(4),
Text(
reason['value'],
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.w600,
color: isSelected ? reason['color'] : Colors.grey[600],
),
textAlign: TextAlign.center,
),
],
),
),
);
}
}

View File

@ -1,13 +1,12 @@
import 'package:enaklo_pos/core/components/buttons.dart';
import 'package:enaklo_pos/core/components/flushbar.dart';
import 'package:enaklo_pos/core/components/spaces.dart';
import 'package:enaklo_pos/core/extensions/build_context_ext.dart';
import 'package:enaklo_pos/data/models/response/order_response_model.dart';
import 'package:enaklo_pos/presentation/home/bloc/order_form/order_form_bloc.dart';
import 'package:enaklo_pos/presentation/refund/pages/refund_page.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/dialog/refund_dialog.dart';
import 'package:enaklo_pos/presentation/void/pages/void_page.dart';
import 'package:enaklo_pos/presentation/sales/widgets/sales_detail.dart';
import 'package:enaklo_pos/presentation/sales/widgets/sales_list_order.dart';
@ -214,37 +213,14 @@ class _SalesPageState extends State<SalesPage> {
),
],
if (widget.status == 'completed')
BlocBuilder<OrderFormBloc, OrderFormState>(
builder: (context, state) {
return state.maybeWhen(
orElse: () => Button.outlined(
onPressed: () {},
label: 'Refund',
icon: Icon(Icons.autorenew),
),
loaded: (order, selectedItems,
totalVoidOrRefund, isAllSelected) =>
Button.outlined(
onPressed: () {
if (selectedItems.isEmpty) {
AppFlushbar.showError(context,
'Silahkan pilih item yang ingin di refund.');
return;
}
showDialog(
context: context,
builder: (context) => RefundDialog(
order: orderDetail!,
selectedItems: selectedItems,
),
);
},
label: 'Refund',
icon: Icon(Icons.autorenew),
),
);
Button.outlined(
onPressed: () {
context.push(RefundPage(
selectedOrder: orderDetail!,
));
},
label: 'Refund',
icon: Icon(Icons.autorenew),
),
],
),