2025-08-06 00:53:02 +07:00

819 lines
29 KiB
Dart

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(
orderId: 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,
),
);
}
}