feat: refund page
This commit is contained in:
parent
2912a438e3
commit
1b1d01c1e8
@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
40
lib/presentation/refund/bloc/refund_bloc.dart
Normal file
40
lib/presentation/refund/bloc/refund_bloc.dart
Normal 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()),
|
||||
);
|
||||
}
|
||||
}
|
||||
855
lib/presentation/refund/bloc/refund_bloc.freezed.dart
Normal file
855
lib/presentation/refund/bloc/refund_bloc.freezed.dart
Normal 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;
|
||||
}
|
||||
10
lib/presentation/refund/bloc/refund_event.dart
Normal file
10
lib/presentation/refund/bloc/refund_event.dart
Normal 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;
|
||||
}
|
||||
9
lib/presentation/refund/bloc/refund_state.dart
Normal file
9
lib/presentation/refund/bloc/refund_state.dart
Normal 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;
|
||||
}
|
||||
90
lib/presentation/refund/dialog/refund_error_dialog.dart
Normal file
90
lib/presentation/refund/dialog/refund_error_dialog.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
131
lib/presentation/refund/dialog/refund_success_dialog.dart
Normal file
131
lib/presentation/refund/dialog/refund_success_dialog.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
818
lib/presentation/refund/pages/refund_page.dart
Normal file
818
lib/presentation/refund/pages/refund_page.dart
Normal 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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
104
lib/presentation/refund/widgets/refund_appbar.dart
Normal file
104
lib/presentation/refund/widgets/refund_appbar.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
57
lib/presentation/refund/widgets/refund_info_tile.dart
Normal file
57
lib/presentation/refund/widgets/refund_info_tile.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
130
lib/presentation/refund/widgets/refund_order_Item_tile.dart
Normal file
130
lib/presentation/refund/widgets/refund_order_Item_tile.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
53
lib/presentation/refund/widgets/refund_reason_tile.dart
Normal file
53
lib/presentation/refund/widgets/refund_reason_tile.dart
Normal 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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user