apskel-pos-flutter/lib/presentation/home/pages/confirm_payment_page.dart
2025-08-06 19:08:59 +07:00

1084 lines
54 KiB
Dart

// ignore_for_file: public_member_api_docs, sort_constructors_first
import 'dart:developer';
import 'package:enaklo_pos/core/components/dashed_divider.dart';
import 'package:enaklo_pos/core/components/flushbar.dart';
import 'package:enaklo_pos/core/extensions/build_context_ext.dart';
import 'package:enaklo_pos/data/models/response/customer_response_model.dart';
import 'package:enaklo_pos/data/models/response/delivery_response_model.dart';
import 'package:enaklo_pos/presentation/home/bloc/order_form/order_form_bloc.dart';
import 'package:enaklo_pos/presentation/home/dialog/save_dialog.dart';
import 'package:enaklo_pos/presentation/home/models/order_type.dart';
import 'package:enaklo_pos/presentation/home/models/product_quantity.dart';
import 'package:enaklo_pos/presentation/home/widgets/confirm_payment_title.dart';
import 'package:enaklo_pos/presentation/home/widgets/customer_auto_complete_field.dart';
import 'package:enaklo_pos/presentation/success/pages/success_order_page.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.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/response/table_model.dart';
import 'package:enaklo_pos/presentation/home/bloc/payment_methods/payment_methods_bloc.dart';
import 'package:enaklo_pos/data/models/response/payment_methods_response_model.dart';
import '../../../core/components/buttons.dart';
import '../../../core/components/spaces.dart';
import '../../../core/constants/colors.dart';
import '../bloc/checkout/checkout_bloc.dart';
import '../widgets/order_menu.dart';
class ConfirmPaymentPage extends StatefulWidget {
final bool isTable;
final TableModel? table;
const ConfirmPaymentPage({
super.key,
required this.isTable,
this.table,
});
@override
State<ConfirmPaymentPage> createState() => _ConfirmPaymentPageState();
}
class _ConfirmPaymentPageState extends State<ConfirmPaymentPage> {
final totalPriceController = TextEditingController();
final customerController = TextEditingController();
bool isPayNow = true;
bool isAddToOrder = false;
PaymentMethod? selectedPaymentMethod;
TableModel? selectTable;
int discountAmount = 0;
int priceValue = 0;
int uangPas = 0;
int uangPas2 = 0;
int uangPas3 = 0;
Customer? selectedCustomer;
// int discountAmountValue = 0;
int totalPriceFinal = 0;
// int taxFinal = 0;
// int serviceChargeFinal = 0;
@override
void initState() {
// Fetch available tables by default
context
.read<PaymentMethodsBloc>()
.add(PaymentMethodsEvent.fetchPaymentMethods());
// Set a default payment method in case API fails
selectedPaymentMethod = PaymentMethod(
id: "4b1c0d21-c98a-4fc0-a2f9-8d90a0c9d905",
organizationId: "3e8b1793-d18b-40c4-a03d-0c6480b630c7",
name: "CASH",
type: "cash",
isActive: true,
createdAt: DateTime.tryParse('2025-07-18T03:43:13.857048+07:00'),
updatedAt: DateTime.tryParse('2025-07-18T03:43:13.857048+07:00'),
);
// if (selectTable == null && widget.table != null) {
// selectTable = tables.firstWhere(
// (t) => t.id == widget.table!.id,
// orElse: () => null,
// );
// }
if (widget.table != null) {
// selectTable = TableModel(
// tableNumber: widget.table!.tableNumber,
// startTime: widget.table!.startTime,
// status: widget.table!.status,
// orderId: widget.table!.orderId,
// paymentAmount: widget.table!.paymentAmount,
// position: widget.table!.position,
// );
}
super.initState();
}
@override
void dispose() {
totalPriceController.dispose();
customerController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return SafeArea(
child: Hero(
tag: 'payment_confirmation_screen',
child: Scaffold(
backgroundColor: AppColors.white,
body: Row(
children: [
Expanded(
flex: 3,
child: Align(
alignment: Alignment.topCenter,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ConfirmPaymentTitle(
isBack: false,
title: 'Konfirmasi',
subtitle: widget.isTable
? 'Orders Table ${widget.table?.tableName}'
: 'Orders #1',
actionWidget: [
BlocBuilder<CheckoutBloc, CheckoutState>(
builder: (context, state) {
return state.maybeWhen(
orElse: () => const SizedBox(),
loaded: (
products,
discountModel,
discount,
discountAmount,
tax,
serviceCharge,
totalQuantity,
totalPrice,
draftName,
orderType,
deliveryType,
) {
return Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8.0),
border: Border.all(
color: AppColors.primary,
width: 1.0,
),
),
padding: const EdgeInsets.all(8.0),
child: Text(
orderType.value,
style: TextStyle(
color: AppColors.primary,
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
);
},
);
},
),
],
),
Container(
padding: const EdgeInsets.all(16.0).copyWith(bottom: 8),
decoration: const BoxDecoration(
border: Border(
bottom: BorderSide(
color: AppColors.grey,
width: 1.0,
),
),
),
child: const Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Item',
style: TextStyle(
color: AppColors.primary,
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
SizedBox(
width: 160,
),
SizedBox(
width: 50.0,
child: Text(
'Qty',
style: TextStyle(
color: AppColors.primary,
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
SizedBox(
child: Text(
'Price',
style: TextStyle(
color: AppColors.primary,
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
],
),
),
Expanded(
child: BlocBuilder<CheckoutBloc, CheckoutState>(
builder: (context, state) {
return state.maybeWhen(
orElse: () => const Center(
child: Text('No Items'),
),
loaded: (
products,
discountModel,
discount,
discountAmount,
tax,
serviceCharge,
totalQuantity,
totalPrice,
draftName,
orderType,
deliveryType,
) {
if (products.isEmpty) {
return const Center(
child: Text('No Items'),
);
}
return ListView.separated(
shrinkWrap: true,
padding: const EdgeInsets.all(16.0)
.copyWith(top: 8.0),
itemBuilder: (context, index) =>
OrderMenu(data: products[index]),
separatorBuilder: (context, index) =>
const SpaceHeight(12.0),
itemCount: products.length,
);
},
);
},
),
),
Container(
padding: const EdgeInsets.all(16.0),
decoration: const BoxDecoration(
border: Border(
top: BorderSide(
color: AppColors.grey,
width: 1.0,
),
),
),
child: Column(
children: [
BlocBuilder<CheckoutBloc, CheckoutState>(
builder: (context, state) {
return state.maybeWhen(
orElse: () => const SizedBox.shrink(),
loaded: (
products,
discountModel,
discount,
discountAmount,
tax,
serviceCharge,
totalQuantity,
totalPrice,
draftName,
orderType,
deliveryType,
) {
if (deliveryType == null) {
return const SizedBox.shrink();
}
return Column(
children: [
Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Text(
'Pengiriman',
style: TextStyle(
color: AppColors.black,
fontWeight: FontWeight.w600,
),
),
Text(
deliveryType.name,
style: TextStyle(
color: AppColors.black,
fontWeight: FontWeight.w600,
),
),
],
),
const SpaceHeight(8.0),
DashedDivider(
color: AppColors.grey,
),
const SpaceHeight(8.0),
],
);
});
},
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Sub total',
style: TextStyle(
color: AppColors.black,
fontWeight: FontWeight.w600,
),
),
BlocBuilder<CheckoutBloc, CheckoutState>(
builder: (context, state) {
final price = state.maybeWhen(
orElse: () => 0,
loaded: (
products,
discountModel,
discount,
discountAmount,
tax,
serviceCharge,
totalQuantity,
totalPrice,
draftName,
orderType,
deliveryType,
) =>
products.fold(
0,
(previousValue, element) =>
previousValue +
(element.product.price! *
element.quantity),
));
return Text(
price.currencyFormatRp,
style: TextStyle(
color: AppColors.black,
fontWeight: FontWeight.w600,
),
);
},
),
],
),
const SpaceHeight(8.0),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'Pajak PB1',
style: TextStyle(
color: AppColors.black,
fontWeight: FontWeight.w400,
),
),
BlocBuilder<CheckoutBloc, CheckoutState>(
builder: (context, state) {
final tax = state.maybeWhen(
orElse: () => 0,
loaded: (
products,
discountModel,
discount,
discountAmount,
tax,
serviceCharge,
totalQuantity,
totalPrice,
draftName,
orderType,
deliveryType,
) =>
tax,
);
final price = state.maybeWhen(
orElse: () => 0,
loaded: (
products,
discountModel,
discount,
discountAmount,
tax,
serviceCharge,
totalQuantity,
totalPrice,
draftName,
orderType,
deliveryType,
) =>
products.fold(
0,
(previousValue, element) =>
previousValue +
(element.product.price! *
element.quantity),
),
);
final discount = state.maybeWhen(
orElse: () => 0,
loaded: (
products,
discountModel,
discount,
discountAmount,
tax,
serviceCharge,
totalQuantity,
totalPrice,
draftName,
orderType,
deliveryType,
) {
if (discountModel == null) {
return 0;
}
return discountModel.value!
.replaceAll('.00', '')
.toIntegerFromText;
});
final subTotal =
price - (discount / 100 * price);
final finalTax = subTotal * (tax / 100);
// final finalDiscount = discount / 100 * subTotal;
// discountAmountValue = finalDiscount.toInt();
// taxFinal = finalTax.toInt();
return Text(
'$tax % (${finalTax.toInt().currencyFormatRp})',
style: TextStyle(
color: AppColors.black,
fontWeight: FontWeight.w500,
),
);
},
),
],
),
const SpaceHeight(8.0),
DashedDivider(
color: AppColors.grey,
),
const SpaceHeight(8.0),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'Total',
style: TextStyle(
color: AppColors.black,
fontWeight: FontWeight.bold,
fontSize: 18,
),
),
BlocBuilder<CheckoutBloc, CheckoutState>(
builder: (context, state) {
final price = state.maybeWhen(
orElse: () => 0,
loaded: (
products,
discountModel,
discount,
discountAmount,
tax,
serviceCharge,
totalQuantity,
totalPrice,
draftName,
orderType,
deliveryType,
) =>
products.fold(
0,
(previousValue, element) =>
previousValue +
(element.product.price! *
element.quantity),
),
);
final discount = state.maybeWhen(
orElse: () => 0,
loaded: (
products,
discountModel,
discount,
discountAmount,
tax,
serviceCharge,
totalQuantity,
totalPrice,
draftName,
orderType,
deliveryType,
) {
if (discountModel == null) {
return 0;
}
return discountModel.value!
.replaceAll('.00', '')
.toIntegerFromText;
});
final tax = state.maybeWhen(
orElse: () => 0,
loaded: (
products,
discountModel,
discount,
discountAmount,
tax,
serviceCharge,
totalQuantity,
totalPrice,
draftName,
orderType,
deliveryType,
) =>
tax,
);
final serviceCharge = state.maybeWhen(
orElse: () => 0,
loaded: (
products,
discountModel,
discount,
discountAmount,
tax,
serviceCharge,
totalQuantity,
totalPrice,
draftName,
orderType,
deliveryType,
) =>
serviceCharge,
);
final subTotal =
price - (discount / 100 * price);
final finalTax = subTotal * (tax / 100);
final service =
(serviceCharge / 100) * subTotal;
final total = subTotal + finalTax + service;
priceValue = total.toInt();
WidgetsBinding.instance
.addPostFrameCallback((_) {
totalPriceController.text =
total.ceil().currencyFormatRpV2;
});
uangPas = total.ceil();
uangPas2 = uangPas ~/ 50000 * 50000 + 50000;
uangPas3 =
uangPas ~/ 50000 * 50000 + 100000;
totalPriceFinal = total.ceil();
// log("totalPriceFinal: $totalPriceFinal");
return Text(
total.ceil().currencyFormatRp,
style: const TextStyle(
color: AppColors.primary,
fontWeight: FontWeight.bold,
fontSize: 18,
),
);
},
),
],
),
],
),
),
],
),
),
),
SpaceWidth(2),
Expanded(
flex: 3,
child: Align(
alignment: Alignment.topCenter,
child: Column(
children: [
ConfirmPaymentTitle(
title: 'Pembayaran',
isBack: false,
subtitle: 'Silahkan lakukan pembayaran',
),
Expanded(
child: SingleChildScrollView(
child: Column(
children: [
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColors.white,
border: Border(
bottom: BorderSide(
color: AppColors.grey,
width: 1.0,
),
),
),
child: CustomerAutocomplete(
controller: customerController,
selectedCustomer: selectedCustomer,
onSelected: (customer) {
setState(() {
selectedCustomer = customer;
});
},
),
),
Container(
padding: const EdgeInsets.all(16),
width: double.infinity,
decoration: BoxDecoration(
color: AppColors.white,
border: Border(
bottom: BorderSide(
color: AppColors.grey,
width: 1.0,
),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Metode Pembayaran',
style: TextStyle(
color: AppColors.black,
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
const SpaceHeight(12.0),
BlocBuilder<PaymentMethodsBloc,
PaymentMethodsState>(
builder: (context, state) {
return state.maybeWhen(
orElse: () => const Center(
child: CircularProgressIndicator(),
),
loading: () => const Center(
child: Column(
children: [
CircularProgressIndicator(),
SizedBox(height: 8.0),
Text(
'Loading payment methods...'),
],
),
),
error: (message) => Column(
children: [
Center(
child: Text(
'Error loading payment methods: $message'),
),
const SpaceHeight(16.0),
Button.filled(
onPressed: () {
context
.read<
PaymentMethodsBloc>()
.add(PaymentMethodsEvent
.fetchPaymentMethods());
},
label: 'Retry',
),
],
),
loaded: (paymentMethods) {
log("Loaded ${paymentMethods.length} payment methods");
paymentMethods.forEach((method) {
log("Payment method: ${method.name} (ID: ${method.id})");
});
if (paymentMethods.isEmpty) {
return Column(
children: [
const Center(
child: Text(
'No payment methods available'),
),
const SpaceHeight(16.0),
Button.filled(
onPressed: () {
context
.read<
PaymentMethodsBloc>()
.add(PaymentMethodsEvent
.fetchPaymentMethods());
},
label: 'Retry',
),
],
);
}
// Set default selected payment method if none selected or if current selection is not in the list
if (selectedPaymentMethod == null ||
!paymentMethods.any((method) =>
method.id ==
selectedPaymentMethod
?.id)) {
selectedPaymentMethod =
paymentMethods.first;
}
return Wrap(
spacing: 12.0,
runSpacing: 8.0,
children:
paymentMethods.map((method) {
final isSelected =
selectedPaymentMethod?.id ==
method.id;
return GestureDetector(
onTap: () {
setState(() {
selectedPaymentMethod =
method;
});
},
child: Container(
height: 60,
width: 80,
alignment: Alignment.center,
padding:
const EdgeInsets.all(
8.0),
decoration: BoxDecoration(
color: isSelected
? AppColors.primary
: AppColors.white,
border: Border.all(
color:
AppColors.primary,
width: 1.0,
),
borderRadius:
BorderRadius.circular(
8.0),
),
child: Text(
method.name ?? "",
style: TextStyle(
color: isSelected
? AppColors.white
: AppColors.primary,
fontWeight:
FontWeight.bold,
),
textAlign:
TextAlign.center,
),
),
);
}).toList(),
);
},
);
},
),
],
),
),
if (selectedPaymentMethod?.type == "cash")
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColors.white,
border: Border(
bottom: BorderSide(
color: AppColors.grey,
width: 1.0,
),
),
),
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
const Text(
'Total Bayar',
style: TextStyle(
color: AppColors.black,
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
const SpaceHeight(8.0),
BlocBuilder<CheckoutBloc, CheckoutState>(
builder: (context, state) {
return TextFormField(
controller: totalPriceController,
keyboardType: TextInputType.number,
decoration: InputDecoration(
border: OutlineInputBorder(
borderRadius:
BorderRadius.circular(8.0),
),
hintText: 'Total harga',
),
onChanged: (value) {
priceValue =
value.toIntegerFromText;
final int newValue =
value.toIntegerFromText;
totalPriceController.text =
newValue.currencyFormatRp;
totalPriceController.selection =
TextSelection.fromPosition(
TextPosition(
offset:
totalPriceController
.text
.length));
},
);
},
),
const SpaceHeight(20.0),
BlocBuilder<CheckoutBloc, CheckoutState>(
builder: (context, state) {
return SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: [
Button.outlined(
width: 150.0,
onPressed: () {
totalPriceController.text =
uangPas
.toString()
.currencyFormatRpV2;
priceValue = uangPas;
},
label: 'UANG PAS',
),
const SpaceWidth(20.0),
Button.outlined(
width: 150.0,
onPressed: () {
totalPriceController.text =
uangPas2
.toString()
.currencyFormatRpV2;
priceValue = uangPas2;
},
label: uangPas2
.toString()
.currencyFormatRpV2,
),
const SpaceWidth(20.0),
Button.outlined(
width: 150.0,
onPressed: () {
totalPriceController.text =
uangPas3
.toString()
.currencyFormatRpV2;
priceValue = uangPas3;
},
label: uangPas3
.toString()
.currencyFormatRpV2,
),
],
),
);
},
),
],
),
),
],
),
),
),
BlocBuilder<CheckoutBloc, CheckoutState>(
builder: (context, state) {
final orderType = state.maybeWhen(
orElse: () => OrderType.dineIn,
loaded: (
products,
discountModel,
discount,
discountAmount,
tax,
serviceCharge,
totalQuantity,
totalPrice,
draftName,
orderType,
deliveryType,
) =>
orderType,
);
DeliveryModel? delivery = state.maybeWhen(
orElse: () => null,
loaded: (
products,
discountModel,
discount,
discountAmount,
tax,
serviceCharge,
totalQuantity,
totalPrice,
draftName,
orderType,
deliveryType,
) =>
deliveryType,
);
List<ProductQuantity> items = state.maybeWhen(
orElse: () => [],
loaded: (
products,
discountModel,
discount,
discountAmount,
tax,
serviceCharge,
totalQuantity,
totalPrice,
draftName,
orderType,
deliveryType,
) =>
products,
);
return Container(
padding: EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColors.white,
border: Border(
top: BorderSide(
color: AppColors.background,
width: 1.0,
),
),
),
child: Row(
children: [
Expanded(
child: Button.outlined(
onPressed: () => context.pop(),
label: 'Batalkan',
),
),
SpaceWidth(12),
Expanded(
child: Button.filled(
onPressed: () {
if (customerController.text == '') {
AppFlushbar.showError(
context,
'Pilih Pelanggan terlebih dahulu',
);
return;
}
showDialog(
context: context,
builder: (dcontext) => SaveDialog(
selectedTable: widget.table,
customerName: customerController.text,
items: items,
orderType: orderType,
customer: selectedCustomer,
deliveryModel: delivery,
),
);
},
label: 'Simpan',
),
),
SpaceWidth(12),
BlocListener<OrderFormBloc, OrderFormState>(
listener: (lcontext, state) {
state.maybeWhen(
orElse: () {},
success: (data) {
context
.pushReplacement(SuccessOrderPage(
productQuantity: items,
order: data,
));
},
error: (message) => AppFlushbar.showError(
context,
message,
),
);
},
child: BlocBuilder<OrderFormBloc,
OrderFormState>(
builder: (context, state) {
return Expanded(
child: state.maybeMap(
orElse: () => Button.filled(
onPressed: () {
if (customerController.text ==
'') {
AppFlushbar.showError(
context,
'Pilih Pelanggan terlebih dahulu',
);
return;
}
context.read<OrderFormBloc>().add(
OrderFormEvent.create(
items: items,
customerName:
customerController
.text,
orderType: orderType,
table: widget.table,
customer:
selectedCustomer,
),
);
},
label: 'Bayar',
),
loading: (_) => Center(
child: CircularProgressIndicator(),
),
),
);
},
),
),
],
),
);
},
),
],
),
),
),
],
),
),
),
);
}
}