update total price

This commit is contained in:
efrilm 2025-10-25 03:03:44 +07:00
parent 013f313e35
commit 0e83d213fc
9 changed files with 401 additions and 60 deletions

View File

@ -46,30 +46,33 @@ class CheckoutFormBloc extends Bloc<CheckoutFormEvent, CheckoutFormState> {
product: e.product,
quantity: 1,
variant: e.variant,
notes: '',
),
);
}
log('🛒 Items updated: ${items.length} items total');
log('🛒 Items updated: ${items.length} items total ${items.toList()}');
final totalQuantity = items.fold<int>(
0,
(sum, item) => sum + item.quantity,
);
final totalPrice = items.fold<int>(
0,
(sum, item) =>
sum +
(item.quantity *
(item.variant?.priceModifier.toInt() ??
item.product.price.toInt())),
);
final totalPrice = state.items.isEmpty
? 0.0
: state.items
.map(
(e) =>
(e.product.price * e.quantity) +
(e.variant?.priceModifier ?? 0),
)
.reduce((value, element) => value + element);
emit(
currentState.copyWith(
items: items,
totalQuantity: totalQuantity,
totalPrice: totalPrice,
totalPrice: totalPrice.toInt(),
isLoading: false,
),
);

View File

@ -6,9 +6,9 @@ class ProductQuantity with _$ProductQuantity {
required Product product,
ProductVariant? variant,
required int quantity,
String? notes,
required String notes,
}) = _ProductQuantity;
factory ProductQuantity.empty() =>
ProductQuantity(product: Product.empty(), quantity: 0);
ProductQuantity(product: Product.empty(), quantity: 0, notes: '');
}

View File

@ -1097,7 +1097,7 @@ mixin _$ProductQuantity {
Product get product => throw _privateConstructorUsedError;
ProductVariant? get variant => throw _privateConstructorUsedError;
int get quantity => throw _privateConstructorUsedError;
String? get notes => throw _privateConstructorUsedError;
String get notes => throw _privateConstructorUsedError;
/// Create a copy of ProductQuantity
/// with the given fields replaced by the non-null parameter values.
@ -1117,7 +1117,7 @@ abstract class $ProductQuantityCopyWith<$Res> {
Product product,
ProductVariant? variant,
int quantity,
String? notes,
String notes,
});
$ProductCopyWith<$Res> get product;
@ -1142,7 +1142,7 @@ class _$ProductQuantityCopyWithImpl<$Res, $Val extends ProductQuantity>
Object? product = null,
Object? variant = freezed,
Object? quantity = null,
Object? notes = freezed,
Object? notes = null,
}) {
return _then(
_value.copyWith(
@ -1158,10 +1158,10 @@ class _$ProductQuantityCopyWithImpl<$Res, $Val extends ProductQuantity>
? _value.quantity
: quantity // ignore: cast_nullable_to_non_nullable
as int,
notes: freezed == notes
notes: null == notes
? _value.notes
: notes // ignore: cast_nullable_to_non_nullable
as String?,
as String,
)
as $Val,
);
@ -1205,7 +1205,7 @@ abstract class _$$ProductQuantityImplCopyWith<$Res>
Product product,
ProductVariant? variant,
int quantity,
String? notes,
String notes,
});
@override
@ -1231,7 +1231,7 @@ class __$$ProductQuantityImplCopyWithImpl<$Res>
Object? product = null,
Object? variant = freezed,
Object? quantity = null,
Object? notes = freezed,
Object? notes = null,
}) {
return _then(
_$ProductQuantityImpl(
@ -1247,10 +1247,10 @@ class __$$ProductQuantityImplCopyWithImpl<$Res>
? _value.quantity
: quantity // ignore: cast_nullable_to_non_nullable
as int,
notes: freezed == notes
notes: null == notes
? _value.notes
: notes // ignore: cast_nullable_to_non_nullable
as String?,
as String,
),
);
}
@ -1265,7 +1265,7 @@ class _$ProductQuantityImpl
required this.product,
this.variant,
required this.quantity,
this.notes,
required this.notes,
});
@override
@ -1275,7 +1275,7 @@ class _$ProductQuantityImpl
@override
final int quantity;
@override
final String? notes;
final String notes;
@override
String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) {
@ -1326,7 +1326,7 @@ abstract class _ProductQuantity implements ProductQuantity {
required final Product product,
final ProductVariant? variant,
required final int quantity,
final String? notes,
required final String notes,
}) = _$ProductQuantityImpl;
@override
@ -1336,7 +1336,7 @@ abstract class _ProductQuantity implements ProductQuantity {
@override
int get quantity;
@override
String? get notes;
String get notes;
/// Create a copy of ProductQuantity
/// with the given fields replaced by the non-null parameter values.

View File

@ -3,6 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import '../application/auth/auth_bloc.dart';
import '../application/category/category_loader/category_loader_bloc.dart';
import '../application/checkout/checkout_form/checkout_form_bloc.dart';
import '../application/outlet/outlet_loader/outlet_loader_bloc.dart';
import '../application/product/product_loader/product_loader_bloc.dart';
import '../common/theme/theme.dart';
@ -29,6 +30,7 @@ class _AppWidgetState extends State<AppWidget> {
BlocProvider(create: (context) => getIt<OutletLoaderBloc>()),
BlocProvider(create: (context) => getIt<CategoryLoaderBloc>()),
BlocProvider(create: (context) => getIt<ProductLoaderBloc>()),
BlocProvider(create: (context) => getIt<CheckoutFormBloc>()),
],
child: MaterialApp.router(
debugShowCheckedModeBanner: false,

View File

@ -0,0 +1,156 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../application/checkout/checkout_form/checkout_form_bloc.dart';
import '../../../common/extension/extension.dart';
import '../../../common/theme/theme.dart';
import '../../../domain/product/product.dart';
import '../image/image.dart';
import '../spaces/space.dart';
class OrderMenu extends StatefulWidget {
final ProductQuantity data;
const OrderMenu({super.key, required this.data});
@override
State<OrderMenu> createState() => _OrderMenuState();
}
class _OrderMenuState extends State<OrderMenu> {
final _controller = TextEditingController();
@override
void initState() {
super.initState();
_controller.text = widget.data.notes;
_controller.addListener(() {
context.read<CheckoutFormBloc>().add(
CheckoutFormEvent.updateItemNotes(
widget.data.product,
_controller.text,
),
);
});
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
children: [
Row(
children: [
Flexible(
child: ListTile(
contentPadding: EdgeInsets.zero,
leading: ClipRRect(
borderRadius: BorderRadius.all(Radius.circular(8.0)),
child: AppNetworkImage(
url: widget.data.product.imageUrl,
width: 50.0,
height: 50.0,
fit: BoxFit.cover,
),
),
title: Row(
children: [
Expanded(
child: Text(
widget.data.product.name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
),
],
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
(widget.data.product.price +
(widget.data.variant?.priceModifier ?? 0))
.currencyFormatRp,
),
if (widget.data.variant != null)
Text(widget.data.variant?.name ?? ""),
],
),
),
),
Row(
children: [
GestureDetector(
onTap: () {
context.read<CheckoutFormBloc>().add(
CheckoutFormEvent.removeItem(
widget.data.product,
widget.data.variant,
),
);
},
child: Container(
width: 30,
height: 30,
color: AppColor.white,
child: const Icon(
Icons.remove_circle,
color: AppColor.primary,
),
),
),
SizedBox(
width: 30.0,
child: Center(child: Text(widget.data.quantity.toString())),
),
GestureDetector(
onTap: () {
context.read<CheckoutFormBloc>().add(
CheckoutFormEvent.addItem(
widget.data.product,
widget.data.variant,
),
);
},
child: Container(
width: 30,
height: 30,
color: AppColor.white,
child: const Icon(
Icons.add_circle,
color: AppColor.primary,
),
),
),
],
),
const SpaceWidth(8),
SizedBox(
width: 80.0,
child: Text(
((widget.data.product.price +
(widget.data.variant?.priceModifier ?? 0)) *
widget.data.quantity)
.currencyFormatRp,
textAlign: TextAlign.right,
style: const TextStyle(
color: AppColor.primary,
fontWeight: FontWeight.bold,
),
),
),
],
),
],
);
}
}

View File

@ -1,5 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../application/checkout/checkout_form/checkout_form_bloc.dart';
import '../../../common/extension/extension.dart';
import '../../../common/theme/theme.dart';
import '../../../domain/product/product.dart';
@ -17,9 +19,9 @@ class ProductCard extends StatelessWidget {
onTap: () {
if (product.isActive == true) {
if (product.variants.isEmpty) {
// context.read<CheckoutBloc>().add(
// CheckoutEvent.addItem(data, null),
// );
context.read<CheckoutFormBloc>().add(
CheckoutFormEvent.addItem(product, null),
);
} else {
showDialog(
context: context,
@ -85,28 +87,40 @@ class ProductCard extends StatelessWidget {
),
),
Positioned(
top: 4,
right: 4,
child: Container(
width: 40,
height: 40,
padding: const EdgeInsets.all(6),
decoration: const BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(9.0)),
color: AppColor.primary,
),
child: Center(
child: Text(
0.toString(),
style: const TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.bold,
BlocBuilder<CheckoutFormBloc, CheckoutFormState>(
builder: (context, state) {
final totalQuantity = state.items
.where((item) => item.product.id == product.id)
.map((item) => item.quantity)
.fold(0, (sum, qty) => sum + qty);
if (totalQuantity == 0) {
return const SizedBox.shrink();
}
return Positioned(
top: 4,
right: 4,
child: Container(
width: 40,
height: 40,
padding: const EdgeInsets.all(6),
decoration: const BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(9.0)),
color: AppColor.primary,
),
child: Center(
child: Text(
totalQuantity.toString(),
style: const TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
),
),
),
),
);
},
),
if (product.isActive == false)
Container(

View File

@ -2,6 +2,7 @@ import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../application/checkout/checkout_form/checkout_form_bloc.dart';
import '../../../application/outlet/outlet_loader/outlet_loader_bloc.dart';
import '../../../common/extension/extension.dart';
import '../../../common/theme/theme.dart';

View File

@ -21,10 +21,10 @@ class VariantDialog extends StatelessWidget {
return GestureDetector(
onTap: () {
// Aksi saat varian dipilih
// context.pop();
// context.read<CheckoutBloc>().add(
// CheckoutEvent.addItem(product, variant),
// );
context.maybePop();
context.read<CheckoutFormBloc>().add(
CheckoutFormEvent.addItem(product, variant),
);
},
child: Container(
width: (context.deviceWidth * 0.4 - 12 - 32) / 2 - 6, // 2 per row

View File

@ -1,6 +1,15 @@
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../../../application/checkout/checkout_form/checkout_form_bloc.dart';
import '../../../../../../common/extension/extension.dart';
import '../../../../../../common/theme/theme.dart';
import '../../../../../components/button/button.dart';
import '../../../../../components/card/order_menu.dart';
import '../../../../../components/spaces/space.dart';
import '../../../../../components/toast/flushbar.dart';
import 'home_right_title.dart';
class HomeRightPanel extends StatelessWidget {
@ -8,12 +17,168 @@ class HomeRightPanel extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Align(
alignment: Alignment.topCenter,
child: Material(
color: Colors.white,
child: Column(children: [HomeRightTitle()]),
),
return BlocBuilder<CheckoutFormBloc, CheckoutFormState>(
builder: (context, state) {
final price = state.items.isEmpty
? 0.0
: state.items
.map(
(e) =>
(e.product.price + (e.variant?.priceModifier ?? 0)) *
e.quantity,
)
.reduce((value, element) => value + element);
log('🛒 Total Price: $price');
return Align(
alignment: Alignment.topCenter,
child: Material(
color: Colors.white,
child: Column(
children: [
HomeRightTitle(),
Padding(
padding: const EdgeInsets.all(
16.0,
).copyWith(bottom: 0, top: 20),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Item',
style: AppStyle.lg.copyWith(
color: AppColor.primary,
fontWeight: FontWeight.w600,
),
),
SizedBox(width: 130),
SizedBox(
width: 50.0,
child: Text(
'Qty',
style: AppStyle.lg.copyWith(
color: AppColor.primary,
fontWeight: FontWeight.w600,
),
),
),
SizedBox(
child: Text(
'Price',
style: AppStyle.lg.copyWith(
color: AppColor.primary,
fontWeight: FontWeight.w600,
),
),
),
],
),
const SpaceHeight(8),
const Divider(),
],
),
),
Expanded(
child: state.items.isEmpty
? const Center(child: Text('No Items'))
: ListView.separated(
shrinkWrap: true,
padding: const EdgeInsets.symmetric(horizontal: 16),
itemBuilder: (context, index) =>
OrderMenu(data: state.items[index]),
separatorBuilder: (context, index) =>
const SpaceHeight(1.0),
itemCount: state.items.length,
),
),
Padding(
padding: const EdgeInsets.all(16.0).copyWith(top: 0),
child: Column(
children: [
const Divider(),
const SpaceHeight(16.0),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Pajak',
style: AppStyle.md.copyWith(
color: AppColor.black,
fontWeight: FontWeight.bold,
),
),
Text(
'${state.tax} %',
style: AppStyle.md.copyWith(
color: AppColor.primary,
fontWeight: FontWeight.w600,
),
),
],
),
const SpaceHeight(16.0),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'Sub total',
style: TextStyle(
color: AppColor.black,
fontWeight: FontWeight.bold,
),
),
Text(
price.currencyFormatRp,
style: const TextStyle(
color: AppColor.primary,
fontWeight: FontWeight.w900,
),
),
],
),
SpaceHeight(16.0),
Align(
alignment: Alignment.bottomCenter,
child: AppElevatedButton.filled(
borderRadius: 12,
elevation: 1,
disabled: state.items.isEmpty,
onPressed: () {
if (state.orderType.name == 'dineIn') {
AppFlushbar.showError(
context,
'Mohon pilih meja terlebih dahulu',
);
return;
}
if (state.orderType.name == 'delivery' &&
state.delivery == null) {
AppFlushbar.showError(
context,
'Mohon pilih pengiriman terlebih dahulu',
);
return;
}
// context.push(
// ConfirmPaymentPage(
// isTable: widget.table == null ? false : true,
// table: widget.table,
// ),
// );
},
label: 'Lanjutkan Pembayaran',
),
),
],
),
),
],
),
),
);
},
);
}
}