split bill

This commit is contained in:
efrilm
2025-10-31 19:24:15 +07:00
parent 7961c9d8c5
commit 6b42cd86ff
14 changed files with 2622 additions and 100 deletions
@@ -0,0 +1,229 @@
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import '../../../../application/split_bill/split_bill_form/split_bill_form_bloc.dart';
import '../../../../common/extension/extension.dart';
import '../../../../common/theme/theme.dart';
import '../../../../domain/order/order.dart';
import '../../../components/border/dashed_border.dart';
import '../../../components/page/page_title.dart';
import '../../../components/spaces/space.dart';
class SplitBillLeftPanel extends StatelessWidget {
final SplitBillFormState state;
const SplitBillLeftPanel({super.key, required this.state});
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(color: AppColor.white),
child: Column(
children: [
PageTitle(
title: 'Ringkasan Pesanan',
subtitle: state.order.orderNumber,
),
SpaceHeight(16),
Expanded(
child: ListView.builder(
padding: EdgeInsets.symmetric(horizontal: 16),
itemCount: state.pendingItems.length,
itemBuilder: (context, index) {
return _buildOrderItem(state.pendingItems[index]);
},
),
),
DashedDivider(color: AppColor.border),
Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
if (state.order.subtotal != state.order.totalAmount) ...[
_buildSummaryItem(
title: 'Subtotal',
value: state.order.subtotal.currencyFormatRpV2,
),
if ((state.order.taxAmount) > 0)
_buildSummaryItem(
title: 'Pajax',
value: state.order.taxAmount.currencyFormatRpV2,
),
if ((state.order.discountAmount) > 0)
_buildSummaryItem(
title: 'Diskon',
value: state.order.discountAmount.currencyFormatRpV2,
),
SpaceHeight(8),
],
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Total terbayar',
style: AppStyle.md.copyWith(
color: AppColor.textPrimary,
fontWeight: FontWeight.w600,
),
),
Text(
state.order.totalPaid.currencyFormatRpV2,
style: AppStyle.md.copyWith(
color: AppColor.textPrimary,
fontWeight: FontWeight.w600,
),
),
],
),
SpaceHeight(4),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Sisa Tagihan',
style: AppStyle.md.copyWith(
color: AppColor.error,
fontWeight: FontWeight.w600,
),
),
Text(
state.order.remainingAmount.currencyFormatRpV2,
style: AppStyle.md.copyWith(
color: AppColor.error,
fontWeight: FontWeight.w600,
),
),
],
),
SpaceHeight(6),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Total Pembayaran',
style: AppStyle.lg.copyWith(
color: AppColor.primary,
fontWeight: FontWeight.bold,
),
),
Text(
state.order.totalAmount.currencyFormatRpV2,
style: AppStyle.lg.copyWith(
color: AppColor.primary,
fontWeight: FontWeight.bold,
),
),
],
),
],
),
),
],
),
);
}
Row _buildSummaryItem({required String title, required String value}) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(title, style: AppStyle.md.copyWith(color: AppColor.textSecondary)),
Text(value, style: AppStyle.md.copyWith(color: AppColor.textSecondary)),
],
);
}
Widget _buildOrderItem(OrderItem item) {
return Container(
padding: EdgeInsets.symmetric(vertical: 12),
child: Column(
children: [
Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
item.productName,
style: AppStyle.lg.copyWith(
fontWeight: FontWeight.w600,
color: AppColor.textPrimary,
),
),
if (item.productVariantName.isNotEmpty)
Text(
item.productVariantName,
style: AppStyle.md.copyWith(
color: AppColor.textSecondary,
fontStyle: FontStyle.italic,
),
),
Text(
'Qty: ${item.quantity} x Rp ${item.unitPrice.currencyFormatRpV2} ',
style: AppStyle.md.copyWith(
color: AppColor.textSecondary,
),
),
],
),
),
Text(
item.totalPrice.currencyFormatRpV2,
style: AppStyle.lg.copyWith(
fontWeight: FontWeight.w600,
color: AppColor.textPrimary,
),
),
],
),
if ((item.paidQuantity) > 1) ...[
SpaceHeight(6),
Align(
alignment: Alignment.centerRight,
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 4,
),
decoration: BoxDecoration(
color: AppColor.primary.withOpacity(0.2),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: AppColor.primary),
),
child: RichText(
text: TextSpan(
children: [
TextSpan(
text: '${item.paidQuantity} ',
style: AppStyle.sm.copyWith(
fontWeight: FontWeight.w600,
color: AppColor.primary,
),
),
TextSpan(
text: 'dari ',
style: AppStyle.sm.copyWith(color: AppColor.primary),
),
TextSpan(
text: '${item.quantity} ',
style: AppStyle.sm.copyWith(
fontWeight: FontWeight.w600,
color: AppColor.primary,
),
),
TextSpan(
text: 'kuantiti telah dibayar.',
style: AppStyle.sm.copyWith(color: AppColor.primary),
),
],
),
),
),
),
],
],
),
);
}
}
@@ -0,0 +1,547 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../application/split_bill/split_bill_form/split_bill_form_bloc.dart';
import '../../../../common/extension/extension.dart';
import '../../../../common/theme/theme.dart';
import '../../../../common/types/split_type.dart';
import '../../../../domain/order/order.dart';
import '../../../components/button/button.dart';
import '../../../components/field/field.dart';
import '../../../components/spaces/space.dart';
import '../../../components/toast/flushbar.dart';
import '../../../router/app_router.gr.dart';
class SplitBillRightPanel extends StatefulWidget {
final SplitBillFormState state;
const SplitBillRightPanel({super.key, required this.state});
@override
State<SplitBillRightPanel> createState() => _SplitBillRightPanelState();
}
class _SplitBillRightPanelState extends State<SplitBillRightPanel> {
final TextEditingController _customerController = TextEditingController();
final TextEditingController _amountController = TextEditingController();
@override
void initState() {
super.initState();
_customerController.addListener(() {
context.read<SplitBillFormBloc>().add(
SplitBillFormEvent.customerNameChanged(_customerController.text),
);
});
}
@override
void dispose() {
super.dispose();
_customerController.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
children: [
Expanded(
child: SingleChildScrollView(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Split Bill',
style: AppStyle.h5.copyWith(
fontWeight: FontWeight.bold,
color: AppColor.textPrimary,
),
),
SpaceHeight(16),
_buildCustomer(context),
SpaceHeight(16),
_buildRowButtonSplit(),
SpaceHeight(16),
Container(
child: widget.state.splitType.isItem
? _buildPerProductSplit()
: _buildPerAmountSplit(),
),
SpaceHeight(16),
],
),
),
),
_buildBottom(),
],
);
}
Widget _buildBottom() {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(color: AppColor.white),
child: Row(
children: [
Expanded(
child: AppElevatedButton.outlined(
onPressed: () => context.router.maybePop(),
label: 'Batal',
),
),
SpaceWidth(16),
Expanded(
child: AppElevatedButton.filled(
onPressed: () => _confirmSplit(),
label: 'Konfirmasi Split',
),
),
],
),
);
}
Widget _buildPerAmountSplit() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Masukkan Jumlah yang Akan Dibayar',
style: AppStyle.xl.copyWith(
fontWeight: FontWeight.bold,
color: AppColor.textPrimary,
),
),
SizedBox(height: 16),
Column(
children: [
Container(
padding: EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColor.white,
border: Border.all(color: AppColor.border),
borderRadius: BorderRadius.circular(8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Split Bill',
style: AppStyle.lg.copyWith(
fontWeight: FontWeight.bold,
color: AppColor.primary,
),
),
SizedBox(height: 16),
Text(
'Jumlah yang akan dibayar:',
style: AppStyle.md.copyWith(color: AppColor.textSecondary),
),
SizedBox(height: 8),
Container(
decoration: BoxDecoration(
border: Border.all(color: AppColor.border),
borderRadius: BorderRadius.circular(8),
),
child: TextField(
controller: _amountController,
keyboardType: TextInputType.number,
style: AppStyle.lg.copyWith(
fontWeight: FontWeight.w600,
color: AppColor.textPrimary,
),
onChanged: (value) {
context.read<SplitBillFormBloc>().add(
SplitBillFormEvent.amountChanged(
int.tryParse(value) ?? 0,
),
);
// setState(() {
// splitAmount = int.tryParse(value) ?? 0;
// });
},
decoration: InputDecoration(
hintText: '0',
prefixText: 'Rp ',
prefixStyle: AppStyle.lg.copyWith(
fontWeight: FontWeight.w600,
color: AppColor.textPrimary,
),
border: InputBorder.none,
contentPadding: EdgeInsets.all(16),
),
),
),
SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Total yang dibayar:',
style: AppStyle.lg.copyWith(
fontWeight: FontWeight.w600,
color: AppColor.textPrimary,
),
),
Text(
widget.state.totalAmount.currencyFormatRpV2,
style: AppStyle.lg.copyWith(
fontWeight: FontWeight.w600,
color: AppColor.primary,
),
),
],
),
SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Sisa bill:',
style: AppStyle.md.copyWith(
color: AppColor.textSecondary,
),
),
Text(
(widget.state.order.remainingAmount -
widget.state.totalAmount)
.currencyFormatRpV2,
style: AppStyle.md.copyWith(
color: AppColor.textSecondary,
),
),
],
),
],
),
),
],
),
],
);
}
Widget _buildPerProductSplit() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Pilih Produk untuk Split',
style: AppStyle.xl.copyWith(
fontWeight: FontWeight.bold,
color: AppColor.textPrimary,
),
),
SizedBox(height: 16),
Container(
padding: EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColor.white,
border: Border.all(color: AppColor.border),
borderRadius: BorderRadius.circular(8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Split Bill',
style: AppStyle.lg.copyWith(
fontWeight: FontWeight.bold,
color: AppColor.primary,
),
),
SizedBox(height: 16),
ListView.builder(
itemCount: widget.state.pendingItems.length,
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
itemBuilder: (context, index) {
return _buildProductSplitItem(
widget.state.pendingItems[index],
);
},
),
Container(
width: double.infinity,
height: 1,
color: AppColor.border,
margin: EdgeInsets.symmetric(vertical: 12),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Total Split:',
style: AppStyle.lg.copyWith(
fontWeight: FontWeight.w600,
color: AppColor.textPrimary,
),
),
Text(
widget.state.totalAmount.currencyFormatRpV2,
style: AppStyle.lg.copyWith(
fontWeight: FontWeight.w600,
color: AppColor.primary,
),
),
],
),
],
),
),
],
);
}
Widget _buildProductSplitItem(OrderItem item) {
int selectedQty = widget.state.selectedProducts[item.id] ?? 0;
int maxQty = (item.quantity) - (item.paidQuantity);
return Container(
margin: EdgeInsets.only(bottom: 12),
padding: EdgeInsets.all(12),
decoration: BoxDecoration(
color: selectedQty > 0
? AppColor.primary.withOpacity(0.1)
: Colors.transparent,
border: Border.all(
color: selectedQty > 0 ? AppColor.primary : AppColor.border,
),
borderRadius: BorderRadius.circular(6),
),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
item.productName,
style: AppStyle.md.copyWith(
fontWeight: FontWeight.w600,
color: AppColor.textPrimary,
),
),
if (item.productVariantName.isNotEmpty)
Text(
item.productVariantName,
style: AppStyle.sm.copyWith(
color: AppColor.textSecondary,
fontStyle: FontStyle.italic,
),
),
Text(
'${item.unitPrice.currencyFormatRpV2} per item',
style: AppStyle.sm.copyWith(color: AppColor.textSecondary),
),
],
),
),
Row(
children: [
GestureDetector(
onTap: () {
if (selectedQty > 0) {
setState(() {
context.read<SplitBillFormBloc>().add(
SplitBillFormEvent.itemQuantityChanged(
itemId: item.id,
quantity: selectedQty - 1,
),
);
});
}
},
child: Container(
width: 32,
height: 32,
decoration: BoxDecoration(
color: selectedQty > 0 ? AppColor.primary : AppColor.border,
borderRadius: BorderRadius.circular(4),
),
child: Icon(
Icons.remove,
size: 16,
color: selectedQty > 0
? Colors.white
: AppColor.textSecondary,
),
),
),
Container(
width: 40,
height: 32,
margin: EdgeInsets.symmetric(horizontal: 8),
decoration: BoxDecoration(
border: Border.all(color: AppColor.border),
borderRadius: BorderRadius.circular(4),
),
child: Center(
child: Text(
'$selectedQty',
style: AppStyle.md.copyWith(
fontWeight: FontWeight.bold,
color: AppColor.textPrimary,
),
),
),
),
GestureDetector(
onTap: () {
context.read<SplitBillFormBloc>().add(
SplitBillFormEvent.itemQuantityChanged(
itemId: item.id,
quantity: selectedQty + 1,
),
);
},
child: Container(
width: 32,
height: 32,
decoration: BoxDecoration(
color: selectedQty < maxQty
? AppColor.primary
: AppColor.border,
borderRadius: BorderRadius.circular(4),
),
child: Icon(
Icons.add,
size: 16,
color: selectedQty < maxQty
? Colors.white
: AppColor.textSecondary,
),
),
),
],
),
],
),
);
}
Widget _buildRowButtonSplit() {
switch (widget.state.order.splitType) {
case 'AMOUNT':
return _buildSplitTypeButton('Per Jumlah', SplitType.amount);
case 'ITEM':
return _buildSplitTypeButton('Per Produk', SplitType.item);
default:
return Row(
children: [
_buildSplitTypeButton('Per Produk', SplitType.item),
SizedBox(width: 16),
_buildSplitTypeButton('Per Jumlah', SplitType.amount),
],
);
}
}
Widget _buildSplitTypeButton(String title, SplitType type) {
bool isSelected = widget.state.splitType == type;
return GestureDetector(
onTap: () {
_amountController.clear();
context.read<SplitBillFormBloc>().add(
SplitBillFormEvent.splitTypeChanged(type),
);
},
child: Container(
padding: EdgeInsets.symmetric(horizontal: 24, vertical: 12),
decoration: BoxDecoration(
color: isSelected ? AppColor.primary : Colors.transparent,
border: Border.all(
color: isSelected ? AppColor.primary : AppColor.border,
width: 2,
),
borderRadius: BorderRadius.circular(8),
),
child: Text(
title,
style: AppStyle.lg.copyWith(
fontWeight: FontWeight.w600,
color: isSelected ? AppColor.white : AppColor.textPrimary,
),
),
),
);
}
Widget _buildCustomer(BuildContext context) {
return Container(
padding: EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColor.white,
borderRadius: BorderRadius.circular(12),
),
child: CustomerAutocomplete(
controller: _customerController,
selectedCustomer: widget.state.customer,
onSelected: (customer) {
context.read<SplitBillFormBloc>().add(
SplitBillFormEvent.customerChanged(customer!),
);
},
),
);
}
void _confirmSplit() {
if (widget.state.splitType.isItem) {
int splitTotal = widget.state.totalAmount;
if (splitTotal > 0) {
List<OrderItem> splitItems = [];
widget.state.selectedProducts.forEach((itemId, quantity) {
OrderItem? originalItem = widget.state.pendingItems.firstWhere(
(item) => item.id == itemId,
orElse: () => OrderItem.empty(),
);
// ignore: unnecessary_null_comparison
if (originalItem != null && originalItem.id != null) {
splitItems.add(
originalItem.copyWith(
quantity: quantity,
totalPrice: (originalItem.unitPrice) * quantity,
),
);
}
});
context.router.push(
PaymentRoute(
order: widget.state.order.copyWith(
orderItems: splitItems,
subtotal: splitTotal,
totalAmount: splitTotal,
),
),
);
} else {
AppFlushbar.showError(context, "Pilih minimal satu produk untuk split");
}
} else {
int totalAmount =
(widget.state.order.totalAmount) - (widget.state.order.totalPaid);
if (widget.state.totalAmount > 0 &&
widget.state.totalAmount <= totalAmount) {
context.router.push(
PaymentRoute(
order: widget.state.order.copyWith(
subtotal: widget.state.totalAmount,
totalAmount: widget.state.totalAmount,
),
),
);
} else if (widget.state.totalAmount > totalAmount) {
AppFlushbar.showError(
context,
"Jumlah split tidak boleh melebihi total bill",
);
} else {
AppFlushbar.showError(context, "Total bayar harus lebih dari 0");
}
}
}
}