2025-08-08 10:53:46 +07:00

485 lines
19 KiB
Dart

import 'package:enaklo_pos/core/components/buttons.dart';
import 'package:enaklo_pos/core/components/flushbar.dart';
import 'package:enaklo_pos/core/constants/colors.dart';
import 'package:enaklo_pos/core/extensions/build_context_ext.dart';
import 'package:enaklo_pos/core/extensions/int_ext.dart';
import 'package:enaklo_pos/core/extensions/string_ext.dart';
import 'package:enaklo_pos/data/models/request/payment_request.dart';
import 'package:enaklo_pos/data/models/response/order_response_model.dart';
import 'package:enaklo_pos/data/models/response/payment_methods_response_model.dart';
import 'package:enaklo_pos/data/models/response/product_response_model.dart';
import 'package:enaklo_pos/presentation/home/bloc/payment_methods/payment_methods_bloc.dart';
import 'package:enaklo_pos/presentation/home/models/product_quantity.dart';
import 'package:enaklo_pos/presentation/sales/blocs/payment_form/payment_form_bloc.dart';
import 'package:enaklo_pos/presentation/success/pages/success_payment_page.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class PaymentPage extends StatefulWidget {
final Order order;
final bool isSplit;
final String? splitType;
final String? customerId;
const PaymentPage({
super.key,
required this.order,
this.isSplit = false,
this.splitType,
this.customerId,
});
@override
State<PaymentPage> createState() => _PaymentPageState();
}
class _PaymentPageState extends State<PaymentPage> {
PaymentMethod? selectedPaymentMethod;
final totalPriceController = TextEditingController();
int priceValue = 0;
late int uangPas;
final int uangPas2 = 50000;
final int uangPas3 = 100000;
List<OrderItem> getOrderItemPending() {
final itemPending = widget.order.orderItems
?.where((item) => item.status == "pending")
.toList();
return itemPending ?? [];
}
@override
void initState() {
super.initState();
uangPas = widget.order.totalAmount ?? 0;
totalPriceController.text = uangPas.currencyFormatRpV2;
priceValue = uangPas;
context
.read<PaymentMethodsBloc>()
.add(PaymentMethodsEvent.fetchPaymentMethods());
}
@override
void dispose() {
totalPriceController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final totalAmount = widget.order.totalAmount ?? 0;
return Scaffold(
backgroundColor: AppColors.background,
appBar: AppBar(
backgroundColor: AppColors.primary,
title: const Text(
'Pembayaran',
style: TextStyle(color: AppColors.white),
),
leading: IconButton(
onPressed: () => context.pop(),
icon: Icon(
Icons.arrow_back,
color: AppColors.white,
),
),
),
body: LayoutBuilder(
builder: (context, constraints) {
final isWide = constraints.maxWidth > 800;
return Padding(
padding: const EdgeInsets.all(16),
child: isWide
? Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(flex: 3, child: _buildOrderDetail()),
const SizedBox(width: 24),
Expanded(flex: 2, child: _buildPaymentForm(totalAmount)),
],
)
: SingleChildScrollView(
child: Column(
children: [
_buildOrderDetail(),
const SizedBox(height: 24),
_buildPaymentForm(totalAmount),
],
),
),
);
},
),
);
}
Widget _buildOrderDetail() {
final order = widget.order;
final items = order.orderItems ?? [];
return Card(
elevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
color: AppColors.white,
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Detail Order',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: AppColors.primary)),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('No. Pesanan',
style: TextStyle(fontWeight: FontWeight.w600)),
Text(order.orderNumber ?? '-',
style: const TextStyle(fontWeight: FontWeight.bold)),
],
),
const SizedBox(height: 8),
Divider(color: AppColors.grey),
const SizedBox(height: 8),
Expanded(
child: items.isEmpty
? const Center(child: Text('Tidak ada item'))
: ListView.separated(
shrinkWrap: true,
itemCount: getOrderItemPending().length,
separatorBuilder: (_, __) =>
Divider(color: AppColors.grey.withOpacity(0.5)),
itemBuilder: (context, index) {
final item = getOrderItemPending()[index];
return ListTile(
contentPadding: EdgeInsets.zero,
title: Text(item.productName ?? '-'),
subtitle: Text(
'Qty: ${item.quantity ?? 0} | ${item.productVariantName ?? ''}'),
trailing: Text(
(item.totalPrice ?? 0).currencyFormatRpV2,
style:
const TextStyle(fontWeight: FontWeight.bold)),
);
},
),
),
Divider(color: AppColors.grey),
_buildSummaryRow('Subtotal', order.subtotal ?? 0),
_buildSummaryRow('Pajak', order.taxAmount ?? 0),
_buildSummaryRow('Diskon', order.discountAmount ?? 0),
Divider(thickness: 2, color: AppColors.primary),
_buildSummaryRow('Total', order.totalAmount ?? 0,
isTotal: true, color: AppColors.primary),
],
),
),
);
}
Widget _buildSummaryRow(String label, int amount,
{bool isTotal = false, Color? color}) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(label,
style: TextStyle(
fontWeight: isTotal ? FontWeight.bold : FontWeight.normal,
fontSize: isTotal ? 18 : 14,
color: color ?? Colors.black)),
Text(amount.currencyFormatRpV2,
style: TextStyle(
fontWeight: isTotal ? FontWeight.bold : FontWeight.normal,
fontSize: isTotal ? 18 : 14,
color: color ?? Colors.black)),
],
),
);
}
Widget _buildPaymentForm(int totalAmount) {
return Card(
elevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
color: AppColors.white,
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text('Pembayaran',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: AppColors.primary)),
Expanded(
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 24),
const Text('Metode Pembayaran',
style: TextStyle(fontWeight: FontWeight.w600)),
const SizedBox(height: 12),
BlocBuilder<PaymentMethodsBloc, PaymentMethodsState>(
builder: (context, state) {
return state.maybeWhen(
orElse: () =>
const Center(child: CircularProgressIndicator()),
loading: () => const Center(
child: Column(
children: [
CircularProgressIndicator(),
SizedBox(height: 8),
Text('Loading metode pembayaran...'),
],
),
),
error: (message) => Column(
children: [
Text('Gagal memuat metode pembayaran: $message'),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () {
context.read<PaymentMethodsBloc>().add(
PaymentMethodsEvent
.fetchPaymentMethods());
},
child: const Text('Coba Lagi'),
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primary,
),
),
],
),
loaded: (methods) {
if (methods.isEmpty) {
return Column(
children: [
const Text(
'Tidak ada metode pembayaran tersedia'),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () {
context.read<PaymentMethodsBloc>().add(
PaymentMethodsEvent
.fetchPaymentMethods());
},
child: const Text('Coba Lagi'),
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primary,
),
),
],
);
}
return Wrap(
spacing: 12,
runSpacing: 12,
children: methods.map((method) {
final isSelected =
selectedPaymentMethod?.id == method.id;
return ChoiceChip(
label: Text(method.name ?? ''),
selected: isSelected,
onSelected: (_) {
setState(() {
selectedPaymentMethod = method;
if (method.type != "cash") {
totalPriceController.text =
totalAmount.currencyFormatRpV2;
priceValue = totalAmount;
}
});
},
selectedColor: AppColors.primary,
backgroundColor: AppColors.white,
labelStyle: TextStyle(
color: isSelected
? AppColors.white
: AppColors.primary,
fontWeight: FontWeight.bold,
),
shape: RoundedRectangleBorder(
side: BorderSide(color: AppColors.primary),
borderRadius: BorderRadius.circular(8),
),
padding: const EdgeInsets.symmetric(
horizontal: 16, vertical: 12),
);
}).toList(),
);
},
);
},
),
const SizedBox(height: 24),
if (selectedPaymentMethod?.type == "cash") ...[
const Text('Total Bayar',
style: TextStyle(fontWeight: FontWeight.w600)),
const SizedBox(height: 8),
TextFormField(
controller: totalPriceController,
keyboardType: TextInputType.number,
decoration: InputDecoration(
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8)),
hintText: 'Masukkan nominal bayar',
),
onChanged: (value) {
final newValue = value.toIntegerFromText;
setState(() {
priceValue = newValue;
totalPriceController.text =
newValue.currencyFormatRp;
totalPriceController.selection =
TextSelection.fromPosition(TextPosition(
offset: totalPriceController.text.length));
});
},
),
const SizedBox(height: 20),
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: [
_quickAmountButton('UANG PAS', uangPas),
const SizedBox(width: 16),
_quickAmountButton(
uangPas2.currencyFormatRpV2, uangPas2),
const SizedBox(width: 16),
_quickAmountButton(
uangPas3.currencyFormatRpV2, uangPas3),
],
),
),
],
const SizedBox(height: 32),
],
),
),
),
BlocListener<PaymentFormBloc, PaymentFormState>(
listener: (context, state) {
state.maybeWhen(
orElse: () {},
success: (data) {
context.pushReplacement(SuccessPaymentPage(
productQuantity: widget.order.orderItems
?.map(
(item) => ProductQuantity(
product: Product(
name: item.productName,
price: item.unitPrice,
),
quantity: item.quantity ?? 0,
),
)
.toList() ??
[],
payment: data,
paymentMethod: selectedPaymentMethod?.name ?? '',
nominalBayar: totalPriceController.text.toIntegerFromText,
));
},
error: (message) {
AppFlushbar.showError(context, message);
},
);
},
child: BlocBuilder<PaymentFormBloc, PaymentFormState>(
builder: (context, state) {
return state.maybeWhen(
orElse: () =>
Button.filled(onPressed: _onPayPressed, label: "Bayar"),
loading: () =>
const Center(child: CircularProgressIndicator()),
);
},
),
),
],
),
),
);
}
Widget _quickAmountButton(String label, int amount) {
return OutlinedButton(
onPressed: () {
setState(() {
totalPriceController.text = amount.currencyFormatRpV2;
priceValue = amount;
});
},
style: OutlinedButton.styleFrom(
side: BorderSide(color: AppColors.primary),
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 14),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
child: Text(label,
style:
TextStyle(color: AppColors.primary, fontWeight: FontWeight.bold)),
);
}
void _onPayPressed() {
if (selectedPaymentMethod == null) {
AppFlushbar.showError(context, 'Pilih metode pembayaran terlebih dahulu');
return;
}
if (selectedPaymentMethod?.type == "cash" && priceValue == 0) {
AppFlushbar.showError(context, 'Total bayar tidak boleh 0');
return;
}
final itemPending = widget.order.orderItems
?.where((item) => item.status == "pending")
.toList();
if (widget.isSplit == false) {
final request = PaymentRequestModel(
amount: widget.order.totalAmount ?? 0,
orderId: widget.order.id,
paymentMethodId: selectedPaymentMethod?.id,
splitDescription: '',
splitNumber: 1,
splitTotal: 1,
transactionId: '',
paymentOrderItems: itemPending
?.map((item) => PaymentOrderItemModel(
orderItemId: item.id,
amount: item.totalPrice,
))
.toList(),
);
context.read<PaymentFormBloc>().add(PaymentFormEvent.create(request));
} else {
final request = PaymentSplitBillRequest(
amount: widget.order.totalAmount ?? 0,
customerId: widget.customerId ?? "",
items: itemPending
?.map((item) => SplitItem(
orderItemId: item.id ?? "",
amount: item.unitPrice ?? 0,
))
.toList() ??
[],
orderId: widget.order.id ?? "",
paymentMethodId: selectedPaymentMethod?.id ?? "",
type: widget.splitType ?? "",
);
context.read<PaymentFormBloc>().add(
PaymentFormEvent.createSplitBill(request),
);
}
}
}