Files
apskel-pos-flutter-v2/lib/presentation/pages/checkout/checkout_page.dart
T
efrilmandClaude Opus 5 1505a83dd3 feat: dukung penjualan produk per timbangan
Produk kini punya cara jual `unit` (cacah) atau `weight` (timbangan),
mengikuti kontrak backend di docs/integration-weight-based-products.md.

- tipe SellBy, domain/infrastructure unit baru, dan endpoint /api/v1/units
- dialog input berat saat produk timbangan ditambahkan ke keranjang
- tiap penimbangan jadi baris keranjang sendiri; total baris dihitung
  berat x harga satuan, sedangkan produk cacah tetap digabung
- request ke backend mengirim quantity 1 dan membawa berat
- migrasi DB v3: kolom sell_by, unit_id, unit_name, unit_abbreviation
  di tabel products supaya satuan tetap tampil saat offline
- tampilan keranjang, struk, split bill, void, dan refund memakai berat
- test/weight_based_products_test.dart menutup perilaku di atas (9 test)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-06 23:03:31 +07:00

142 lines
4.8 KiB
Dart

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/order/order_form/order_form_bloc.dart';
import '../../../application/payment_method/payment_method_loader/payment_method_loader_bloc.dart';
import '../../../application/table/table_loader/table_loader_bloc.dart';
import '../../../common/theme/theme.dart';
import '../../../domain/product/product.dart';
import '../../../injection.dart';
import '../../components/spaces/space.dart';
import '../../components/toast/flushbar.dart';
import '../../router/app_router.gr.dart';
import 'widgets/checkout_left_panel.dart';
import 'widgets/checkout_right_panel.dart';
@RoutePage()
class CheckoutPage extends StatelessWidget implements AutoRouteWrapper {
const CheckoutPage({super.key});
@override
Widget build(BuildContext context) {
return MultiBlocListener(
listeners: [
BlocListener<OrderFormBloc, OrderFormState>(
listenWhen: (previous, current) =>
previous.failureOrCreateOrderWithPayment !=
current.failureOrCreateOrderWithPayment,
listener: (context, state) {
state.failureOrCreateOrderWithPayment.fold(() {}, (either) {
either.fold(
(f) => AppFlushbar.showOrderFailureToast(context, f),
(order) {
if (context.mounted) {
context.read<CheckoutFormBloc>().add(
const CheckoutFormEvent.started([]),
);
context.router.replace(SuccessOrderRoute(order: order));
}
},
);
});
},
),
BlocListener<OrderFormBloc, OrderFormState>(
listenWhen: (previous, current) =>
previous.failureOrCreateOrder != current.failureOrCreateOrder,
listener: (context, state) {
state.failureOrCreateOrder.fold(() {}, (either) {
either.fold(
(f) => AppFlushbar.showOrderFailureToast(context, f),
(order) {
if (context.mounted) {
context.read<CheckoutFormBloc>().add(
const CheckoutFormEvent.started([]),
);
context.router.replace(SuccessOrderRoute(order: order));
}
},
);
});
},
),
BlocListener<OrderFormBloc, OrderFormState>(
listenWhen: (previous, current) =>
previous.failureOrAddItemOrder != current.failureOrAddItemOrder,
listener: (context, state) {
state.failureOrAddItemOrder.fold(() {}, (either) {
either.fold(
(f) => AppFlushbar.showOrderFailureToast(context, f),
(order) {
if (context.mounted) {
context.read<CheckoutFormBloc>().add(
CheckoutFormEvent.orderAddedItems(order),
);
context.router.replace(const SuccessAddItemOrderRoute());
}
},
);
});
},
),
],
child: SafeArea(
child: Hero(
tag: 'checkout_screen',
child: BlocBuilder<CheckoutFormBloc, CheckoutFormState>(
builder: (context, state) {
final price = state.items.cartTotal.toInt();
return Scaffold(
backgroundColor: AppColor.white,
body: Row(
children: [
Expanded(
flex: 3,
child: CheckoutLeftPanel(
checkoutState: state,
price: price,
),
),
const SpaceWidth(2),
Expanded(
flex: 3,
child: CheckoutRightPanel(
checkoutState: state,
price: price,
),
),
],
),
);
},
),
),
),
);
}
@override
Widget wrappedRoute(BuildContext context) => MultiBlocProvider(
providers: [
BlocProvider(
create: (context) =>
getIt<PaymentMethodLoaderBloc>()
..add(const PaymentMethodLoaderEvent.fetched(isRefresh: true)),
),
BlocProvider(
create: (context) => getIt<TableLoaderBloc>()
..add(
const TableLoaderEvent.fetched(
isRefresh: true,
status: 'available',
),
),
),
],
child: this,
);
}