2025-08-04 23:13:52 +07:00
|
|
|
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 = [
|
2025-08-07 20:11:57 +07:00
|
|
|
{
|
|
|
|
|
'value': 'Barang Rusak',
|
|
|
|
|
'icon': Icons.broken_image,
|
|
|
|
|
'color': AppColors.primary
|
|
|
|
|
},
|
2025-08-04 23:13:52 +07:00
|
|
|
{'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
|
|
|
|
|
},
|
2025-08-07 20:11:57 +07:00
|
|
|
{
|
|
|
|
|
'value': 'Lainnya',
|
|
|
|
|
'icon': Icons.more_horiz,
|
|
|
|
|
'color': Colors.red,
|
|
|
|
|
},
|
2025-08-04 23:13:52 +07:00
|
|
|
];
|
|
|
|
|
|
|
|
|
|
@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),
|
|
|
|
|
),
|
|
|
|
|
child: Padding(
|
|
|
|
|
padding: EdgeInsets.all(20),
|
|
|
|
|
child: Column(
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
children: [
|
|
|
|
|
Row(
|
|
|
|
|
children: [
|
|
|
|
|
Container(
|
|
|
|
|
padding: EdgeInsets.all(12),
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
gradient: LinearGradient(
|
2025-08-07 20:11:57 +07:00
|
|
|
colors: [
|
|
|
|
|
const Color.fromARGB(255, 96, 56, 148),
|
|
|
|
|
AppColors.primary
|
|
|
|
|
],
|
2025-08-04 23:13:52 +07:00
|
|
|
),
|
|
|
|
|
borderRadius: BorderRadius.circular(16),
|
|
|
|
|
),
|
|
|
|
|
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),
|
|
|
|
|
),
|
|
|
|
|
child: Padding(
|
|
|
|
|
padding: EdgeInsets.all(20),
|
|
|
|
|
child: Column(
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
children: [
|
|
|
|
|
Row(
|
|
|
|
|
children: [
|
|
|
|
|
Container(
|
|
|
|
|
padding: EdgeInsets.all(12),
|
|
|
|
|
decoration: BoxDecoration(
|
2025-08-07 20:11:57 +07:00
|
|
|
gradient: LinearGradient(
|
|
|
|
|
colors: [
|
|
|
|
|
const Color.fromARGB(255, 96, 56, 148),
|
|
|
|
|
AppColors.primary
|
|
|
|
|
],
|
|
|
|
|
),
|
2025-08-04 23:13:52 +07:00
|
|
|
borderRadius: BorderRadius.circular(12),
|
|
|
|
|
),
|
|
|
|
|
child: Icon(
|
2025-08-07 20:11:57 +07:00
|
|
|
Icons.account_balance_wallet_outlined,
|
|
|
|
|
color: AppColors.white,
|
2025-08-04 23:13:52 +07:00
|
|
|
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),
|
|
|
|
|
),
|
|
|
|
|
child: Padding(
|
|
|
|
|
padding: EdgeInsets.all(20),
|
|
|
|
|
child: Column(
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
children: [
|
|
|
|
|
Row(
|
|
|
|
|
children: [
|
|
|
|
|
Container(
|
|
|
|
|
padding: EdgeInsets.all(12),
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
gradient: LinearGradient(
|
2025-08-07 20:11:57 +07:00
|
|
|
colors: [
|
|
|
|
|
const Color.fromARGB(255, 96, 56, 148),
|
|
|
|
|
AppColors.primary
|
|
|
|
|
],
|
2025-08-04 23:13:52 +07:00
|
|
|
),
|
|
|
|
|
borderRadius: BorderRadius.circular(16),
|
|
|
|
|
),
|
|
|
|
|
child: Icon(
|
2025-08-07 20:11:57 +07:00
|
|
|
Icons.assignment_return_outlined,
|
2025-08-04 23:13:52 +07:00
|
|
|
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,
|
|
|
|
|
);
|
|
|
|
|
|
2025-08-07 20:11:57 +07:00
|
|
|
return SizedBox(
|
2025-08-04 23:13:52 +07:00
|
|
|
width: double.infinity,
|
|
|
|
|
height: 64,
|
|
|
|
|
child: ElevatedButton(
|
|
|
|
|
onPressed:
|
|
|
|
|
isLoading ? null : () => _processRefund(context),
|
|
|
|
|
style: ElevatedButton.styleFrom(
|
2025-08-07 20:11:57 +07:00
|
|
|
backgroundColor: AppColors.primary,
|
2025-08-04 23:13:52 +07:00
|
|
|
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(
|
2025-08-06 00:53:02 +07:00
|
|
|
orderId: widget.selectedOrder.id ??
|
2025-08-04 23:13:52 +07:00
|
|
|
'', // 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,
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|