fix: cetak semua station tanpa saling membatalkan

printStruckOrder berhenti di kegagalan pertama, jadi satu station gagal membuat semua station sesudahnya tidak pernah dicoba. Karena checker (Bluetooth) berada sebelum kitchen dan bar (Network) dalam rantai, satu kegagalan koneksi Bluetooth menghilangkan tiga tiket sekaligus, dua di antaranya di jaringan yang sehat.

Jalur printer Network juga mengembalikan right(false) saat gagal, yang lolos sebagai sukses ke pemanggil, sehingga kasir melihat 'berhasil dicetak' padahal tidak ada kertas keluar.

Perubahan: repository kini mengembalikan Stream<List<PrintStationResult>> dan tetap mencoba semua station; kegagalan Network jadi left; station Network berjalan paralel dengan rantai Bluetooth; station Bluetooth dikelompokkan per alamat agar perpindahan koneksi seminimal mungkin; CapabilityProfile di-cache.

Bluetooth: retry connect dengan backoff dan disconnect di tiap percobaan ulang (socket RFCOMM setengah terbuka membuat connect berulang gagal identik), jeda settle dikembalikan setelah dipangkas ke 300ms, satu lock untuk seluruh radio (bukan per MAC), izin runtime dipastikan di jalur cetak, dan repository jadi LazySingleton agar state koneksi benar-benar dibagi.

UI: dialog progres per-station menggantikan toast tunggal, lengkap dengan durasi tiap station dan tombol cetak ulang khusus yang gagal. Enam listener duplikat di halaman diganti satu listener terpusat di AppWidget. Menu Print di detail transaksi dapat item Print All.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
efrilm
2026-09-02 20:44:14 +07:00
co-authored by Claude Opus 5
parent 038f4d65fe
commit 1b4caf45d5
21 changed files with 1963 additions and 1403 deletions
+1 -16
View File
@@ -3,10 +3,8 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../application/order/order_loader/order_loader_bloc.dart';
import '../../../application/printer/print_struck/print_struck_bloc.dart';
import '../../../common/theme/theme.dart';
import '../../../injection.dart';
import '../../components/toast/flushbar.dart';
import 'widgets/order_left_panel.dart';
import 'widgets/order_right_panel.dart';
@@ -37,20 +35,7 @@ class OrderPage extends StatelessWidget implements AutoRouteWrapper {
);
},
),
BlocListener<PrintStruckBloc, PrintStruckState>(
listenWhen: (previous, current) =>
previous.failureOrPrintStruck != current.failureOrPrintStruck,
listener: (context, state) {
state.failureOrPrintStruck.fold(() {}, (either) {
either.fold(
(f) => AppFlushbar.showPrinterFailureToast(context, f),
(success) {
AppFlushbar.showSuccess(context, "Struck berhasil dicetak");
},
);
});
},
),
// Hasil cetak dilaporkan lewat dialog progres global di `AppWidget`.
],
child: SafeArea(
child: Scaffold(
@@ -3,10 +3,8 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../../application/checkout/checkout_form/checkout_form_bloc.dart';
import '../../../../../application/printer/print_struck/print_struck_bloc.dart';
import '../../../../../common/theme/theme.dart';
import '../../../../components/spaces/space.dart';
import '../../../../components/toast/flushbar.dart';
import 'widgets/success_add_item_order_left_panel.dart';
import 'widgets/success_add_item_order_right_panel.dart';
@@ -16,46 +14,33 @@ class SuccessAddItemOrderPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocListener<PrintStruckBloc, PrintStruckState>(
listenWhen: (p, c) => p.failureOrPrintStruck != c.failureOrPrintStruck,
listener: (context, state) {
state.failureOrPrintStruck.fold(
() {},
(either) => either.fold(
(f) => AppFlushbar.showPrinterFailureToast(context, f),
(success) {
AppFlushbar.showSuccess(context, "Struck berhasil dicetak");
},
),
);
},
child: Scaffold(
backgroundColor: AppColor.background,
body: SafeArea(
child: BlocBuilder<CheckoutFormBloc, CheckoutFormState>(
builder: (context, checkoutState) {
return Padding(
padding: const EdgeInsets.all(24.0),
child: Row(
children: [
Expanded(
flex: 35,
child: SuccessAddItemOrderLeftPanel(
checkoutState: checkoutState,
),
// Hasil cetak dilaporkan lewat dialog progres global di `AppWidget`.
return Scaffold(
backgroundColor: AppColor.background,
body: SafeArea(
child: BlocBuilder<CheckoutFormBloc, CheckoutFormState>(
builder: (context, checkoutState) {
return Padding(
padding: const EdgeInsets.all(24.0),
child: Row(
children: [
Expanded(
flex: 35,
child: SuccessAddItemOrderLeftPanel(
checkoutState: checkoutState,
),
const SpaceWidth(16),
Expanded(
flex: 65,
child: SuccessAddItemOrderRightPanel(
checkoutState: checkoutState,
),
),
const SpaceWidth(16),
Expanded(
flex: 65,
child: SuccessAddItemOrderRightPanel(
checkoutState: checkoutState,
),
],
),
);
},
),
),
],
),
);
},
),
),
);
@@ -3,13 +3,11 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../../application/order/order_loader/order_loader_bloc.dart';
import '../../../../../application/printer/print_struck/print_struck_bloc.dart';
import '../../../../../common/theme/theme.dart';
import '../../../../../domain/order/order.dart';
import '../../../../../injection.dart';
import '../../../../components/loader/loader_with_text.dart';
import '../../../../components/spaces/space.dart';
import '../../../../components/toast/flushbar.dart';
import 'widgets/success_order_left_panel.dart';
import 'widgets/success_order_right_panel.dart';
@@ -20,47 +18,33 @@ class SuccessOrderPage extends StatelessWidget implements AutoRouteWrapper {
@override
Widget build(BuildContext context) {
return BlocListener<PrintStruckBloc, PrintStruckState>(
listenWhen: (previous, current) =>
previous.failureOrPrintStruck != current.failureOrPrintStruck,
listener: (context, state) {
state.failureOrPrintStruck.fold(
() {},
(either) => either.fold(
(f) => AppFlushbar.showPrinterFailureToast(context, f),
(success) {
AppFlushbar.showSuccess(context, "Struck berhasil dicetak");
},
),
);
},
child: Scaffold(
backgroundColor: AppColor.background,
body: SafeArea(
child: BlocBuilder<OrderLoaderBloc, OrderLoaderState>(
builder: (context, state) {
if (state.isFetchingById) {
return const Center(child: LoaderWithText());
}
// Hasil cetak dilaporkan lewat dialog progres global di `AppWidget`.
return Scaffold(
backgroundColor: AppColor.background,
body: SafeArea(
child: BlocBuilder<OrderLoaderBloc, OrderLoaderState>(
builder: (context, state) {
if (state.isFetchingById) {
return const Center(child: LoaderWithText());
}
return Padding(
padding: const EdgeInsets.all(24.0),
child: Row(
children: [
Expanded(
flex: 35,
child: SuccessOrderLeftPanel(order: state.order),
),
const SpaceWidth(16),
Expanded(
flex: 65,
child: SuccessOrderRightPanel(order: state.order),
),
],
),
);
},
),
return Padding(
padding: const EdgeInsets.all(24.0),
child: Row(
children: [
Expanded(
flex: 35,
child: SuccessOrderLeftPanel(order: state.order),
),
const SpaceWidth(16),
Expanded(
flex: 65,
child: SuccessOrderRightPanel(order: state.order),
),
],
),
);
},
),
),
);
@@ -62,6 +62,11 @@ class OrderRightPanel extends StatelessWidget {
color: AppColor.white,
onSelected: (value) {
switch (value) {
case 'all':
context.read<PrintStruckBloc>().add(
PrintStruckEvent.order(state.selectedOrder!),
);
break;
case 'checker':
context.read<PrintStruckBloc>().add(
PrintStruckEvent.checker(state.selectedOrder!),
@@ -79,7 +84,23 @@ class OrderRightPanel extends StatelessWidget {
break;
}
},
itemBuilder: (context) => [
itemBuilder: (context) => <PopupMenuEntry<String>>[
// Cetak semua station sekaligus — dipisah dari cetak per
// station karena sifatnya berbeda.
const PopupMenuItem(
value: 'all',
child: Row(
children: [
Icon(Icons.done_all_rounded, color: AppColor.primary),
SizedBox(width: 8),
Text(
'Print All',
style: TextStyle(fontWeight: FontWeight.w600),
),
],
),
),
const PopupMenuDivider(),
const PopupMenuItem(
value: 'checker',
child: Row(
@@ -3,12 +3,10 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../../application/order/order_loader/order_loader_bloc.dart';
import '../../../../../application/printer/print_struck/print_struck_bloc.dart';
import '../../../../../common/theme/theme.dart';
import '../../../../../injection.dart';
import '../../../../components/error/order_loader_error_state_widget.dart';
import '../../../../components/loader/loader_with_text.dart';
import '../../../../components/toast/flushbar.dart';
import 'widgets/payment_success_left_panel.dart';
import 'widgets/payment_success_right_panel.dart';
@@ -19,58 +17,46 @@ class PaymentSuccessPage extends StatelessWidget implements AutoRouteWrapper {
@override
Widget build(BuildContext context) {
return BlocListener<PrintStruckBloc, PrintStruckState>(
listenWhen: (previous, current) =>
previous.failureOrPrintStruck != current.failureOrPrintStruck,
listener: (context, state) {
state.failureOrPrintStruck.fold(() {}, (either) {
either.fold((f) => AppFlushbar.showPrinterFailureToast(context, f), (
success,
) {
AppFlushbar.showSuccess(context, "Struck berhasil dicetak");
});
});
},
child: Scaffold(
backgroundColor: AppColor.background,
body: SafeArea(
child: BlocBuilder<OrderLoaderBloc, OrderLoaderState>(
builder: (context, state) {
if (state.isFetchingById) {
return const Center(child: LoaderWithText());
}
return state.failureOptionGetById.fold(
() => Container(
padding: const EdgeInsets.all(24.0),
child: Row(
children: [
// Left Panel - Success Message & Order Info
Expanded(
flex: 35,
child: PaymentSuccessLeftPanel(order: state.order),
),
// Hasil cetak dilaporkan lewat dialog progres global di `AppWidget`.
return Scaffold(
backgroundColor: AppColor.background,
body: SafeArea(
child: BlocBuilder<OrderLoaderBloc, OrderLoaderState>(
builder: (context, state) {
if (state.isFetchingById) {
return const Center(child: LoaderWithText());
}
return state.failureOptionGetById.fold(
() => Container(
padding: const EdgeInsets.all(24.0),
child: Row(
children: [
// Left Panel - Success Message & Order Info
Expanded(
flex: 35,
child: PaymentSuccessLeftPanel(order: state.order),
),
const SizedBox(width: 16),
const SizedBox(width: 16),
// Right Panel - Order Details
Expanded(
flex: 65,
child: PaymentSuccessRightPanel(order: state.order),
),
],
),
// Right Panel - Order Details
Expanded(
flex: 65,
child: PaymentSuccessRightPanel(order: state.order),
),
],
),
(f) => OrderLoaderErrorStateWidget(
failure: f,
onRefresh: () {
context.read<OrderLoaderBloc>().add(
OrderLoaderEvent.getById(orderId),
);
},
),
);
},
),
),
(f) => OrderLoaderErrorStateWidget(
failure: f,
onRefresh: () {
context.read<OrderLoaderBloc>().add(
OrderLoaderEvent.getById(orderId),
);
},
),
);
},
),
),
);
@@ -3,13 +3,11 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../../application/order/order_loader/order_loader_bloc.dart';
import '../../../../../application/printer/print_struck/print_struck_bloc.dart';
import '../../../../../common/theme/theme.dart';
import '../../../../../domain/order/order.dart';
import '../../../../../injection.dart';
import '../../../../components/error/order_loader_error_state_widget.dart';
import '../../../../components/loader/loader_with_text.dart';
import '../../../../components/toast/flushbar.dart';
import 'widgets/payment_success_split_bill_left_panel.dart';
import 'widgets/payment_success_split_bill_right_panel.dart';
@@ -26,64 +24,50 @@ class PaymentSuccessSplitBillPage extends StatelessWidget
@override
Widget build(BuildContext context) {
return BlocListener<PrintStruckBloc, PrintStruckState>(
listenWhen: (previous, current) =>
previous.failureOrPrintStruck != current.failureOrPrintStruck,
listener: (context, state) {
state.failureOrPrintStruck.fold(
() {},
(either) => either.fold(
(f) => AppFlushbar.showPrinterFailureToast(context, f),
(success) {
AppFlushbar.showSuccess(context, "Struck berhasil dicetak");
},
),
);
},
child: Scaffold(
backgroundColor: AppColor.background,
body: SafeArea(
child: BlocBuilder<OrderLoaderBloc, OrderLoaderState>(
builder: (context, state) {
if (state.isFetchingById) {
return const Center(child: LoaderWithText());
}
return state.failureOptionGetById.fold(
() => Container(
padding: const EdgeInsets.all(24.0),
child: Row(
children: [
// Left Panel - Success Message & Order Info
Expanded(
flex: 35,
child: PaymentSuccessSplitBillLeftPanel(
order: state.order.copyWith(orderItems: orderItems),
),
// Hasil cetak dilaporkan lewat dialog progres global di `AppWidget`.
return Scaffold(
backgroundColor: AppColor.background,
body: SafeArea(
child: BlocBuilder<OrderLoaderBloc, OrderLoaderState>(
builder: (context, state) {
if (state.isFetchingById) {
return const Center(child: LoaderWithText());
}
return state.failureOptionGetById.fold(
() => Container(
padding: const EdgeInsets.all(24.0),
child: Row(
children: [
// Left Panel - Success Message & Order Info
Expanded(
flex: 35,
child: PaymentSuccessSplitBillLeftPanel(
order: state.order.copyWith(orderItems: orderItems),
),
),
const SizedBox(width: 16),
const SizedBox(width: 16),
// Right Panel - Order Details
Expanded(
flex: 65,
child: PaymentSuccessSplitBillRightPanel(
order: state.order.copyWith(orderItems: orderItems),
),
// Right Panel - Order Details
Expanded(
flex: 65,
child: PaymentSuccessSplitBillRightPanel(
order: state.order.copyWith(orderItems: orderItems),
),
],
),
),
],
),
(f) => OrderLoaderErrorStateWidget(
failure: f,
onRefresh: () {
context.read<OrderLoaderBloc>().add(
OrderLoaderEvent.getById(orderId),
);
},
),
);
},
),
),
(f) => OrderLoaderErrorStateWidget(
failure: f,
onRefresh: () {
context.read<OrderLoaderBloc>().add(
OrderLoaderEvent.getById(orderId),
);
},
),
);
},
),
),
);
@@ -2,11 +2,9 @@ import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../../application/printer/print_struck/print_struck_bloc.dart';
import '../../../../../application/void/void_form/void_form_bloc.dart';
import '../../../../../common/theme/theme.dart';
import '../../../../components/spaces/space.dart';
import '../../../../components/toast/flushbar.dart';
import 'widgets/void_success_left_panel.dart';
import 'widgets/void_success_right_panel.dart';
@@ -16,44 +14,30 @@ class VoidSuccessPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocListener<PrintStruckBloc, PrintStruckState>(
listenWhen: (previous, current) =>
previous.failureOrPrintStruck != current.failureOrPrintStruck,
listener: (context, state) {
state.failureOrPrintStruck.fold(
() {},
(either) => either.fold(
(f) => AppFlushbar.showPrinterFailureToast(context, f),
(success) {
AppFlushbar.showSuccess(context, "Struck berhasil dicetak");
},
),
);
},
child: Scaffold(
backgroundColor: AppColor.background,
body: BlocBuilder<VoidFormBloc, VoidFormState>(
builder: (context, state) {
return SafeArea(
child: Padding(
padding: const EdgeInsets.all(24.0),
child: Row(
children: [
Expanded(
flex: 35,
child: VoidSuccessLeftPanel(state: state),
),
const SpaceWidth(16),
Expanded(
flex: 65,
child: VoidSuccessRightPanel(state: state),
),
],
),
// Hasil cetak dilaporkan lewat dialog progres global di `AppWidget`.
return Scaffold(
backgroundColor: AppColor.background,
body: BlocBuilder<VoidFormBloc, VoidFormState>(
builder: (context, state) {
return SafeArea(
child: Padding(
padding: const EdgeInsets.all(24.0),
child: Row(
children: [
Expanded(
flex: 35,
child: VoidSuccessLeftPanel(state: state),
),
const SpaceWidth(16),
Expanded(
flex: 65,
child: VoidSuccessRightPanel(state: state),
),
],
),
);
},
),
),
);
},
),
);
}