From 1b4caf45d5fd31d754c9362690320e47a6cba9de Mon Sep 17 00:00:00 2001 From: efrilm Date: Wed, 2 Sep 2026 20:44:14 +0700 Subject: [PATCH] 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> 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) --- .../print_struck/print_struck_bloc.dart | 178 +- .../print_struck_bloc.freezed.dart | 532 ++++-- .../print_struck/print_struck_event.dart | 6 + .../print_struck/print_struck_state.dart | 30 +- .../entities/print_station_result.dart | 83 + lib/domain/printer/printer.dart | 1 + lib/domain/printer/printer.freezed.dart | 210 +++ .../repositories/i_printer_repository.dart | 30 +- .../repositories/printer_repository.dart | 1547 +++++++---------- lib/injection.config.dart | 2 +- lib/presentation/app_widget.dart | 15 + .../components/dialog/dialog.dart | 5 + .../dialog/print_progress_dialog.dart | 273 +++ .../components/print/print_ui.dart | 37 +- lib/presentation/pages/order/order_page.dart | 17 +- .../success_add_item_order_page.dart | 65 +- .../success_order/success_order_page.dart | 68 +- .../order/widgets/order_right_panel.dart | 23 +- .../payment_success/payment_success_page.dart | 88 +- .../payment_success_split_bill_page.dart | 94 +- .../pages/void_success/void_success_page.dart | 62 +- 21 files changed, 1963 insertions(+), 1403 deletions(-) create mode 100644 lib/domain/printer/entities/print_station_result.dart create mode 100644 lib/presentation/components/dialog/print_progress_dialog.dart diff --git a/lib/application/printer/print_struck/print_struck_bloc.dart b/lib/application/printer/print_struck/print_struck_bloc.dart index 7a52377..096a316 100644 --- a/lib/application/printer/print_struck/print_struck_bloc.dart +++ b/lib/application/printer/print_struck/print_struck_bloc.dart @@ -1,5 +1,4 @@ import 'package:bloc/bloc.dart'; -import 'package:dartz/dartz.dart' hide Order; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:injectable/injectable.dart' hide Order; @@ -21,136 +20,63 @@ class PrintStruckBloc extends Bloc { Future _onPrintStruckEvent( PrintStruckEvent event, Emitter emit, - ) { - return event.map( - order: (e) async { - Either failureOrSuccess; + ) => event.map( + order: (e) => _print(emit, e.order, _stationsForOrder(e.order)), + payment: (e) => _print(emit, e.order, const [PrintStation.receipt]), + cashier: (e) => _print(emit, e.order, const [PrintStation.cashier]), + checker: (e) => _print(emit, e.order, const [PrintStation.checker]), + kitchen: (e) => _print(emit, e.order, const [PrintStation.kitchen]), + bar: (e) => _print(emit, e.order, const [PrintStation.bar]), + voided: (e) => _print(emit, e.order, const [PrintStation.voided]), + splitBill: (e) => _print(emit, e.order, const [PrintStation.splitBill]), + retryFailed: (_) { + final order = state.order; + final failed = [ + for (final station in state.failedStations) station.station, + ]; - emit(state.copyWith(isPrinting: true, failureOrPrintStruck: none())); + if (order == null || failed.isEmpty) return Future.value(); - failureOrSuccess = await _printerRepository.printStruckOrder( - order: e.order, - ); + return _print(emit, order, failed); + }, + ); - emit( - state.copyWith( - isPrinting: false, - failureOrPrintStruck: optionOf(failureOrSuccess), - ), - ); - }, - cashier: (e) async { - Either failureOrSuccess; + /// Struk pelanggan hanya ikut dicetak kalau ordernya sudah dibayar. + List _stationsForOrder(Order order) => [ + if (order.totalPaid > 0) PrintStation.receipt, + PrintStation.cashier, + PrintStation.checker, + PrintStation.kitchen, + PrintStation.bar, + ]; - emit(state.copyWith(isPrinting: true, failureOrPrintStruck: none())); + Future _print( + Emitter emit, + Order order, + List stations, + ) async { + // Tombol yang tertekan dua kali tidak boleh memulai job kedua — repository + // akan mengantrikannya dan progres yang tampil jadi membingungkan. + if (state.isPrinting) return; - failureOrSuccess = await _printerRepository.printStruckCashier( - order: e.order, - ); - - emit( - state.copyWith( - isPrinting: false, - failureOrPrintStruck: optionOf(failureOrSuccess), - ), - ); - }, - payment: (e) async { - Either failureOrSuccess; - - emit(state.copyWith(isPrinting: true, failureOrPrintStruck: none())); - - failureOrSuccess = await _printerRepository.printStruckPayment( - order: e.order, - ); - - emit( - state.copyWith( - isPrinting: false, - failureOrPrintStruck: optionOf(failureOrSuccess), - ), - ); - }, - voided: (e) async { - Either failureOrSuccess; - - emit(state.copyWith(isPrinting: true, failureOrPrintStruck: none())); - - failureOrSuccess = await _printerRepository.printStruckVoid( - order: e.order, - ); - - emit( - state.copyWith( - isPrinting: false, - failureOrPrintStruck: optionOf(failureOrSuccess), - ), - ); - }, - splitBill: (e) async { - Either failureOrSuccess; - - emit(state.copyWith(isPrinting: true, failureOrPrintStruck: none())); - - failureOrSuccess = await _printerRepository.printStruckSplitBill( - order: e.order, - ); - - emit( - state.copyWith( - isPrinting: false, - failureOrPrintStruck: optionOf(failureOrSuccess), - ), - ); - }, - kitchen: (e) async { - Either failureOrSuccess; - - emit(state.copyWith(isPrinting: true, failureOrPrintStruck: none())); - - failureOrSuccess = await _printerRepository.printStruckKitchen( - order: e.order, - ); - - emit( - state.copyWith( - isPrinting: false, - failureOrPrintStruck: optionOf(failureOrSuccess), - ), - ); - }, - bar: (e) async { - Either failureOrSuccess; - - emit(state.copyWith(isPrinting: true, failureOrPrintStruck: none())); - - failureOrSuccess = await _printerRepository.printStruckBar( - order: e.order, - ); - - emit( - state.copyWith( - isPrinting: false, - failureOrPrintStruck: optionOf(failureOrSuccess), - ), - ); - }, - checker: (e) async { - Either failureOrSuccess; - - emit(state.copyWith(isPrinting: true, failureOrPrintStruck: none())); - - failureOrSuccess = await _printerRepository.printStruckChecker( - order: e.order, - ); - - emit( - state.copyWith( - isPrinting: false, - failureOrPrintStruck: optionOf(failureOrSuccess), - ), - ); - }, + emit( + state.copyWith( + isPrinting: true, + order: order, + jobId: state.jobId + 1, + stations: [ + for (final station in stations) PrintStationResult.pending(station), + ], + ), ); + + await emit.forEach>( + _printerRepository.printStations(order: order, stations: stations), + onData: (results) => state.copyWith(stations: results), + ); + + // Cetak bisa selesai setelah bloc ditutup (kasir keluar dari aplikasi). + if (isClosed) return; + emit(state.copyWith(isPrinting: false)); } } diff --git a/lib/application/printer/print_struck/print_struck_bloc.freezed.dart b/lib/application/printer/print_struck/print_struck_bloc.freezed.dart index 9b700fd..5e35e22 100644 --- a/lib/application/printer/print_struck/print_struck_bloc.freezed.dart +++ b/lib/application/printer/print_struck/print_struck_bloc.freezed.dart @@ -17,7 +17,6 @@ final _privateConstructorUsedError = UnsupportedError( /// @nodoc mixin _$PrintStruckEvent { - Order get order => throw _privateConstructorUsedError; @optionalTypeArgs TResult when({ required TResult Function(Order order) order, @@ -28,6 +27,7 @@ mixin _$PrintStruckEvent { required TResult Function(Order order) kitchen, required TResult Function(Order order) bar, required TResult Function(Order order) splitBill, + required TResult Function() retryFailed, }) => throw _privateConstructorUsedError; @optionalTypeArgs TResult? whenOrNull({ @@ -39,6 +39,7 @@ mixin _$PrintStruckEvent { TResult? Function(Order order)? kitchen, TResult? Function(Order order)? bar, TResult? Function(Order order)? splitBill, + TResult? Function()? retryFailed, }) => throw _privateConstructorUsedError; @optionalTypeArgs TResult maybeWhen({ @@ -50,6 +51,7 @@ mixin _$PrintStruckEvent { TResult Function(Order order)? kitchen, TResult Function(Order order)? bar, TResult Function(Order order)? splitBill, + TResult Function()? retryFailed, required TResult orElse(), }) => throw _privateConstructorUsedError; @optionalTypeArgs @@ -62,6 +64,7 @@ mixin _$PrintStruckEvent { required TResult Function(_Kitchen value) kitchen, required TResult Function(_Bar value) bar, required TResult Function(_SplitBill value) splitBill, + required TResult Function(_RetryFailed value) retryFailed, }) => throw _privateConstructorUsedError; @optionalTypeArgs TResult? mapOrNull({ @@ -73,6 +76,7 @@ mixin _$PrintStruckEvent { TResult? Function(_Kitchen value)? kitchen, TResult? Function(_Bar value)? bar, TResult? Function(_SplitBill value)? splitBill, + TResult? Function(_RetryFailed value)? retryFailed, }) => throw _privateConstructorUsedError; @optionalTypeArgs TResult maybeMap({ @@ -84,14 +88,9 @@ mixin _$PrintStruckEvent { TResult Function(_Kitchen value)? kitchen, TResult Function(_Bar value)? bar, TResult Function(_SplitBill value)? splitBill, + TResult Function(_RetryFailed value)? retryFailed, required TResult orElse(), }) => throw _privateConstructorUsedError; - - /// Create a copy of PrintStruckEvent - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - $PrintStruckEventCopyWith get copyWith => - throw _privateConstructorUsedError; } /// @nodoc @@ -100,10 +99,6 @@ abstract class $PrintStruckEventCopyWith<$Res> { PrintStruckEvent value, $Res Function(PrintStruckEvent) then, ) = _$PrintStruckEventCopyWithImpl<$Res, PrintStruckEvent>; - @useResult - $Res call({Order order}); - - $OrderCopyWith<$Res> get order; } /// @nodoc @@ -118,43 +113,17 @@ class _$PrintStruckEventCopyWithImpl<$Res, $Val extends PrintStruckEvent> /// Create a copy of PrintStruckEvent /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({Object? order = null}) { - return _then( - _value.copyWith( - order: null == order - ? _value.order - : order // ignore: cast_nullable_to_non_nullable - as Order, - ) - as $Val, - ); - } - - /// Create a copy of PrintStruckEvent - /// with the given fields replaced by the non-null parameter values. - @override - @pragma('vm:prefer-inline') - $OrderCopyWith<$Res> get order { - return $OrderCopyWith<$Res>(_value.order, (value) { - return _then(_value.copyWith(order: value) as $Val); - }); - } } /// @nodoc -abstract class _$$OrderImplCopyWith<$Res> - implements $PrintStruckEventCopyWith<$Res> { +abstract class _$$OrderImplCopyWith<$Res> { factory _$$OrderImplCopyWith( _$OrderImpl value, $Res Function(_$OrderImpl) then, ) = __$$OrderImplCopyWithImpl<$Res>; - @override @useResult $Res call({Order order}); - @override $OrderCopyWith<$Res> get order; } @@ -181,6 +150,16 @@ class __$$OrderImplCopyWithImpl<$Res> ), ); } + + /// Create a copy of PrintStruckEvent + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $OrderCopyWith<$Res> get order { + return $OrderCopyWith<$Res>(_value.order, (value) { + return _then(_value.copyWith(order: value)); + }); + } } /// @nodoc @@ -226,6 +205,7 @@ class _$OrderImpl implements _Order { required TResult Function(Order order) kitchen, required TResult Function(Order order) bar, required TResult Function(Order order) splitBill, + required TResult Function() retryFailed, }) { return order(this.order); } @@ -241,6 +221,7 @@ class _$OrderImpl implements _Order { TResult? Function(Order order)? kitchen, TResult? Function(Order order)? bar, TResult? Function(Order order)? splitBill, + TResult? Function()? retryFailed, }) { return order?.call(this.order); } @@ -256,6 +237,7 @@ class _$OrderImpl implements _Order { TResult Function(Order order)? kitchen, TResult Function(Order order)? bar, TResult Function(Order order)? splitBill, + TResult Function()? retryFailed, required TResult orElse(), }) { if (order != null) { @@ -275,6 +257,7 @@ class _$OrderImpl implements _Order { required TResult Function(_Kitchen value) kitchen, required TResult Function(_Bar value) bar, required TResult Function(_SplitBill value) splitBill, + required TResult Function(_RetryFailed value) retryFailed, }) { return order(this); } @@ -290,6 +273,7 @@ class _$OrderImpl implements _Order { TResult? Function(_Kitchen value)? kitchen, TResult? Function(_Bar value)? bar, TResult? Function(_SplitBill value)? splitBill, + TResult? Function(_RetryFailed value)? retryFailed, }) { return order?.call(this); } @@ -305,6 +289,7 @@ class _$OrderImpl implements _Order { TResult Function(_Kitchen value)? kitchen, TResult Function(_Bar value)? bar, TResult Function(_SplitBill value)? splitBill, + TResult Function(_RetryFailed value)? retryFailed, required TResult orElse(), }) { if (order != null) { @@ -317,29 +302,24 @@ class _$OrderImpl implements _Order { abstract class _Order implements PrintStruckEvent { const factory _Order(final Order order) = _$OrderImpl; - @override Order get order; /// Create a copy of PrintStruckEvent /// with the given fields replaced by the non-null parameter values. - @override @JsonKey(includeFromJson: false, includeToJson: false) _$$OrderImplCopyWith<_$OrderImpl> get copyWith => throw _privateConstructorUsedError; } /// @nodoc -abstract class _$$CashierImplCopyWith<$Res> - implements $PrintStruckEventCopyWith<$Res> { +abstract class _$$CashierImplCopyWith<$Res> { factory _$$CashierImplCopyWith( _$CashierImpl value, $Res Function(_$CashierImpl) then, ) = __$$CashierImplCopyWithImpl<$Res>; - @override @useResult $Res call({Order order}); - @override $OrderCopyWith<$Res> get order; } @@ -366,6 +346,16 @@ class __$$CashierImplCopyWithImpl<$Res> ), ); } + + /// Create a copy of PrintStruckEvent + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $OrderCopyWith<$Res> get order { + return $OrderCopyWith<$Res>(_value.order, (value) { + return _then(_value.copyWith(order: value)); + }); + } } /// @nodoc @@ -411,6 +401,7 @@ class _$CashierImpl implements _Cashier { required TResult Function(Order order) kitchen, required TResult Function(Order order) bar, required TResult Function(Order order) splitBill, + required TResult Function() retryFailed, }) { return cashier(this.order); } @@ -426,6 +417,7 @@ class _$CashierImpl implements _Cashier { TResult? Function(Order order)? kitchen, TResult? Function(Order order)? bar, TResult? Function(Order order)? splitBill, + TResult? Function()? retryFailed, }) { return cashier?.call(this.order); } @@ -441,6 +433,7 @@ class _$CashierImpl implements _Cashier { TResult Function(Order order)? kitchen, TResult Function(Order order)? bar, TResult Function(Order order)? splitBill, + TResult Function()? retryFailed, required TResult orElse(), }) { if (cashier != null) { @@ -460,6 +453,7 @@ class _$CashierImpl implements _Cashier { required TResult Function(_Kitchen value) kitchen, required TResult Function(_Bar value) bar, required TResult Function(_SplitBill value) splitBill, + required TResult Function(_RetryFailed value) retryFailed, }) { return cashier(this); } @@ -475,6 +469,7 @@ class _$CashierImpl implements _Cashier { TResult? Function(_Kitchen value)? kitchen, TResult? Function(_Bar value)? bar, TResult? Function(_SplitBill value)? splitBill, + TResult? Function(_RetryFailed value)? retryFailed, }) { return cashier?.call(this); } @@ -490,6 +485,7 @@ class _$CashierImpl implements _Cashier { TResult Function(_Kitchen value)? kitchen, TResult Function(_Bar value)? bar, TResult Function(_SplitBill value)? splitBill, + TResult Function(_RetryFailed value)? retryFailed, required TResult orElse(), }) { if (cashier != null) { @@ -502,29 +498,24 @@ class _$CashierImpl implements _Cashier { abstract class _Cashier implements PrintStruckEvent { const factory _Cashier(final Order order) = _$CashierImpl; - @override Order get order; /// Create a copy of PrintStruckEvent /// with the given fields replaced by the non-null parameter values. - @override @JsonKey(includeFromJson: false, includeToJson: false) _$$CashierImplCopyWith<_$CashierImpl> get copyWith => throw _privateConstructorUsedError; } /// @nodoc -abstract class _$$PaymentImplCopyWith<$Res> - implements $PrintStruckEventCopyWith<$Res> { +abstract class _$$PaymentImplCopyWith<$Res> { factory _$$PaymentImplCopyWith( _$PaymentImpl value, $Res Function(_$PaymentImpl) then, ) = __$$PaymentImplCopyWithImpl<$Res>; - @override @useResult $Res call({Order order}); - @override $OrderCopyWith<$Res> get order; } @@ -551,6 +542,16 @@ class __$$PaymentImplCopyWithImpl<$Res> ), ); } + + /// Create a copy of PrintStruckEvent + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $OrderCopyWith<$Res> get order { + return $OrderCopyWith<$Res>(_value.order, (value) { + return _then(_value.copyWith(order: value)); + }); + } } /// @nodoc @@ -596,6 +597,7 @@ class _$PaymentImpl implements _Payment { required TResult Function(Order order) kitchen, required TResult Function(Order order) bar, required TResult Function(Order order) splitBill, + required TResult Function() retryFailed, }) { return payment(this.order); } @@ -611,6 +613,7 @@ class _$PaymentImpl implements _Payment { TResult? Function(Order order)? kitchen, TResult? Function(Order order)? bar, TResult? Function(Order order)? splitBill, + TResult? Function()? retryFailed, }) { return payment?.call(this.order); } @@ -626,6 +629,7 @@ class _$PaymentImpl implements _Payment { TResult Function(Order order)? kitchen, TResult Function(Order order)? bar, TResult Function(Order order)? splitBill, + TResult Function()? retryFailed, required TResult orElse(), }) { if (payment != null) { @@ -645,6 +649,7 @@ class _$PaymentImpl implements _Payment { required TResult Function(_Kitchen value) kitchen, required TResult Function(_Bar value) bar, required TResult Function(_SplitBill value) splitBill, + required TResult Function(_RetryFailed value) retryFailed, }) { return payment(this); } @@ -660,6 +665,7 @@ class _$PaymentImpl implements _Payment { TResult? Function(_Kitchen value)? kitchen, TResult? Function(_Bar value)? bar, TResult? Function(_SplitBill value)? splitBill, + TResult? Function(_RetryFailed value)? retryFailed, }) { return payment?.call(this); } @@ -675,6 +681,7 @@ class _$PaymentImpl implements _Payment { TResult Function(_Kitchen value)? kitchen, TResult Function(_Bar value)? bar, TResult Function(_SplitBill value)? splitBill, + TResult Function(_RetryFailed value)? retryFailed, required TResult orElse(), }) { if (payment != null) { @@ -687,29 +694,24 @@ class _$PaymentImpl implements _Payment { abstract class _Payment implements PrintStruckEvent { const factory _Payment(final Order order) = _$PaymentImpl; - @override Order get order; /// Create a copy of PrintStruckEvent /// with the given fields replaced by the non-null parameter values. - @override @JsonKey(includeFromJson: false, includeToJson: false) _$$PaymentImplCopyWith<_$PaymentImpl> get copyWith => throw _privateConstructorUsedError; } /// @nodoc -abstract class _$$VoidedImplCopyWith<$Res> - implements $PrintStruckEventCopyWith<$Res> { +abstract class _$$VoidedImplCopyWith<$Res> { factory _$$VoidedImplCopyWith( _$VoidedImpl value, $Res Function(_$VoidedImpl) then, ) = __$$VoidedImplCopyWithImpl<$Res>; - @override @useResult $Res call({Order order}); - @override $OrderCopyWith<$Res> get order; } @@ -736,6 +738,16 @@ class __$$VoidedImplCopyWithImpl<$Res> ), ); } + + /// Create a copy of PrintStruckEvent + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $OrderCopyWith<$Res> get order { + return $OrderCopyWith<$Res>(_value.order, (value) { + return _then(_value.copyWith(order: value)); + }); + } } /// @nodoc @@ -781,6 +793,7 @@ class _$VoidedImpl implements _Voided { required TResult Function(Order order) kitchen, required TResult Function(Order order) bar, required TResult Function(Order order) splitBill, + required TResult Function() retryFailed, }) { return voided(this.order); } @@ -796,6 +809,7 @@ class _$VoidedImpl implements _Voided { TResult? Function(Order order)? kitchen, TResult? Function(Order order)? bar, TResult? Function(Order order)? splitBill, + TResult? Function()? retryFailed, }) { return voided?.call(this.order); } @@ -811,6 +825,7 @@ class _$VoidedImpl implements _Voided { TResult Function(Order order)? kitchen, TResult Function(Order order)? bar, TResult Function(Order order)? splitBill, + TResult Function()? retryFailed, required TResult orElse(), }) { if (voided != null) { @@ -830,6 +845,7 @@ class _$VoidedImpl implements _Voided { required TResult Function(_Kitchen value) kitchen, required TResult Function(_Bar value) bar, required TResult Function(_SplitBill value) splitBill, + required TResult Function(_RetryFailed value) retryFailed, }) { return voided(this); } @@ -845,6 +861,7 @@ class _$VoidedImpl implements _Voided { TResult? Function(_Kitchen value)? kitchen, TResult? Function(_Bar value)? bar, TResult? Function(_SplitBill value)? splitBill, + TResult? Function(_RetryFailed value)? retryFailed, }) { return voided?.call(this); } @@ -860,6 +877,7 @@ class _$VoidedImpl implements _Voided { TResult Function(_Kitchen value)? kitchen, TResult Function(_Bar value)? bar, TResult Function(_SplitBill value)? splitBill, + TResult Function(_RetryFailed value)? retryFailed, required TResult orElse(), }) { if (voided != null) { @@ -872,29 +890,24 @@ class _$VoidedImpl implements _Voided { abstract class _Voided implements PrintStruckEvent { const factory _Voided(final Order order) = _$VoidedImpl; - @override Order get order; /// Create a copy of PrintStruckEvent /// with the given fields replaced by the non-null parameter values. - @override @JsonKey(includeFromJson: false, includeToJson: false) _$$VoidedImplCopyWith<_$VoidedImpl> get copyWith => throw _privateConstructorUsedError; } /// @nodoc -abstract class _$$CheckerImplCopyWith<$Res> - implements $PrintStruckEventCopyWith<$Res> { +abstract class _$$CheckerImplCopyWith<$Res> { factory _$$CheckerImplCopyWith( _$CheckerImpl value, $Res Function(_$CheckerImpl) then, ) = __$$CheckerImplCopyWithImpl<$Res>; - @override @useResult $Res call({Order order}); - @override $OrderCopyWith<$Res> get order; } @@ -921,6 +934,16 @@ class __$$CheckerImplCopyWithImpl<$Res> ), ); } + + /// Create a copy of PrintStruckEvent + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $OrderCopyWith<$Res> get order { + return $OrderCopyWith<$Res>(_value.order, (value) { + return _then(_value.copyWith(order: value)); + }); + } } /// @nodoc @@ -966,6 +989,7 @@ class _$CheckerImpl implements _Checker { required TResult Function(Order order) kitchen, required TResult Function(Order order) bar, required TResult Function(Order order) splitBill, + required TResult Function() retryFailed, }) { return checker(this.order); } @@ -981,6 +1005,7 @@ class _$CheckerImpl implements _Checker { TResult? Function(Order order)? kitchen, TResult? Function(Order order)? bar, TResult? Function(Order order)? splitBill, + TResult? Function()? retryFailed, }) { return checker?.call(this.order); } @@ -996,6 +1021,7 @@ class _$CheckerImpl implements _Checker { TResult Function(Order order)? kitchen, TResult Function(Order order)? bar, TResult Function(Order order)? splitBill, + TResult Function()? retryFailed, required TResult orElse(), }) { if (checker != null) { @@ -1015,6 +1041,7 @@ class _$CheckerImpl implements _Checker { required TResult Function(_Kitchen value) kitchen, required TResult Function(_Bar value) bar, required TResult Function(_SplitBill value) splitBill, + required TResult Function(_RetryFailed value) retryFailed, }) { return checker(this); } @@ -1030,6 +1057,7 @@ class _$CheckerImpl implements _Checker { TResult? Function(_Kitchen value)? kitchen, TResult? Function(_Bar value)? bar, TResult? Function(_SplitBill value)? splitBill, + TResult? Function(_RetryFailed value)? retryFailed, }) { return checker?.call(this); } @@ -1045,6 +1073,7 @@ class _$CheckerImpl implements _Checker { TResult Function(_Kitchen value)? kitchen, TResult Function(_Bar value)? bar, TResult Function(_SplitBill value)? splitBill, + TResult Function(_RetryFailed value)? retryFailed, required TResult orElse(), }) { if (checker != null) { @@ -1057,29 +1086,24 @@ class _$CheckerImpl implements _Checker { abstract class _Checker implements PrintStruckEvent { const factory _Checker(final Order order) = _$CheckerImpl; - @override Order get order; /// Create a copy of PrintStruckEvent /// with the given fields replaced by the non-null parameter values. - @override @JsonKey(includeFromJson: false, includeToJson: false) _$$CheckerImplCopyWith<_$CheckerImpl> get copyWith => throw _privateConstructorUsedError; } /// @nodoc -abstract class _$$KitchenImplCopyWith<$Res> - implements $PrintStruckEventCopyWith<$Res> { +abstract class _$$KitchenImplCopyWith<$Res> { factory _$$KitchenImplCopyWith( _$KitchenImpl value, $Res Function(_$KitchenImpl) then, ) = __$$KitchenImplCopyWithImpl<$Res>; - @override @useResult $Res call({Order order}); - @override $OrderCopyWith<$Res> get order; } @@ -1106,6 +1130,16 @@ class __$$KitchenImplCopyWithImpl<$Res> ), ); } + + /// Create a copy of PrintStruckEvent + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $OrderCopyWith<$Res> get order { + return $OrderCopyWith<$Res>(_value.order, (value) { + return _then(_value.copyWith(order: value)); + }); + } } /// @nodoc @@ -1151,6 +1185,7 @@ class _$KitchenImpl implements _Kitchen { required TResult Function(Order order) kitchen, required TResult Function(Order order) bar, required TResult Function(Order order) splitBill, + required TResult Function() retryFailed, }) { return kitchen(this.order); } @@ -1166,6 +1201,7 @@ class _$KitchenImpl implements _Kitchen { TResult? Function(Order order)? kitchen, TResult? Function(Order order)? bar, TResult? Function(Order order)? splitBill, + TResult? Function()? retryFailed, }) { return kitchen?.call(this.order); } @@ -1181,6 +1217,7 @@ class _$KitchenImpl implements _Kitchen { TResult Function(Order order)? kitchen, TResult Function(Order order)? bar, TResult Function(Order order)? splitBill, + TResult Function()? retryFailed, required TResult orElse(), }) { if (kitchen != null) { @@ -1200,6 +1237,7 @@ class _$KitchenImpl implements _Kitchen { required TResult Function(_Kitchen value) kitchen, required TResult Function(_Bar value) bar, required TResult Function(_SplitBill value) splitBill, + required TResult Function(_RetryFailed value) retryFailed, }) { return kitchen(this); } @@ -1215,6 +1253,7 @@ class _$KitchenImpl implements _Kitchen { TResult? Function(_Kitchen value)? kitchen, TResult? Function(_Bar value)? bar, TResult? Function(_SplitBill value)? splitBill, + TResult? Function(_RetryFailed value)? retryFailed, }) { return kitchen?.call(this); } @@ -1230,6 +1269,7 @@ class _$KitchenImpl implements _Kitchen { TResult Function(_Kitchen value)? kitchen, TResult Function(_Bar value)? bar, TResult Function(_SplitBill value)? splitBill, + TResult Function(_RetryFailed value)? retryFailed, required TResult orElse(), }) { if (kitchen != null) { @@ -1242,27 +1282,22 @@ class _$KitchenImpl implements _Kitchen { abstract class _Kitchen implements PrintStruckEvent { const factory _Kitchen(final Order order) = _$KitchenImpl; - @override Order get order; /// Create a copy of PrintStruckEvent /// with the given fields replaced by the non-null parameter values. - @override @JsonKey(includeFromJson: false, includeToJson: false) _$$KitchenImplCopyWith<_$KitchenImpl> get copyWith => throw _privateConstructorUsedError; } /// @nodoc -abstract class _$$BarImplCopyWith<$Res> - implements $PrintStruckEventCopyWith<$Res> { +abstract class _$$BarImplCopyWith<$Res> { factory _$$BarImplCopyWith(_$BarImpl value, $Res Function(_$BarImpl) then) = __$$BarImplCopyWithImpl<$Res>; - @override @useResult $Res call({Order order}); - @override $OrderCopyWith<$Res> get order; } @@ -1287,6 +1322,16 @@ class __$$BarImplCopyWithImpl<$Res> ), ); } + + /// Create a copy of PrintStruckEvent + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $OrderCopyWith<$Res> get order { + return $OrderCopyWith<$Res>(_value.order, (value) { + return _then(_value.copyWith(order: value)); + }); + } } /// @nodoc @@ -1332,6 +1377,7 @@ class _$BarImpl implements _Bar { required TResult Function(Order order) kitchen, required TResult Function(Order order) bar, required TResult Function(Order order) splitBill, + required TResult Function() retryFailed, }) { return bar(this.order); } @@ -1347,6 +1393,7 @@ class _$BarImpl implements _Bar { TResult? Function(Order order)? kitchen, TResult? Function(Order order)? bar, TResult? Function(Order order)? splitBill, + TResult? Function()? retryFailed, }) { return bar?.call(this.order); } @@ -1362,6 +1409,7 @@ class _$BarImpl implements _Bar { TResult Function(Order order)? kitchen, TResult Function(Order order)? bar, TResult Function(Order order)? splitBill, + TResult Function()? retryFailed, required TResult orElse(), }) { if (bar != null) { @@ -1381,6 +1429,7 @@ class _$BarImpl implements _Bar { required TResult Function(_Kitchen value) kitchen, required TResult Function(_Bar value) bar, required TResult Function(_SplitBill value) splitBill, + required TResult Function(_RetryFailed value) retryFailed, }) { return bar(this); } @@ -1396,6 +1445,7 @@ class _$BarImpl implements _Bar { TResult? Function(_Kitchen value)? kitchen, TResult? Function(_Bar value)? bar, TResult? Function(_SplitBill value)? splitBill, + TResult? Function(_RetryFailed value)? retryFailed, }) { return bar?.call(this); } @@ -1411,6 +1461,7 @@ class _$BarImpl implements _Bar { TResult Function(_Kitchen value)? kitchen, TResult Function(_Bar value)? bar, TResult Function(_SplitBill value)? splitBill, + TResult Function(_RetryFailed value)? retryFailed, required TResult orElse(), }) { if (bar != null) { @@ -1423,29 +1474,24 @@ class _$BarImpl implements _Bar { abstract class _Bar implements PrintStruckEvent { const factory _Bar(final Order order) = _$BarImpl; - @override Order get order; /// Create a copy of PrintStruckEvent /// with the given fields replaced by the non-null parameter values. - @override @JsonKey(includeFromJson: false, includeToJson: false) _$$BarImplCopyWith<_$BarImpl> get copyWith => throw _privateConstructorUsedError; } /// @nodoc -abstract class _$$SplitBillImplCopyWith<$Res> - implements $PrintStruckEventCopyWith<$Res> { +abstract class _$$SplitBillImplCopyWith<$Res> { factory _$$SplitBillImplCopyWith( _$SplitBillImpl value, $Res Function(_$SplitBillImpl) then, ) = __$$SplitBillImplCopyWithImpl<$Res>; - @override @useResult $Res call({Order order}); - @override $OrderCopyWith<$Res> get order; } @@ -1472,6 +1518,16 @@ class __$$SplitBillImplCopyWithImpl<$Res> ), ); } + + /// Create a copy of PrintStruckEvent + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $OrderCopyWith<$Res> get order { + return $OrderCopyWith<$Res>(_value.order, (value) { + return _then(_value.copyWith(order: value)); + }); + } } /// @nodoc @@ -1517,6 +1573,7 @@ class _$SplitBillImpl implements _SplitBill { required TResult Function(Order order) kitchen, required TResult Function(Order order) bar, required TResult Function(Order order) splitBill, + required TResult Function() retryFailed, }) { return splitBill(this.order); } @@ -1532,6 +1589,7 @@ class _$SplitBillImpl implements _SplitBill { TResult? Function(Order order)? kitchen, TResult? Function(Order order)? bar, TResult? Function(Order order)? splitBill, + TResult? Function()? retryFailed, }) { return splitBill?.call(this.order); } @@ -1547,6 +1605,7 @@ class _$SplitBillImpl implements _SplitBill { TResult Function(Order order)? kitchen, TResult Function(Order order)? bar, TResult Function(Order order)? splitBill, + TResult Function()? retryFailed, required TResult orElse(), }) { if (splitBill != null) { @@ -1566,6 +1625,7 @@ class _$SplitBillImpl implements _SplitBill { required TResult Function(_Kitchen value) kitchen, required TResult Function(_Bar value) bar, required TResult Function(_SplitBill value) splitBill, + required TResult Function(_RetryFailed value) retryFailed, }) { return splitBill(this); } @@ -1581,6 +1641,7 @@ class _$SplitBillImpl implements _SplitBill { TResult? Function(_Kitchen value)? kitchen, TResult? Function(_Bar value)? bar, TResult? Function(_SplitBill value)? splitBill, + TResult? Function(_RetryFailed value)? retryFailed, }) { return splitBill?.call(this); } @@ -1596,6 +1657,7 @@ class _$SplitBillImpl implements _SplitBill { TResult Function(_Kitchen value)? kitchen, TResult Function(_Bar value)? bar, TResult Function(_SplitBill value)? splitBill, + TResult Function(_RetryFailed value)? retryFailed, required TResult orElse(), }) { if (splitBill != null) { @@ -1608,23 +1670,178 @@ class _$SplitBillImpl implements _SplitBill { abstract class _SplitBill implements PrintStruckEvent { const factory _SplitBill(final Order order) = _$SplitBillImpl; - @override Order get order; /// Create a copy of PrintStruckEvent /// with the given fields replaced by the non-null parameter values. - @override @JsonKey(includeFromJson: false, includeToJson: false) _$$SplitBillImplCopyWith<_$SplitBillImpl> get copyWith => throw _privateConstructorUsedError; } +/// @nodoc +abstract class _$$RetryFailedImplCopyWith<$Res> { + factory _$$RetryFailedImplCopyWith( + _$RetryFailedImpl value, + $Res Function(_$RetryFailedImpl) then, + ) = __$$RetryFailedImplCopyWithImpl<$Res>; +} + +/// @nodoc +class __$$RetryFailedImplCopyWithImpl<$Res> + extends _$PrintStruckEventCopyWithImpl<$Res, _$RetryFailedImpl> + implements _$$RetryFailedImplCopyWith<$Res> { + __$$RetryFailedImplCopyWithImpl( + _$RetryFailedImpl _value, + $Res Function(_$RetryFailedImpl) _then, + ) : super(_value, _then); + + /// Create a copy of PrintStruckEvent + /// with the given fields replaced by the non-null parameter values. +} + +/// @nodoc + +class _$RetryFailedImpl implements _RetryFailed { + const _$RetryFailedImpl(); + + @override + String toString() { + return 'PrintStruckEvent.retryFailed()'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && other is _$RetryFailedImpl); + } + + @override + int get hashCode => runtimeType.hashCode; + + @override + @optionalTypeArgs + TResult when({ + required TResult Function(Order order) order, + required TResult Function(Order order) cashier, + required TResult Function(Order order) payment, + required TResult Function(Order order) voided, + required TResult Function(Order order) checker, + required TResult Function(Order order) kitchen, + required TResult Function(Order order) bar, + required TResult Function(Order order) splitBill, + required TResult Function() retryFailed, + }) { + return retryFailed(); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(Order order)? order, + TResult? Function(Order order)? cashier, + TResult? Function(Order order)? payment, + TResult? Function(Order order)? voided, + TResult? Function(Order order)? checker, + TResult? Function(Order order)? kitchen, + TResult? Function(Order order)? bar, + TResult? Function(Order order)? splitBill, + TResult? Function()? retryFailed, + }) { + return retryFailed?.call(); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(Order order)? order, + TResult Function(Order order)? cashier, + TResult Function(Order order)? payment, + TResult Function(Order order)? voided, + TResult Function(Order order)? checker, + TResult Function(Order order)? kitchen, + TResult Function(Order order)? bar, + TResult Function(Order order)? splitBill, + TResult Function()? retryFailed, + required TResult orElse(), + }) { + if (retryFailed != null) { + return retryFailed(); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_Order value) order, + required TResult Function(_Cashier value) cashier, + required TResult Function(_Payment value) payment, + required TResult Function(_Voided value) voided, + required TResult Function(_Checker value) checker, + required TResult Function(_Kitchen value) kitchen, + required TResult Function(_Bar value) bar, + required TResult Function(_SplitBill value) splitBill, + required TResult Function(_RetryFailed value) retryFailed, + }) { + return retryFailed(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_Order value)? order, + TResult? Function(_Cashier value)? cashier, + TResult? Function(_Payment value)? payment, + TResult? Function(_Voided value)? voided, + TResult? Function(_Checker value)? checker, + TResult? Function(_Kitchen value)? kitchen, + TResult? Function(_Bar value)? bar, + TResult? Function(_SplitBill value)? splitBill, + TResult? Function(_RetryFailed value)? retryFailed, + }) { + return retryFailed?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_Order value)? order, + TResult Function(_Cashier value)? cashier, + TResult Function(_Payment value)? payment, + TResult Function(_Voided value)? voided, + TResult Function(_Checker value)? checker, + TResult Function(_Kitchen value)? kitchen, + TResult Function(_Bar value)? bar, + TResult Function(_SplitBill value)? splitBill, + TResult Function(_RetryFailed value)? retryFailed, + required TResult orElse(), + }) { + if (retryFailed != null) { + return retryFailed(this); + } + return orElse(); + } +} + +abstract class _RetryFailed implements PrintStruckEvent { + const factory _RetryFailed() = _$RetryFailedImpl; +} + /// @nodoc mixin _$PrintStruckState { - Option> get failureOrPrintStruck => - throw _privateConstructorUsedError; + /// Status seluruh station di job cetak terakhir, urut sesuai urutan cetak. + List get stations => throw _privateConstructorUsedError; bool get isPrinting => throw _privateConstructorUsedError; + /// Order yang sedang dicetak, disimpan supaya station yang gagal bisa + /// diulang tanpa perlu dikirim ulang dari halaman pemanggil. + Order? get order => throw _privateConstructorUsedError; + + /// Naik tiap job baru dimulai. UI memakainya untuk tahu kapan harus + /// membuka dialog progres. + int get jobId => throw _privateConstructorUsedError; + /// Create a copy of PrintStruckState /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @@ -1640,9 +1857,13 @@ abstract class $PrintStruckStateCopyWith<$Res> { ) = _$PrintStruckStateCopyWithImpl<$Res, PrintStruckState>; @useResult $Res call({ - Option> failureOrPrintStruck, + List stations, bool isPrinting, + Order? order, + int jobId, }); + + $OrderCopyWith<$Res>? get order; } /// @nodoc @@ -1659,21 +1880,48 @@ class _$PrintStruckStateCopyWithImpl<$Res, $Val extends PrintStruckState> /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override - $Res call({Object? failureOrPrintStruck = null, Object? isPrinting = null}) { + $Res call({ + Object? stations = null, + Object? isPrinting = null, + Object? order = freezed, + Object? jobId = null, + }) { return _then( _value.copyWith( - failureOrPrintStruck: null == failureOrPrintStruck - ? _value.failureOrPrintStruck - : failureOrPrintStruck // ignore: cast_nullable_to_non_nullable - as Option>, + stations: null == stations + ? _value.stations + : stations // ignore: cast_nullable_to_non_nullable + as List, isPrinting: null == isPrinting ? _value.isPrinting : isPrinting // ignore: cast_nullable_to_non_nullable as bool, + order: freezed == order + ? _value.order + : order // ignore: cast_nullable_to_non_nullable + as Order?, + jobId: null == jobId + ? _value.jobId + : jobId // ignore: cast_nullable_to_non_nullable + as int, ) as $Val, ); } + + /// Create a copy of PrintStruckState + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $OrderCopyWith<$Res>? get order { + if (_value.order == null) { + return null; + } + + return $OrderCopyWith<$Res>(_value.order!, (value) { + return _then(_value.copyWith(order: value) as $Val); + }); + } } /// @nodoc @@ -1686,9 +1934,14 @@ abstract class _$$PrintStruckStateImplCopyWith<$Res> @override @useResult $Res call({ - Option> failureOrPrintStruck, + List stations, bool isPrinting, + Order? order, + int jobId, }); + + @override + $OrderCopyWith<$Res>? get order; } /// @nodoc @@ -1704,17 +1957,30 @@ class __$$PrintStruckStateImplCopyWithImpl<$Res> /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override - $Res call({Object? failureOrPrintStruck = null, Object? isPrinting = null}) { + $Res call({ + Object? stations = null, + Object? isPrinting = null, + Object? order = freezed, + Object? jobId = null, + }) { return _then( _$PrintStruckStateImpl( - failureOrPrintStruck: null == failureOrPrintStruck - ? _value.failureOrPrintStruck - : failureOrPrintStruck // ignore: cast_nullable_to_non_nullable - as Option>, + stations: null == stations + ? _value._stations + : stations // ignore: cast_nullable_to_non_nullable + as List, isPrinting: null == isPrinting ? _value.isPrinting : isPrinting // ignore: cast_nullable_to_non_nullable as bool, + order: freezed == order + ? _value.order + : order // ignore: cast_nullable_to_non_nullable + as Order?, + jobId: null == jobId + ? _value.jobId + : jobId // ignore: cast_nullable_to_non_nullable + as int, ), ); } @@ -1722,21 +1988,45 @@ class __$$PrintStruckStateImplCopyWithImpl<$Res> /// @nodoc -class _$PrintStruckStateImpl implements _PrintStruckState { - _$PrintStruckStateImpl({ - required this.failureOrPrintStruck, +class _$PrintStruckStateImpl extends _PrintStruckState { + const _$PrintStruckStateImpl({ + final List stations = const [], this.isPrinting = false, - }); + this.order, + this.jobId = 0, + }) : _stations = stations, + super._(); + /// Status seluruh station di job cetak terakhir, urut sesuai urutan cetak. + final List _stations; + + /// Status seluruh station di job cetak terakhir, urut sesuai urutan cetak. @override - final Option> failureOrPrintStruck; + @JsonKey() + List get stations { + if (_stations is EqualUnmodifiableListView) return _stations; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_stations); + } + @override @JsonKey() final bool isPrinting; + /// Order yang sedang dicetak, disimpan supaya station yang gagal bisa + /// diulang tanpa perlu dikirim ulang dari halaman pemanggil. + @override + final Order? order; + + /// Naik tiap job baru dimulai. UI memakainya untuk tahu kapan harus + /// membuka dialog progres. + @override + @JsonKey() + final int jobId; + @override String toString() { - return 'PrintStruckState(failureOrPrintStruck: $failureOrPrintStruck, isPrinting: $isPrinting)'; + return 'PrintStruckState(stations: $stations, isPrinting: $isPrinting, order: $order, jobId: $jobId)'; } @override @@ -1744,15 +2034,21 @@ class _$PrintStruckStateImpl implements _PrintStruckState { return identical(this, other) || (other.runtimeType == runtimeType && other is _$PrintStruckStateImpl && - (identical(other.failureOrPrintStruck, failureOrPrintStruck) || - other.failureOrPrintStruck == failureOrPrintStruck) && + const DeepCollectionEquality().equals(other._stations, _stations) && (identical(other.isPrinting, isPrinting) || - other.isPrinting == isPrinting)); + other.isPrinting == isPrinting) && + (identical(other.order, order) || other.order == order) && + (identical(other.jobId, jobId) || other.jobId == jobId)); } @override - int get hashCode => - Object.hash(runtimeType, failureOrPrintStruck, isPrinting); + int get hashCode => Object.hash( + runtimeType, + const DeepCollectionEquality().hash(_stations), + isPrinting, + order, + jobId, + ); /// Create a copy of PrintStruckState /// with the given fields replaced by the non-null parameter values. @@ -1766,17 +2062,31 @@ class _$PrintStruckStateImpl implements _PrintStruckState { ); } -abstract class _PrintStruckState implements PrintStruckState { - factory _PrintStruckState({ - required final Option> failureOrPrintStruck, +abstract class _PrintStruckState extends PrintStruckState { + const factory _PrintStruckState({ + final List stations, final bool isPrinting, + final Order? order, + final int jobId, }) = _$PrintStruckStateImpl; + const _PrintStruckState._() : super._(); + /// Status seluruh station di job cetak terakhir, urut sesuai urutan cetak. @override - Option> get failureOrPrintStruck; + List get stations; @override bool get isPrinting; + /// Order yang sedang dicetak, disimpan supaya station yang gagal bisa + /// diulang tanpa perlu dikirim ulang dari halaman pemanggil. + @override + Order? get order; + + /// Naik tiap job baru dimulai. UI memakainya untuk tahu kapan harus + /// membuka dialog progres. + @override + int get jobId; + /// Create a copy of PrintStruckState /// with the given fields replaced by the non-null parameter values. @override diff --git a/lib/application/printer/print_struck/print_struck_event.dart b/lib/application/printer/print_struck/print_struck_event.dart index c85aed5..c0f384c 100644 --- a/lib/application/printer/print_struck/print_struck_event.dart +++ b/lib/application/printer/print_struck/print_struck_event.dart @@ -2,7 +2,9 @@ part of 'print_struck_bloc.dart'; @freezed class PrintStruckEvent with _$PrintStruckEvent { + /// Cetak seluruh station untuk satu order. const factory PrintStruckEvent.order(Order order) = _Order; + const factory PrintStruckEvent.cashier(Order order) = _Cashier; const factory PrintStruckEvent.payment(Order order) = _Payment; const factory PrintStruckEvent.voided(Order order) = _Voided; @@ -10,4 +12,8 @@ class PrintStruckEvent with _$PrintStruckEvent { const factory PrintStruckEvent.kitchen(Order order) = _Kitchen; const factory PrintStruckEvent.bar(Order order) = _Bar; const factory PrintStruckEvent.splitBill(Order order) = _SplitBill; + + /// Ulangi hanya station yang gagal di job terakhir, tanpa mencetak ulang + /// station yang sudah keluar. + const factory PrintStruckEvent.retryFailed() = _RetryFailed; } diff --git a/lib/application/printer/print_struck/print_struck_state.dart b/lib/application/printer/print_struck/print_struck_state.dart index 2aeec9c..8a061ab 100644 --- a/lib/application/printer/print_struck/print_struck_state.dart +++ b/lib/application/printer/print_struck/print_struck_state.dart @@ -2,11 +2,33 @@ part of 'print_struck_bloc.dart'; @freezed class PrintStruckState with _$PrintStruckState { - factory PrintStruckState({ - required Option> failureOrPrintStruck, + const PrintStruckState._(); + + const factory PrintStruckState({ + /// Status seluruh station di job cetak terakhir, urut sesuai urutan cetak. + @Default([]) List stations, @Default(false) bool isPrinting, + + /// Order yang sedang dicetak, disimpan supaya station yang gagal bisa + /// diulang tanpa perlu dikirim ulang dari halaman pemanggil. + Order? order, + + /// Naik tiap job baru dimulai. UI memakainya untuk tahu kapan harus + /// membuka dialog progres. + @Default(0) int jobId, }) = _PrintStruckState; - factory PrintStruckState.initial() => - PrintStruckState(failureOrPrintStruck: none()); + factory PrintStruckState.initial() => const PrintStruckState(); + + List get failedStations => + stations.where((station) => station.isFailed).toList(); + + bool get hasFailure => failedStations.isNotEmpty; + + /// Station yang benar-benar dicetak — yang dilewati tidak ikut dihitung. + int get plannedCount => + stations.where((station) => !station.isSkipped).length; + + int get successCount => + stations.where((station) => station.isSuccess).length; } diff --git a/lib/domain/printer/entities/print_station_result.dart b/lib/domain/printer/entities/print_station_result.dart new file mode 100644 index 0000000..bcc33d8 --- /dev/null +++ b/lib/domain/printer/entities/print_station_result.dart @@ -0,0 +1,83 @@ +part of '../printer.dart'; + +/// Satu tujuan cetak dalam sekali proses print. +/// +/// Perhatikan kasir, void, dan split bill sengaja menumpang printer `receipt` +/// — ketiganya tidak punya printer sendiri di halaman pengaturan. +enum PrintStation { + receipt, + cashier, + checker, + kitchen, + bar, + voided, + splitBill; + + String get label => switch (this) { + PrintStation.receipt => 'Struk Customer', + PrintStation.cashier => 'Struk Kasir', + PrintStation.checker => 'Checker', + PrintStation.kitchen => 'Kitchen', + PrintStation.bar => 'Bar', + PrintStation.voided => 'Struk Void', + PrintStation.splitBill => 'Struk Split Bill', + }; + + /// Kode printer di tabel `printers`. + String get printerCode => switch (this) { + PrintStation.receipt || + PrintStation.cashier || + PrintStation.voided || + PrintStation.splitBill => 'receipt', + PrintStation.checker => 'checker', + PrintStation.kitchen => 'kitchen', + PrintStation.bar => 'bar', + }; + + /// Station yang hanya mencetak item dengan `printerType` tertentu. + /// Station lain mencetak seluruh item order. + String? get itemPrinterType => switch (this) { + PrintStation.kitchen => 'kitchen', + PrintStation.bar => 'bar', + _ => null, + }; + +} + +enum PrintStationStatus { + /// Belum dijalankan. + pending, + + /// Sedang dikirim ke printer. + printing, + + /// Berhasil dicetak. + success, + + /// Gagal — lihat `message`. + failed, + + /// Sengaja dilewati, misal tidak ada item untuk station tersebut. + /// Ini bukan kegagalan dan tidak perlu dicetak ulang. + skipped, +} + +@freezed +class PrintStationResult with _$PrintStationResult { + const PrintStationResult._(); + + const factory PrintStationResult({ + required PrintStation station, + required PrintStationStatus status, + String? message, + @Default(Duration.zero) Duration elapsed, + }) = _PrintStationResult; + + factory PrintStationResult.pending(PrintStation station) => + PrintStationResult(station: station, status: PrintStationStatus.pending); + + bool get isSuccess => status == PrintStationStatus.success; + bool get isFailed => status == PrintStationStatus.failed; + bool get isSkipped => status == PrintStationStatus.skipped; + bool get isFinished => isSuccess || isFailed || isSkipped; +} diff --git a/lib/domain/printer/printer.dart b/lib/domain/printer/printer.dart index 2d1fc04..f0fefe1 100644 --- a/lib/domain/printer/printer.dart +++ b/lib/domain/printer/printer.dart @@ -9,5 +9,6 @@ import '../order/order.dart'; part 'printer.freezed.dart'; part 'entities/printer_entity.dart'; +part 'entities/print_station_result.dart'; part 'failures/printer_failure.dart'; part 'repositories/i_printer_repository.dart'; diff --git a/lib/domain/printer/printer.freezed.dart b/lib/domain/printer/printer.freezed.dart index 6b66cc7..170f257 100644 --- a/lib/domain/printer/printer.freezed.dart +++ b/lib/domain/printer/printer.freezed.dart @@ -307,6 +307,216 @@ abstract class _Printer implements Printer { throw _privateConstructorUsedError; } +/// @nodoc +mixin _$PrintStationResult { + PrintStation get station => throw _privateConstructorUsedError; + PrintStationStatus get status => throw _privateConstructorUsedError; + String? get message => throw _privateConstructorUsedError; + Duration get elapsed => throw _privateConstructorUsedError; + + /// Create a copy of PrintStationResult + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $PrintStationResultCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $PrintStationResultCopyWith<$Res> { + factory $PrintStationResultCopyWith( + PrintStationResult value, + $Res Function(PrintStationResult) then, + ) = _$PrintStationResultCopyWithImpl<$Res, PrintStationResult>; + @useResult + $Res call({ + PrintStation station, + PrintStationStatus status, + String? message, + Duration elapsed, + }); +} + +/// @nodoc +class _$PrintStationResultCopyWithImpl<$Res, $Val extends PrintStationResult> + implements $PrintStationResultCopyWith<$Res> { + _$PrintStationResultCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of PrintStationResult + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? station = null, + Object? status = null, + Object? message = freezed, + Object? elapsed = null, + }) { + return _then( + _value.copyWith( + station: null == station + ? _value.station + : station // ignore: cast_nullable_to_non_nullable + as PrintStation, + status: null == status + ? _value.status + : status // ignore: cast_nullable_to_non_nullable + as PrintStationStatus, + message: freezed == message + ? _value.message + : message // ignore: cast_nullable_to_non_nullable + as String?, + elapsed: null == elapsed + ? _value.elapsed + : elapsed // ignore: cast_nullable_to_non_nullable + as Duration, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$PrintStationResultImplCopyWith<$Res> + implements $PrintStationResultCopyWith<$Res> { + factory _$$PrintStationResultImplCopyWith( + _$PrintStationResultImpl value, + $Res Function(_$PrintStationResultImpl) then, + ) = __$$PrintStationResultImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + PrintStation station, + PrintStationStatus status, + String? message, + Duration elapsed, + }); +} + +/// @nodoc +class __$$PrintStationResultImplCopyWithImpl<$Res> + extends _$PrintStationResultCopyWithImpl<$Res, _$PrintStationResultImpl> + implements _$$PrintStationResultImplCopyWith<$Res> { + __$$PrintStationResultImplCopyWithImpl( + _$PrintStationResultImpl _value, + $Res Function(_$PrintStationResultImpl) _then, + ) : super(_value, _then); + + /// Create a copy of PrintStationResult + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? station = null, + Object? status = null, + Object? message = freezed, + Object? elapsed = null, + }) { + return _then( + _$PrintStationResultImpl( + station: null == station + ? _value.station + : station // ignore: cast_nullable_to_non_nullable + as PrintStation, + status: null == status + ? _value.status + : status // ignore: cast_nullable_to_non_nullable + as PrintStationStatus, + message: freezed == message + ? _value.message + : message // ignore: cast_nullable_to_non_nullable + as String?, + elapsed: null == elapsed + ? _value.elapsed + : elapsed // ignore: cast_nullable_to_non_nullable + as Duration, + ), + ); + } +} + +/// @nodoc + +class _$PrintStationResultImpl extends _PrintStationResult { + const _$PrintStationResultImpl({ + required this.station, + required this.status, + this.message, + this.elapsed = Duration.zero, + }) : super._(); + + @override + final PrintStation station; + @override + final PrintStationStatus status; + @override + final String? message; + @override + @JsonKey() + final Duration elapsed; + + @override + String toString() { + return 'PrintStationResult(station: $station, status: $status, message: $message, elapsed: $elapsed)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$PrintStationResultImpl && + (identical(other.station, station) || other.station == station) && + (identical(other.status, status) || other.status == status) && + (identical(other.message, message) || other.message == message) && + (identical(other.elapsed, elapsed) || other.elapsed == elapsed)); + } + + @override + int get hashCode => + Object.hash(runtimeType, station, status, message, elapsed); + + /// Create a copy of PrintStationResult + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$PrintStationResultImplCopyWith<_$PrintStationResultImpl> get copyWith => + __$$PrintStationResultImplCopyWithImpl<_$PrintStationResultImpl>( + this, + _$identity, + ); +} + +abstract class _PrintStationResult extends PrintStationResult { + const factory _PrintStationResult({ + required final PrintStation station, + required final PrintStationStatus status, + final String? message, + final Duration elapsed, + }) = _$PrintStationResultImpl; + const _PrintStationResult._() : super._(); + + @override + PrintStation get station; + @override + PrintStationStatus get status; + @override + String? get message; + @override + Duration get elapsed; + + /// Create a copy of PrintStationResult + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$PrintStationResultImplCopyWith<_$PrintStationResultImpl> get copyWith => + throw _privateConstructorUsedError; +} + /// @nodoc mixin _$PrinterFailure { @optionalTypeArgs diff --git a/lib/domain/printer/repositories/i_printer_repository.dart b/lib/domain/printer/repositories/i_printer_repository.dart index 331859d..671edb6 100644 --- a/lib/domain/printer/repositories/i_printer_repository.dart +++ b/lib/domain/printer/repositories/i_printer_repository.dart @@ -15,25 +15,17 @@ abstract class IPrinterRepository { List printValue, ); - Future> printStruckOrder({required Order order}); - Future> printStruckSaveOrder({ - required Order order, - }); - Future> printStruckCashier({ - required Order order, - }); - Future> printStruckPayment({ - required Order order, - }); - Future> printStruckChecker({ - required Order order, - }); - Future> printStruckKitchen({ - required Order order, - }); - Future> printStruckBar({required Order order}); - Future> printStruckVoid({required Order order}); - Future> printStruckSplitBill({ + /// Mencetak [stations] untuk [order], melaporkan status tiap station. + /// + /// Setiap emit berisi daftar lengkap semua station, jadi UI cukup + /// menggambar ulang apa adanya. + /// + /// Satu station yang gagal **tidak** membatalkan station lainnya — semuanya + /// tetap dicoba. Station Bluetooth dijalankan berurutan karena radionya + /// hanya satu, station Network dijalankan paralel karena tidak berebut + /// sumber daya dengan Bluetooth. + Stream> printStations({ required Order order, + required List stations, }); } diff --git a/lib/infrastructure/printer/repositories/printer_repository.dart b/lib/infrastructure/printer/repositories/printer_repository.dart index e1a4305..9f29afb 100644 --- a/lib/infrastructure/printer/repositories/printer_repository.dart +++ b/lib/infrastructure/printer/repositories/printer_repository.dart @@ -1,9 +1,11 @@ +import 'dart:async'; import 'dart:developer'; import 'package:dartz/dartz.dart' hide Order; import 'package:firebase_crashlytics/firebase_crashlytics.dart'; import 'package:flutter_esc_pos_network/flutter_esc_pos_network.dart'; import 'package:injectable/injectable.dart' hide Order; +import 'package:permission_handler/permission_handler.dart'; import 'package:print_bluetooth_thermal/print_bluetooth_thermal.dart'; import 'package:synchronized/synchronized.dart'; @@ -17,7 +19,7 @@ import '../../outlet/datasources/local_data_provider.dart'; import '../datasource/local_data_provider.dart'; import '../printer_dtos.dart'; -@Injectable(as: IPrinterRepository) +@LazySingleton(as: IPrinterRepository) class PrinterRepository implements IPrinterRepository { final PrinterLocalDataProvider _localDataProvider; final OutletLocalDatasource _outletLocalDatasource; @@ -30,78 +32,36 @@ class PrinterRepository implements IPrinterRepository { this._authLocalDataProvider, ); - final Map _locks = {}; - final Lock _globalPrintLock = Lock(); + /// Seluruh cetakan Bluetooth berbagi satu lock: radionya cuma satu, jadi + /// lock per alamat tidak menahan dua printer Bluetooth yang berbeda. + final Lock _bluetoothLock = Lock(); + + /// Menjaga satu job cetak selesai sebelum job berikutnya mulai, supaya + /// tiket dua order tidak saling menyelip. + final Lock _jobLock = Lock(); + String? _connectedMac; - Lock _getLock(String address) { - return _locks.putIfAbsent(address, () => Lock()); - } - @override - Future> connectBluetooth( - String macAddress, - ) async { - try { - bool isConnected = await PrintBluetoothThermal.connectionStatus; - if (isConnected) { - log("Already connected to Bluetooth printer", name: _logName); - return right(true); - } - - bool connected = await PrintBluetoothThermal.connect( - macPrinterAddress: macAddress, - ); - - if (connected) { - log( - "Successfully connected to Bluetooth printer: $macAddress", - name: _logName, - ); - } else { - FirebaseCrashlytics.instance.recordError( - 'Failed to connect to Bluetooth printer', - null, - reason: 'Failed to connect to Bluetooth printe', - information: [ - 'function: connectBluetoothPrinter(String macAddress)', - 'macAddress: $macAddress', - ], - ); - log( - "Failed to connect to Bluetooth printer: $macAddress", - name: _logName, - ); - } - - return right(connected); - } catch (e, stackTrace) { - FirebaseCrashlytics.instance.recordError( - e, - stackTrace, - reason: 'Error connecting to Bluetooth printer', - information: [ - 'function: connectBluetoothPrinter(String macAddress)', - 'Printer: Bluetooth printer', - 'macAddress: $macAddress', - ], - ); - log("Error connecting to Bluetooth printer", name: _logName, error: e); - return left( - const PrinterFailure.dynamicErrorMessage( - 'Error connecting to Bluetooth printer', - ), - ); - } + Future> connectBluetooth(String macAddress) { + // Memakai jalur yang sama dengan mencetak: retry, penutupan socket basi, + // dan pencatatan `_connectedMac`. Versi sebelumnya mengembalikan sukses + // begitu ADA koneksi, tanpa memeriksa apakah itu printer yang diminta — + // menyambungkan printer B saat A tersambung akan tetap "berhasil". + return _bluetoothLock.synchronized( + () => _ensureBluetoothConnected(macAddress), + ); } @override Future> disconectBluetooth() async { try { bool result = await PrintBluetoothThermal.disconnect; + _connectedMac = null; log("Bluetooth printer disconnected: $result", name: _logName); return right(result); } catch (e) { + _connectedMac = null; log("Error disconnecting Bluetooth printer", error: e, name: _logName); return left( const PrinterFailure.dynamicErrorMessage( @@ -220,177 +180,348 @@ class PrinterRepository implements IPrinterRepository { } } + // =========================================================================== + // Timing Bluetooth + // + // Radio Bluetooth cuma satu, jadi pindah printer berarti disconnect lalu + // connect ulang. Tiga jeda di bawah melindungi hal yang berbeda; ketiganya + // sempat dipangkas ke 300ms (atau dihapus) dan itu yang membuat tiket + // station setelah perpindahan koneksi sering hilang. + // =========================================================================== + + /// Jeda setelah byte terakhir dikirim, sebelum koneksi boleh diputus. + /// `writeBytes` selesai saat byte masuk socket, bukan saat kertas keluar. + static const _kPostPrintSettle = Duration(milliseconds: 500); + + /// Jeda setelah connect berhasil. Channel SPP dan firmware printer belum + /// tentu siap saat `connect()` return; menulis terlalu cepat membuat + /// byte-byte awal hilang. + static const _kPostConnectSettle = Duration(milliseconds: 800); + + /// Batas menunggu link Bluetooth benar-benar lepas setelah disconnect. + static const _kDisconnectTimeout = Duration(seconds: 2); + + /// Backoff antar percobaan connect. Panjangnya menentukan jumlah percobaan. + /// Retry hanya berjalan saat gagal, jadi tidak menambah waktu di jalur normal. + static const _kConnectBackoff = [ + Duration(milliseconds: 500), + Duration(milliseconds: 1200), + ]; + + static const _kChunkSize = 1024; + static const _kChunkDelay = Duration(milliseconds: 50); + @override Future> printStruct( Printer printer, List printData, ) async { if (printer.type == 'Bluetooth') { - return await _getLock(printer.address).synchronized(() async { - try { - bool isConnected = await PrintBluetoothThermal.connectionStatus; + return _bluetoothLock.synchronized( + () => _printBluetooth(printer, printData), + ); + } + return _printToNetworkPrinter(printer, printData); + } - // Hanya reconnect jika memang perlu - if (!isConnected || _connectedMac != printer.address) { - // Disconnect hanya jika terhubung ke printer LAIN - if (isConnected && _connectedMac != printer.address) { - await PrintBluetoothThermal.disconnect; - await Future.delayed( - const Duration(milliseconds: 300), - ); // dari 1500ms - } + Future> _printBluetooth( + Printer printer, + List printData, + ) async { + try { + final connected = await _ensureBluetoothConnected(printer.address); + if (connected.isLeft()) return connected; - bool connected = await PrintBluetoothThermal.connect( - macPrinterAddress: printer.address, - ); + log( + 'Printer connected to bluetooth: ${printer.name}, ${printer.address}', + name: _logName, + ); - if (!connected) { - _connectedMac = null; - FirebaseCrashlytics.instance.recordError( - 'Failed to connect to Bluetooth printer', - null, - reason: 'Failed to connect to Bluetooth print', - information: [ - 'function: printStruct(Printer printer, List printData)', - 'Printer: ${printer.name}', - 'macAddress: ${printer.address}', - 'in: $_logName', - ], - ); - return left( - const PrinterFailure.dynamicErrorMessage( - 'Printer cannot connect to bluetooth, Please connect in printer setting!', - ), - ); - } + final printResult = await _writeBytesChunked(printData); + log('Print result ${printer.name}: $printResult', name: _logName); - _connectedMac = printer.address; - await Future.delayed( - const Duration(milliseconds: 300), - ); // dari 1000ms - } - - log( - 'Printer connected to bluetooth: ${printer.name}, ${printer.address}', - name: _logName, - ); - - bool printResult = await _writeBytesChunked(printData); - - // Hapus delay 1000ms setelah print — tidak perlu untuk hasil cetak - log("Print result ${printer.name}: $printResult"); - - if (!printResult) { - FirebaseCrashlytics.instance.recordError( - 'Failed to print to ${printer.name}', - null, - information: [ - 'function: await _writeBytesChunked(printData)', - 'print: $printData', - 'in: $_logName', - ], - ); - return left( - PrinterFailure.dynamicErrorMessage( - 'Failed to print to ${printer.name}', - ), - ); - } - - return right(printResult); - } catch (e, stackTrace) { - _connectedMac = null; - FirebaseCrashlytics.instance.recordError( - e, - stackTrace, - reason: 'Error printing ${printer.name}', - information: [ - 'Printer: ${printer.name}', - 'MAC: ${printer.address}', - ], - ); - return left( - const PrinterFailure.dynamicErrorMessage('Error printing struct'), - ); - } - }); - } else { - // Network printer tetap sama - try { - bool printResult = await _printNetwork(printer.address, printData); - - if (!printResult) { - FirebaseCrashlytics.instance.recordError( - 'Failed to connect to Network Printer', - null, - reason: 'Failed to connect to Network Printer', - information: [ - 'function: await _printNetwork(printer.address, printData)', - 'Printer: ${printer.name}', - 'ipAddress: ${printer.address}', - 'print: $printData', - 'in: $_logName', - ], - ); - } - - return right(printResult); - } catch (e, stackTrace) { + if (!printResult) { + // Tulis yang gagal di tengah jalan menyisakan koneksi dengan status + // tidak jelas — paksa reconnect pada cetakan berikutnya. + _connectedMac = null; FirebaseCrashlytics.instance.recordError( - e, - stackTrace, - reason: 'Error printing ${printer.name}', - information: ['Printer: ${printer.name}', 'IP: ${printer.address}'], + 'Failed to print to ${printer.name}', + null, + reason: 'Failed to write bytes to Bluetooth printer', + information: [ + 'function: _writeBytesChunked(printData)', + 'Printer: ${printer.name}', + 'MAC: ${printer.address}', + 'bytes: ${printData.length}', + 'in: $_logName', + ], ); return left( - const PrinterFailure.dynamicErrorMessage('Error printing struct'), + PrinterFailure.dynamicErrorMessage( + 'Gagal mengirim data ke ${printer.name}', + ), ); } + + // Printer masih menghabiskan buffer-nya. Memutus koneksi sekarang + // memotong cetakan, dan membuat connect berikutnya gagal. + await Future.delayed(_kPostPrintSettle); + + return right(true); + } catch (e, stackTrace) { + _connectedMac = null; + FirebaseCrashlytics.instance.recordError( + e, + stackTrace, + reason: 'Error printing ${printer.name}', + information: ['Printer: ${printer.name}', 'MAC: ${printer.address}'], + ); + log('Error printing bluetooth', name: _logName, error: e); + return left( + const PrinterFailure.dynamicErrorMessage('Error printing struct'), + ); } } - // Future _printBluetooth(List printData) async { - // try { - // bool isConnected = await PrintBluetoothThermal.connectionStatus; - // if (!isConnected) { - // log("Not connected to Bluetooth printer", name: _logName); - // return false; - // } + /// Memastikan radio tersambung ke [macAddress], reconnect kalau perlu. + Future> _ensureBluetoothConnected( + String macAddress, + ) async { + // Izin runtime Bluetooth hilang setiap kali app di-reinstall, dan sebelumnya + // hanya diminta dari dialog pilih perangkat di halaman pengaturan. Kalau + // kasir tidak pernah membuka dialog itu, `connect()` gagal terus tanpa + // penjelasan. Dipastikan di sini supaya semua jalur cetak ikut terlindungi. + final hasPermission = await _ensureBluetoothPermission(); - // bool printResult = await PrintBluetoothThermal.writeBytes(printData); - // if (printResult) { - // log("Successfully printed via Bluetooth", name: _logName); - // } else { - // FirebaseCrashlytics.instance.recordError( - // 'Failed to print via Bluetooth', - // null, - // reason: 'Failed to print via Bluetooth', - // information: [ - // 'function: printBluetooth(List printData)', - // 'printData: $printData', - // 'in: $_logName', - // ], - // ); - // log("Failed to print via Bluetooth", name: _logName); - // } + final isConnected = await PrintBluetoothThermal.connectionStatus; + final isAdapterOn = await _isBluetoothOn(); + final isPaired = await _isPaired(macAddress); - // return printResult; - // } catch (e, stackTrace) { - // FirebaseCrashlytics.instance.recordError( - // e, - // stackTrace, - // reason: 'Error printing via Bluetooth', - // information: [ - // 'function: printBluetooth(List printData)', - // 'Printer: Bluetooth printer', - // 'printData: $printData', - // 'in: $_logName', - // ], - // ); - // log("Error printing via Bluetooth: $e", name: _logName); - // return false; - // } - // } + log( + 'Bluetooth state: connected=$isConnected, ' + 'connectedMac=${_connectedMac ?? "-"}, target=$macAddress, ' + 'permission=$hasPermission, adapterOn=$isAdapterOn, paired=$isPaired', + name: _logName, + ); + + if (!hasPermission) { + return left( + const PrinterFailure.dynamicErrorMessage( + 'Izin Bluetooth belum diberikan. Buka pengaturan aplikasi dan izinkan akses "Perangkat di sekitar", lalu cetak ulang.', + ), + ); + } + + if (!isAdapterOn) { + return left( + const PrinterFailure.dynamicErrorMessage( + 'Bluetooth di perangkat ini sedang mati. Nyalakan Bluetooth, lalu cetak ulang.', + ), + ); + } + + // Mencoba connect ke perangkat yang tidak ter-pairing pasti gagal, jadi + // jangan buang waktu tiga kali percobaan untuk itu. + if (!isPaired) { + return left( + PrinterFailure.dynamicErrorMessage( + 'Printer ($macAddress) tidak ada di daftar perangkat Bluetooth yang terpasang. Pasangkan lewat pengaturan Bluetooth Android, lalu hubungkan lagi di pengaturan printer.', + ), + ); + } + + if (isConnected && _connectedMac == macAddress) { + return right(true); + } + + if (isConnected) { + await _forceDisconnect(); + } + + for (var attempt = 0; attempt <= _kConnectBackoff.length; attempt++) { + // 'read failed, socket might closed or timeout' berarti socket RFCOMM + // sebelumnya masih setengah terbuka. Connect ulang tanpa menutupnya + // lebih dulu akan gagal dengan error yang sama terus, jadi tiap + // percobaan ulang harus didahului disconnect. + if (attempt > 0) await _forceDisconnect(); + + final connected = await PrintBluetoothThermal.connect( + macPrinterAddress: macAddress, + ); + + if (connected) { + _connectedMac = macAddress; + await Future.delayed(_kPostConnectSettle); + return right(true); + } + + if (attempt < _kConnectBackoff.length) { + log( + 'Connect ke $macAddress gagal ' + '(percobaan ${attempt + 1}), tutup socket lalu ulangi', + name: _logName, + ); + await Future.delayed(_kConnectBackoff[attempt]); + } + } + + _connectedMac = null; + + FirebaseCrashlytics.instance.recordError( + 'Failed to connect to Bluetooth printer', + null, + reason: 'Failed to connect to Bluetooth printer after retries', + information: [ + 'function: _ensureBluetoothConnected(String macAddress)', + 'macAddress: $macAddress', + 'paired: $isPaired', + 'attempts: ${_kConnectBackoff.length + 1}', + 'in: $_logName', + ], + ); + + // Sampai di sini printer sudah dipastikan ter-pairing dan Bluetooth hidup, + // jadi sisanya adalah printer yang tidak merespons. + return left( + PrinterFailure.dynamicErrorMessage( + 'Printer ($macAddress) tidak merespons. Matikan lalu nyalakan printernya, pastikan tidak sedang tersambung ke perangkat atau aplikasi lain, lalu cetak ulang.', + ), + ); + } + + /// Apakah radio Bluetooth perangkat ini menyala. + Future _isBluetoothOn() async { + try { + return await PrintBluetoothThermal.bluetoothEnabled; + } catch (e) { + log('Gagal membaca status Bluetooth', name: _logName, error: e); + return true; // jangan menghalangi cetak kalau kita tidak tahu + } + } + + /// Apakah [macAddress] masih terdaftar sebagai perangkat Bluetooth ter-pairing. + Future _isPaired(String macAddress) async { + try { + final paired = await PrintBluetoothThermal.pairedBluetooths; + final target = macAddress.toUpperCase(); + + log( + 'Paired devices: ${paired.map((e) => "${e.name}=${e.macAdress}").join(", ")}', + name: _logName, + ); + + return paired.any((e) => e.macAdress.toUpperCase() == target); + } catch (e) { + log('Gagal membaca daftar paired device', name: _logName, error: e); + return true; // jangan menuduh belum ter-pairing kalau kita tidak tahu + } + } + + /// Memastikan izin runtime Bluetooth ada, meminta ke user kalau belum. + /// + /// Di Android 12+ `BLUETOOTH_CONNECT` adalah izin runtime — tanpa itu + /// `connect()` selalu gagal, dan errornya tidak menyebut soal izin sama + /// sekali sehingga tampak seperti printer rusak. + Future _ensureBluetoothPermission() async { + try { + var status = await Permission.bluetoothConnect.status; + + if (status.isDenied) { + status = await Permission.bluetoothConnect.request(); + } + + final scan = await Permission.bluetoothScan.status; + if (scan.isDenied) await Permission.bluetoothScan.request(); + + if (!status.isGranted) { + log('Izin bluetoothConnect: $status', name: _logName); + } + + return status.isGranted; + } catch (e) { + log('Gagal memeriksa izin Bluetooth', name: _logName, error: e); + // Perangkat di bawah Android 12 tidak mengenal izin ini — jangan + // menghalangi cetak hanya karena pemeriksaannya gagal. + return true; + } + } + + /// Menutup socket Bluetooth apa pun keadaannya, lalu menunggu link lepas. + /// + /// `disconnect` dipanggil walau `connectionStatus` bilang tidak tersambung: + /// socket yang setengah terbuka sering melaporkan `false` padahal masih + /// memblokir connect berikutnya. + Future _forceDisconnect() async { + try { + await PrintBluetoothThermal.disconnect; + } catch (e) { + log('Disconnect gagal, diabaikan', name: _logName, error: e); + } + + _connectedMac = null; + await _waitUntilDisconnected(); + } + + /// Teardown RFCOMM di Android berjalan asinkron: `disconnect` selesai + /// sebelum link benar-benar lepas. Menunggu kondisinya (bukan durasi tetap) + /// supaya perangkat yang cepat tidak ikut dihukum jeda panjang. + Future _waitUntilDisconnected() async { + final deadline = DateTime.now().add(_kDisconnectTimeout); + + while (DateTime.now().isBefore(deadline)) { + if (!await PrintBluetoothThermal.connectionStatus) return; + await Future.delayed(const Duration(milliseconds: 100)); + } + + log( + 'Bluetooth masih tersambung setelah ${_kDisconnectTimeout.inMilliseconds}ms', + name: _logName, + ); + } + + Future> _printToNetworkPrinter( + Printer printer, + List printData, + ) async { + try { + final printResult = await _printNetwork(printer.address, printData); + + if (!printResult) { + FirebaseCrashlytics.instance.recordError( + 'Failed to print to Network Printer', + null, + reason: 'Failed to print to Network Printer', + information: [ + 'function: _printNetwork(printer.address, printData)', + 'Printer: ${printer.name}', + 'ipAddress: ${printer.address}', + 'bytes: ${printData.length}', + 'in: $_logName', + ], + ); + // Dulu ini `right(false)`, yang lolos sebagai sukses ke pemanggil — + // kasir melihat "berhasil dicetak" padahal tidak ada kertas keluar. + return left( + PrinterFailure.dynamicErrorMessage( + 'Tidak bisa mencetak ke ${printer.name} (${printer.address})', + ), + ); + } + + return right(true); + } catch (e, stackTrace) { + FirebaseCrashlytics.instance.recordError( + e, + stackTrace, + reason: 'Error printing ${printer.name}', + information: ['Printer: ${printer.name}', 'IP: ${printer.address}'], + ); + log('Error printing network', name: _logName, error: e); + return left( + const PrinterFailure.dynamicErrorMessage('Error printing struct'), + ); + } + } Future _printNetwork(String ipAddress, List printData) async { try { @@ -399,711 +530,347 @@ class PrinterRepository implements IPrinterRepository { port: 9100, timeout: const Duration(seconds: 15), ); - PosPrintResult connect = await printer.connect(); - if (connect == PosPrintResult.success) { - PosPrintResult printing = await printer.printTicket(printData); - printer.disconnect(); - - if (printing == PosPrintResult.success) { - log("Successfully printed via Network printer: $ipAddress"); - return true; - } else { - FirebaseCrashlytics.instance.recordError( - 'Failed to print via Network printer: ${printing.msg}', - null, - reason: 'Failed to print via Network printer', - information: [ - 'function: printNetwork(String ipAddress, List printData)', - 'Printer: Network printer', - 'ipAddress: $ipAddress', - 'printData: $printData', - ], - ); - log("Failed to print via Network printer: ${printing.msg}"); - return false; - } - } else { - FirebaseCrashlytics.instance.recordError( - 'Failed to connect to Network printer: ${connect.msg}', - null, - reason: 'Failed to connectNetwork printer', - information: [ - 'function: printNetwork(String ipAddress, List printData)', - 'Printer: Network printer', - 'ipAddress: $ipAddress', - 'printData: $printData', - ], - ); - log("Failed to connect to Network printer: ${connect.msg}"); + final PosPrintResult connect = await printer.connect(); + if (connect != PosPrintResult.success) { + log('Failed to connect to Network printer: ${connect.msg}'); return false; } + + final PosPrintResult printing = await printer.printTicket(printData); + printer.disconnect(); + + if (printing != PosPrintResult.success) { + log('Failed to print via Network printer: ${printing.msg}'); + return false; + } + + log('Successfully printed via Network printer: $ipAddress'); + return true; } catch (e, stackTrace) { FirebaseCrashlytics.instance.recordError( e, stackTrace, reason: 'Error printing via Network', information: [ - 'function: printNetwork(String ipAddress, List printData)', - 'Printer: Network printer', + 'function: _printNetwork(String ipAddress, List printData)', 'ipAddress: $ipAddress', - 'printData: $printData', ], ); - log("Error printing via Network: $e"); + log('Error printing via Network: $e'); return false; } } - @override - Future> printStruckOrder({ - required Order order, - }) async { - return await _globalPrintLock.synchronized(() async { - try { - final outlet = await _outletLocalDatasource.currentOutlet(); - final user = await _authLocalDataProvider.currentUser(); + Future _writeBytesChunked(List data) async { + for (int i = 0; i < data.length; i += _kChunkSize) { + final end = i + _kChunkSize > data.length ? data.length : i + _kChunkSize; - // Struck for customer - if (order.totalPaid > 0) { - final result = await _printReceipt( - order: order, - outlet: outlet, - cashieName: user.name, - ); - if (result.isLeft()) return result; // Jika gagal, return error - } - - // Struck for cashier - final cashierResult = await _printCashier( - order: order, - outlet: outlet, - cashieName: user.name, - ); - if (cashierResult.isLeft()) return cashierResult; - - // Struck for checker - final checkerResult = await _printChecker( - order: order, - outlet: outlet, - cashieName: user.name, - ); - if (checkerResult.isLeft()) return checkerResult; - - // Struck for bar if exist product bar - final itemsKitchen = order.orderItems.where( - (item) => item.printerType == 'kitchen', - ); - - if (itemsKitchen.isNotEmpty) { - final kitchenResult = await _printKitchen( - order: order, - outlet: outlet, - cashieName: user.name, - ); - if (kitchenResult.isLeft()) return kitchenResult; - } - - // Struck for bar if exist product bar - final itemsBar = order.orderItems.where( - (item) => item.printerType == 'bar', - ); - - if (itemsBar.isNotEmpty) { - final barResult = await _printBar( - order: order, - outlet: outlet, - cashieName: user.name, - ); - if (barResult.isLeft()) return barResult; - } - - return right(unit); - } catch (e, stackTrace) { - FirebaseCrashlytics.instance.recordError( - 'Print Struck Order Error: $e', - stackTrace, - reason: - 'Print struck order error / Printer not setting in printer page', - information: ['Order ID: ${order.id}'], - ); - log("Error printing struck order", name: _logName, error: e); - return left( - const PrinterFailure.dynamicErrorMessage('Error printing order'), - ); - } - }); // end _globalPrintLock.synchronized - } - - @override - Future> printStruckSaveOrder({ - required Order order, - }) async { - return await _globalPrintLock.synchronized(() async { - try { - final outlet = await _outletLocalDatasource.currentOutlet(); - final user = await _authLocalDataProvider.currentUser(); - - // Struck for cashier - final cashierResult = await _printCashier( - order: order, - outlet: outlet, - cashieName: user.name, - ); - if (cashierResult.isLeft()) return cashierResult; - - // Struck for checker - final checkerResult = await _printChecker( - order: order, - outlet: outlet, - cashieName: user.name, - ); - if (checkerResult.isLeft()) return checkerResult; - - // Struck for kitchen - // Struck for bar if exist product bar - final itemsKitchen = order.orderItems.where( - (item) => item.printerType == 'kitchen', - ); - - if (itemsKitchen.isNotEmpty) { - final kitchenResult = await _printKitchen( - order: order, - outlet: outlet, - cashieName: user.name, - ); - if (kitchenResult.isLeft()) return kitchenResult; - } - - // Struck for bar if exist product bar - final itemsBar = order.orderItems.where( - (item) => item.printerType == 'bar', - ); - - if (itemsBar.isNotEmpty) { - final barResult = await _printBar( - order: order, - outlet: outlet, - cashieName: user.name, - ); - if (barResult.isLeft()) return barResult; - } - - return right(unit); - } catch (e, stackTrace) { - FirebaseCrashlytics.instance.recordError( - 'Print Struck Save Order Error: $e', - stackTrace, - reason: - 'Print struck save order error / Printer not setting in printer page', - information: ['Order ID: ${order.id}'], - ); - log("Error printing struck save order", name: _logName, error: e); - return left( - const PrinterFailure.dynamicErrorMessage('Error printing save order'), - ); - } - }); // end _globalPrintLock.synchronized - } - - @override - Future> printStruckCashier({ - required Order order, - }) async { - return await _globalPrintLock.synchronized(() async { - final outlet = await _outletLocalDatasource.currentOutlet(); - final user = await _authLocalDataProvider.currentUser(); - return _printCashier(order: order, outlet: outlet, cashieName: user.name); - }); - } - - @override - Future> printStruckVoid({ - required Order order, - }) async { - return await _globalPrintLock.synchronized(() async { - final outlet = await _outletLocalDatasource.currentOutlet(); - final user = await _authLocalDataProvider.currentUser(); - return _printVoid(order: order, outlet: outlet, cashieName: user.name); - }); - } - - @override - Future> printStruckSplitBill({ - required Order order, - }) async { - return await _globalPrintLock.synchronized(() async { - final outlet = await _outletLocalDatasource.currentOutlet(); - final user = await _authLocalDataProvider.currentUser(); - return _printSplitBill( - order: order, - outlet: outlet, - cashieName: user.name, + final result = await PrintBluetoothThermal.writeBytes( + data.sublist(i, end), ); - }); - } - - @override - Future> printStruckPayment({ - required Order order, - }) async { - return await _globalPrintLock.synchronized(() async { - final outlet = await _outletLocalDatasource.currentOutlet(); - final user = await _authLocalDataProvider.currentUser(); - return _printReceipt(order: order, outlet: outlet, cashieName: user.name); - }); - } - - Future> _printReceipt({ - required Order order, - required Outlet outlet, - required String cashieName, - }) async { - log('Starting to print receipt', name: _logName); - final receiptPrinter = await _localDataProvider.findPrinterByCode( - 'receipt', - ); - if (receiptPrinter.hasData) { - try { - final printer = receiptPrinter.data!.toDomain(); - - final printValue = await PrintUi().printOrder( - order: order, - outlet: outlet, - cashierName: cashieName, - paper: printer.paper.toIntegerFromText, - ); - - final printResult = await printStruct(printer, printValue); - if (printResult.isLeft()) return printResult.map((_) => unit); - - log('Finished printed receipt', name: _logName); - log('Printer Size: ', name: printer.paper); - - return right(unit); - } catch (e, stackTrace) { - FirebaseCrashlytics.instance.recordError( - 'Print Struck Receipt Error: $e', - stackTrace, - reason: - 'Print struck receipt error / Printer not setting in printer page', - information: ['Order ID: ${order.id}'], - ); - log("Error printing receipt", name: _logName, error: e); - return left( - const PrinterFailure.dynamicErrorMessage( - 'Error printing receipt order', - ), - ); - } - } else { - log('Receipt printer not found', name: _logName); - FirebaseCrashlytics.instance.recordError( - 'Receipt printer not found', - null, - reason: - 'Receipt printer not found / Printer not setting in printer page', - information: ['Order ID: ${order.id}'], - ); - return left( - const PrinterFailure.dynamicErrorMessage( - 'Receipt printer belum terhubung, Silahkan hubungkan printer di pengaturan.', - ), - ); - } - } - - Future> _printCashier({ - required Order order, - required Outlet outlet, - required String cashieName, - }) async { - log('Starting to print cashier', name: _logName); - final receiptPrinter = await _localDataProvider.findPrinterByCode( - 'receipt', - ); - if (receiptPrinter.hasData) { - try { - final printer = receiptPrinter.data!.toDomain(); - - final printValue = await PrintUi().printCashier( - order: order, - outlet: outlet, - cashierName: cashieName, - paper: printer.paper.toIntegerFromText, - ); - - final printResult = await printStruct(printer, printValue); - if (printResult.isLeft()) return printResult.map((_) => unit); - - log('Finished printing cashier', name: _logName); - - return right(unit); - } catch (e, stackTrace) { - FirebaseCrashlytics.instance.recordError( - 'Print Struck Cashier Error: $e', - stackTrace, - reason: - 'Print struck cashier error / Printer not setting in printer page', - information: ['Order ID: ${order.id}'], - ); - log("Error printing cashier", name: _logName, error: e); - return left( - const PrinterFailure.dynamicErrorMessage( - 'Error printing cashier order', - ), - ); - } - } else { - FirebaseCrashlytics.instance.recordError( - 'Cashier printer not found', - null, - reason: - 'Cashier printer not found / Printer not setting in printer page', - information: ['Order ID: ${order.id}'], - ); - return left( - const PrinterFailure.dynamicErrorMessage( - 'Receipt printer belum terhubung, Silahkan hubungkan printer di pengaturan.', - ), - ); - } - } - - Future> _printKitchen({ - required Order order, - required Outlet outlet, - required String cashieName, - }) async { - log('Starting to print kitchen', name: _logName); - final kitchenPrinter = await _localDataProvider.findPrinterByCode( - 'kitchen', - ); - if (kitchenPrinter.hasData) { - try { - final productKitchen = order.orderItems - .where((item) => item.printerType == 'kitchen') - .toList(); - - if (productKitchen.isNotEmpty) { - final printer = kitchenPrinter.data!.toDomain(); - - final printValue = await PrintUi().printKitchen( - order: order.copyWith(orderItems: productKitchen), - outlet: outlet, - cashierName: cashieName, - paper: printer.paper.toIntegerFromText, - ); - - final printResult = await printStruct(printer, printValue); - if (printResult.isLeft()) return printResult.map((_) => unit); - } else { - log('Product with printer type bar not found', name: _logName); - } - - log('Finished printed kitchen', name: _logName); - - return right(unit); - } catch (e, stackTrace) { - FirebaseCrashlytics.instance.recordError( - 'Print Struck Kitchen Error: $e', - stackTrace, - reason: - 'Print struck kitchen error / Printer not setting in printer page', - information: ['Order ID: ${order.id}'], - ); - log("Error printing kitchen", name: _logName, error: e); - return left( - const PrinterFailure.dynamicErrorMessage( - 'Error printing kitchen order', - ), - ); - } - } else { - FirebaseCrashlytics.instance.recordError( - 'Kitchen printer not found', - null, - reason: - 'Kitchen printer not found / Printer not setting in printer page', - information: ['Order ID: ${order.id}'], - ); - return left( - const PrinterFailure.dynamicErrorMessage( - 'Kitchen printer belum terhubung, Silahkan hubungkan printer di pengaturan.', - ), - ); - } - } - - Future> _printChecker({ - required Order order, - required Outlet outlet, - required String cashieName, - }) async { - log('Starting to print checker', name: _logName); - final checkerPrinter = await _localDataProvider.findPrinterByCode( - 'checker', - ); - if (checkerPrinter.hasData) { - try { - final printer = checkerPrinter.data!.toDomain(); - - final printValue = await PrintUi().printChecker( - order: order, - outlet: outlet, - cashierName: cashieName, - paper: printer.paper.toIntegerFromText, - ); - - final printResult = await printStruct(printer, printValue); - if (printResult.isLeft()) return printResult.map((_) => unit); - - log('Finished printed checker', name: _logName); - - return right(unit); - } catch (e, stackTrace) { - FirebaseCrashlytics.instance.recordError( - 'Print Struck Checker Error: $e', - stackTrace, - reason: - 'Print struck checker error / Printer not setting in printer page', - information: ['Order ID: ${order.id}'], - ); - log("Error printing checker", name: _logName, error: e); - return left( - const PrinterFailure.dynamicErrorMessage( - 'Error printing checker order', - ), - ); - } - } else { - log('Checker printer not found', name: _logName); - FirebaseCrashlytics.instance.recordError( - 'Checker printer not found', - null, - reason: - 'Checker printer not found / Printer not setting in printer page', - information: ['Order ID: ${order.id}'], - ); - return left( - const PrinterFailure.dynamicErrorMessage( - 'Checker printer belum terhubung, Silahkan hubungkan printer di pengaturan.', - ), - ); - } - } - - Future> _printBar({ - required Order order, - required Outlet outlet, - required String cashieName, - }) async { - final barPrinter = await _localDataProvider.findPrinterByCode('bar'); - if (barPrinter.hasData) { - log('Starting to print bar', name: _logName); - try { - final productBar = order.orderItems - .where((item) => item.printerType == 'bar') - .toList(); - - if (productBar.isNotEmpty) { - final printer = barPrinter.data!.toDomain(); - - final printValue = await PrintUi().printBar( - order: order.copyWith(orderItems: productBar), - outlet: outlet, - cashierName: cashieName, - paper: printer.paper.toIntegerFromText, - ); - - final printResult = await printStruct(printer, printValue); - if (printResult.isLeft()) return printResult.map((_) => unit); - - log('Finished printed bar', name: _logName); - } else { - log('Product with printer type bar not found', name: _logName); - return right(unit); - } - - return right(unit); - } catch (e, stackTrace) { - FirebaseCrashlytics.instance.recordError( - 'Print Struck Bar Error: $e', - stackTrace, - reason: - 'Print struck bar error / Printer not setting in printer page', - information: ['Order ID: ${order.id}'], - ); - log("Error printing bar", name: _logName, error: e); - return left( - const PrinterFailure.dynamicErrorMessage('Error printing bar order'), - ); - } - } else { - FirebaseCrashlytics.instance.recordError( - 'Bar printer not found', - null, - reason: 'Bar printer not found / Printer not setting in printer page', - information: ['Order ID: ${order.id}'], - ); - return left( - const PrinterFailure.dynamicErrorMessage( - 'Bar printer belum terhubung, Silahkan hubungkan printer di pengaturan.', - ), - ); - } - } - - Future> _printVoid({ - required Order order, - required Outlet outlet, - required String cashieName, - }) async { - log('Starting to print void', name: _logName); - final voidPrinter = await _localDataProvider.findPrinterByCode('receipt'); - if (voidPrinter.hasData) { - try { - final printer = voidPrinter.data!.toDomain(); - - final printValue = await PrintUi().printVoid( - order: order, - outlet: outlet, - cashierName: cashieName, - paper: printer.paper.toIntegerFromText, - ); - - final printResult = await printStruct(printer, printValue); - if (printResult.isLeft()) return printResult.map((_) => unit); - - log('Finished printed void', name: _logName); - - return right(unit); - } catch (e, stackTrace) { - FirebaseCrashlytics.instance.recordError( - 'Print Struck Void Error: $e', - stackTrace, - reason: - 'Print struck void error / Printer not setting in printer page', - information: ['Order ID: ${order.id}'], - ); - log("Error printing void", name: _logName, error: e); - return left( - const PrinterFailure.dynamicErrorMessage('Error printing void order'), - ); - } - } else { - FirebaseCrashlytics.instance.recordError( - 'Void printer not found', - null, - reason: 'Void printer not found / Printer not setting in printer page', - information: ['Order ID: ${order.id}'], - ); - return left( - const PrinterFailure.dynamicErrorMessage( - 'Receipt printer belum terhubung, Silahkan hubungkan printer di pengaturan.', - ), - ); - } - } - - Future> _printSplitBill({ - required Order order, - required Outlet outlet, - required String cashieName, - }) async { - log('Starting to print splitBill', name: _logName); - final splitBillPrinter = await _localDataProvider.findPrinterByCode( - 'receipt', - ); - if (splitBillPrinter.hasData) { - try { - final printer = splitBillPrinter.data!.toDomain(); - - final printValue = await PrintUi().printSplit( - order: order, - outlet: outlet, - cashierName: cashieName, - paper: printer.paper.toIntegerFromText, - ); - - final printResult = await printStruct(printer, printValue); - if (printResult.isLeft()) return printResult.map((_) => unit); - - log('Finished printed splitBill', name: _logName); - - return right(unit); - } catch (e, stackTrace) { - FirebaseCrashlytics.instance.recordError( - 'Print Struck SplitBill Error: $e', - stackTrace, - reason: - 'Print struck splitBill error / Printer not setting in printer page', - information: ['Order ID: ${order.id}'], - ); - log("Error printing splitBill", name: _logName, error: e); - return left( - const PrinterFailure.dynamicErrorMessage( - 'Error printing splitBill order', - ), - ); - } - } else { - FirebaseCrashlytics.instance.recordError( - 'SplitBill printer not found', - null, - reason: - 'SplitBill printer not found / Printer not setting in printer page', - information: ['Order ID: ${order.id}'], - ); - return left( - const PrinterFailure.dynamicErrorMessage( - 'Receipt printer belum terhubung, Silahkan hubungkan printer di pengaturan.', - ), - ); - } - } - - Future _writeBytesChunked(List data, {int chunkSize = 512}) async { - for (int i = 0; i < data.length; i += chunkSize) { - final end = (i + chunkSize > data.length) ? data.length : i + chunkSize; - final chunk = data.sublist(i, end); - bool result = await PrintBluetoothThermal.writeBytes(chunk); if (!result) return false; - await Future.delayed(const Duration(milliseconds: 100)); + + if (end < data.length) await Future.delayed(_kChunkDelay); } return true; } - @override - Future> printStruckBar({ - required Order order, - }) async { - return await _globalPrintLock.synchronized(() async { - final outlet = await _outletLocalDatasource.currentOutlet(); - final user = await _authLocalDataProvider.currentUser(); - return _printBar(order: order, outlet: outlet, cashieName: user.name); - }); - } + // =========================================================================== + // Orkestrasi cetak + // =========================================================================== @override - Future> printStruckChecker({ + Stream> printStations({ required Order order, - }) async { - return await _globalPrintLock.synchronized(() async { - final outlet = await _outletLocalDatasource.currentOutlet(); - final user = await _authLocalDataProvider.currentUser(); - return _printChecker(order: order, outlet: outlet, cashieName: user.name); - }); + required List stations, + }) { + final controller = StreamController>(); + controller.onListen = () => _runPrintJob(order, stations, controller); + return controller.stream; } - @override - Future> printStruckKitchen({ - required Order order, - }) async { - return await _globalPrintLock.synchronized(() async { - final outlet = await _outletLocalDatasource.currentOutlet(); - final user = await _authLocalDataProvider.currentUser(); - return _printKitchen(order: order, outlet: outlet, cashieName: user.name); - }); + Future _runPrintJob( + Order order, + List stations, + StreamController> controller, + ) async { + final results = { + for (final station in stations) + station: PrintStationResult.pending(station), + }; + + void publish() { + if (controller.isClosed) return; + controller.add([for (final station in stations) results[station]!]); + } + + void mark(PrintStation station, PrintStationResult result) { + results[station] = result; + publish(); + } + + publish(); + + try { + // Job diserialkan supaya dua kali cetak tidak saling menyelipkan tiket. + await _jobLock.synchronized(() async { + final outlet = await _outletLocalDatasource.currentOutlet(); + final user = await _authLocalDataProvider.currentUser(); + + final plans = <_StationPlan>[]; + + // Printer diresolusi di depan supaya kita tahu station mana yang lewat + // Bluetooth dan mana yang lewat jaringan sebelum mulai mencetak. + for (final station in stations) { + final items = _itemsFor(order, station); + + if (items.isEmpty) { + mark( + station, + results[station]!.copyWith( + status: PrintStationStatus.skipped, + message: 'Tidak ada item untuk station ini', + ), + ); + continue; + } + + final record = await _localDataProvider.findPrinterByCode( + station.printerCode, + ); + + if (!record.hasData) { + FirebaseCrashlytics.instance.recordError( + '${station.printerCode} printer not found', + null, + reason: 'Printer belum diatur di halaman pengaturan', + information: [ + 'Order ID: ${order.id}', + 'Station: ${station.name}', + ], + ); + mark( + station, + results[station]!.copyWith( + status: PrintStationStatus.failed, + message: + 'Printer ${station.label} belum diatur di halaman pengaturan', + ), + ); + continue; + } + + plans.add( + _StationPlan( + station: station, + printer: record.data!.toDomain(), + order: order.copyWith(orderItems: items), + ), + ); + } + + Future run(_StationPlan plan) async { + final stopwatch = Stopwatch()..start(); + + mark( + plan.station, + results[plan.station]!.copyWith( + status: PrintStationStatus.printing, + ), + ); + + final result = await _printStation(plan, outlet, user.name); + stopwatch.stop(); + + mark( + plan.station, + result.fold( + (failure) => PrintStationResult( + station: plan.station, + status: PrintStationStatus.failed, + message: _failureMessage(failure), + elapsed: stopwatch.elapsed, + ), + (_) => PrintStationResult( + station: plan.station, + status: PrintStationStatus.success, + elapsed: stopwatch.elapsed, + ), + ), + ); + } + + final bluetooth = plans.where((plan) => plan.isBluetooth).toList(); + final network = plans.where((plan) => !plan.isBluetooth).toList(); + + await Future.wait([ + // Bluetooth berurutan — radionya cuma satu. + _runSequentially(_groupedByAddress(bluetooth), run), + // Jaringan paralel — tiap printer punya IP sendiri dan tidak + // berebut radio dengan Bluetooth. + ...network.map(run), + ]); + }); + } catch (e, stackTrace) { + FirebaseCrashlytics.instance.recordError( + e, + stackTrace, + reason: 'Print job error', + information: ['Order ID: ${order.id}'], + ); + log('Error running print job', name: _logName, error: e); + + for (final station in stations) { + if (results[station]!.isFinished) continue; + results[station] = results[station]!.copyWith( + status: PrintStationStatus.failed, + message: 'Terjadi kesalahan saat mencetak', + ); + } + publish(); + } finally { + await controller.close(); + } } + + Future> _printStation( + _StationPlan plan, + Outlet outlet, + String cashierName, + ) async { + try { + log('Starting to print ${plan.station.name}', name: _logName); + + final payload = await _buildPayload(plan, outlet, cashierName); + final result = await printStruct(plan.printer, payload); + + log('Finished printing ${plan.station.name}', name: _logName); + + return result.map((_) => unit); + } catch (e, stackTrace) { + FirebaseCrashlytics.instance.recordError( + e, + stackTrace, + reason: 'Print station error: ${plan.station.name}', + information: [ + 'Order ID: ${plan.order.id}', + 'Printer: ${plan.printer.name}', + ], + ); + log('Error printing ${plan.station.name}', name: _logName, error: e); + return left( + PrinterFailure.dynamicErrorMessage( + 'Gagal menyiapkan cetakan ${plan.station.label}', + ), + ); + } + } + + Future> _buildPayload( + _StationPlan plan, + Outlet outlet, + String cashierName, + ) { + final ui = PrintUi(); + final order = plan.order; + final paper = plan.printer.paper.toIntegerFromText; + + return switch (plan.station) { + PrintStation.receipt => ui.printOrder( + order: order, + outlet: outlet, + cashierName: cashierName, + paper: paper, + ), + PrintStation.cashier => ui.printCashier( + order: order, + outlet: outlet, + cashierName: cashierName, + paper: paper, + ), + PrintStation.checker => ui.printChecker( + order: order, + outlet: outlet, + cashierName: cashierName, + paper: paper, + ), + PrintStation.kitchen => ui.printKitchen( + order: order, + outlet: outlet, + cashierName: cashierName, + paper: paper, + ), + PrintStation.bar => ui.printBar( + order: order, + outlet: outlet, + cashierName: cashierName, + paper: paper, + ), + PrintStation.voided => ui.printVoid( + order: order, + outlet: outlet, + cashierName: cashierName, + paper: paper, + ), + PrintStation.splitBill => ui.printSplit( + order: order, + outlet: outlet, + cashierName: cashierName, + paper: paper, + ), + }; + } + + List _itemsFor(Order order, PrintStation station) { + final type = station.itemPrinterType; + if (type == null) return order.orderItems; + return order.orderItems.where((item) => item.printerType == type).toList(); + } + + /// Mengelompokkan station per alamat printer supaya perpindahan koneksi + /// terjadi sesedikit mungkin — tiap perpindahan berarti disconnect dan + /// connect ulang di radio yang sama. Urutan kemunculan alamat dipertahankan, + /// jadi struk pelanggan tetap keluar lebih dulu. + List<_StationPlan> _groupedByAddress(List<_StationPlan> plans) { + final grouped = >{}; + for (final plan in plans) { + grouped.putIfAbsent(plan.printer.address, () => []).add(plan); + } + return [for (final group in grouped.values) ...group]; + } + + Future _runSequentially( + List<_StationPlan> plans, + Future Function(_StationPlan) run, + ) async { + for (final plan in plans) { + await run(plan); + } + } + + String _failureMessage(PrinterFailure failure) => failure.map( + serverError: (_) => 'Terjadi kesalahan pada server', + unexpectedError: (_) => 'Terjadi kesalahan, silahkan coba lagi', + empty: (_) => 'Tidak ada data', + localStorageError: (value) => value.erroMessage, + dynamicErrorMessage: (value) => value.erroMessage, + ); +} + +class _StationPlan { + final PrintStation station; + final Printer printer; + + /// Order yang sudah difilter untuk station ini — kitchen dan bar hanya + /// memuat item miliknya. + final Order order; + + const _StationPlan({ + required this.station, + required this.printer, + required this.order, + }); + + bool get isBluetooth => printer.type == 'Bluetooth'; } diff --git a/lib/injection.config.dart b/lib/injection.config.dart index 44d3d43..47d615c 100644 --- a/lib/injection.config.dart +++ b/lib/injection.config.dart @@ -233,7 +233,7 @@ extension GetItInjectableX on _i174.GetIt { () => _i457.ApiClient(gh<_i361.Dio>(), gh<_i923.Env>()), ); gh.factory<_i923.Env>(() => _i923.ProdEnv(), registerFor: {_prod}); - gh.factory<_i104.IPrinterRepository>( + gh.lazySingleton<_i104.IPrinterRepository>( () => _i881.PrinterRepository( gh<_i149.PrinterLocalDataProvider>(), gh<_i693.OutletLocalDatasource>(), diff --git a/lib/presentation/app_widget.dart b/lib/presentation/app_widget.dart index 90e58f3..50da218 100644 --- a/lib/presentation/app_widget.dart +++ b/lib/presentation/app_widget.dart @@ -23,6 +23,7 @@ import '../application/void/void_form/void_form_bloc.dart'; import '../common/theme/theme.dart'; import '../common/constant/app_constant.dart'; import '../injection.dart'; +import 'components/dialog/dialog.dart'; import 'router/app_router.dart'; import 'router/app_router_observer.dart'; @@ -67,6 +68,20 @@ class _AppWidgetState extends State { routerConfig: _appRouter.config( navigatorObservers: () => [AppRouteObserver()], ), + // Satu listener untuk seluruh aplikasi: cetak bisa dipicu dari banyak + // halaman, dan `PrintStruckBloc` hidup di atas router jadi prosesnya + // tetap jalan walau kasir sudah pindah halaman. + builder: (context, child) => BlocListener( + listenWhen: (previous, current) => previous.jobId != current.jobId, + listener: (_, __) { + // Dialog harus dibuka dari context di bawah Navigator, sementara + // builder ini berada di atasnya. + final navigatorContext = _appRouter.navigatorKey.currentContext; + if (navigatorContext == null) return; + showPrintProgressDialog(navigatorContext); + }, + child: child ?? const SizedBox.shrink(), + ), ), ); } diff --git a/lib/presentation/components/dialog/dialog.dart b/lib/presentation/components/dialog/dialog.dart index fbad2fc..840bfdb 100644 --- a/lib/presentation/components/dialog/dialog.dart +++ b/lib/presentation/components/dialog/dialog.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -6,11 +8,13 @@ import '../../../application/checkout/checkout_form/checkout_form_bloc.dart'; import '../../../application/outlet/outlet_loader/outlet_loader_bloc.dart'; import '../../../application/printer/bluetooth/bluetooth_connect/bluetooth_connect_bloc.dart'; import '../../../application/printer/bluetooth/bluetooth_loader/bluetooth_loader_bloc.dart'; +import '../../../application/printer/print_struck/print_struck_bloc.dart'; import '../../../common/extension/extension.dart'; import '../../../common/function/app_function.dart'; import '../../../common/theme/theme.dart'; import '../../../common/types/order_type.dart'; import '../../../domain/delivery/delivery.dart'; +import '../../../domain/printer/printer.dart'; import '../../../domain/product/product.dart'; import '../button/button.dart'; import '../card/outlet_card.dart'; @@ -23,3 +27,4 @@ part 'variant_dialog.dart'; part 'delivery_dialog.dart'; part 'order_type_dialog.dart'; part 'printer_bluetooth_dialog.dart'; +part 'print_progress_dialog.dart'; diff --git a/lib/presentation/components/dialog/print_progress_dialog.dart b/lib/presentation/components/dialog/print_progress_dialog.dart new file mode 100644 index 0000000..fdd053e --- /dev/null +++ b/lib/presentation/components/dialog/print_progress_dialog.dart @@ -0,0 +1,273 @@ +part of 'dialog.dart'; + +/// Dijaga supaya satu job cetak hanya membuka satu dialog, walau listener +/// terpicu lebih dari sekali. +bool _isPrintProgressDialogOpen = false; + +Future showPrintProgressDialog(BuildContext context) async { + if (_isPrintProgressDialogOpen) return; + _isPrintProgressDialogOpen = true; + + try { + await showDialog( + context: context, + // Cetak jalan di latar belakang, kasir tidak perlu menunggui dialognya. + barrierDismissible: true, + builder: (_) => const PrintProgressDialog(), + ); + } finally { + _isPrintProgressDialogOpen = false; + } +} + +class PrintProgressDialog extends StatefulWidget { + const PrintProgressDialog({super.key}); + + @override + State createState() => _PrintProgressDialogState(); +} + +class _PrintProgressDialogState extends State { + Timer? _autoClose; + + @override + void dispose() { + _autoClose?.cancel(); + super.dispose(); + } + + /// Semua station keluar — tutup sendiri supaya antrean kasir tidak tertahan, + /// tapi beri jeda sebentar agar centang hijaunya sempat terbaca. + void _scheduleAutoClose() { + _autoClose?.cancel(); + _autoClose = Timer(const Duration(milliseconds: 1500), () { + if (mounted) Navigator.of(context).pop(); + }); + } + + @override + Widget build(BuildContext context) { + return BlocConsumer( + listenWhen: (previous, current) => + previous.isPrinting && !current.isPrinting, + listener: (context, state) { + if (state.hasFailure) return; + _scheduleAutoClose(); + }, + builder: (context, state) { + final isDone = !state.isPrinting; + + return CustomModalDialog( + title: isDone ? 'Hasil Cetak' : 'Mencetak Struk', + maxWidth: 480, + minHeight: 0, + contentPadding: const EdgeInsets.all(20), + bottom: _buildFooter(context, state), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _buildSummary(state), + const SpaceHeight(16), + for (final result in state.stations) ...[ + _StationRow(result: result), + const SpaceHeight(8), + ], + ], + ), + ); + }, + ); + } + + Widget _buildSummary(PrintStruckState state) { + final isDone = !state.isPrinting; + final total = state.plannedCount; + final success = state.successCount; + + final Color color; + final String message; + + if (!isDone) { + color = AppColor.primary; + message = 'Mengirim ke printer…'; + } else if (state.hasFailure) { + color = AppColor.error; + message = '${state.failedStations.length} station gagal dicetak'; + } else { + color = AppColor.success; + message = 'Semua struk berhasil dicetak'; + } + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + color: color.withOpacity(0.08), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: color.withOpacity(0.25)), + ), + child: Row( + children: [ + Expanded( + child: Text( + message, + style: AppStyle.md.copyWith( + color: color, + fontWeight: FontWeight.w600, + ), + ), + ), + Text( + '$success dari $total', + style: AppStyle.md.copyWith( + color: color, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ); + } + + Widget _buildFooter(BuildContext context, PrintStruckState state) { + final isDone = !state.isPrinting; + + return Container( + padding: const EdgeInsets.fromLTRB(20, 0, 20, 20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (!isDone) ...[ + Text( + 'Cetak berjalan di latar belakang. Dialog ini boleh ditutup.', + style: AppStyle.sm.copyWith(color: AppColor.textSecondary), + textAlign: TextAlign.center, + ), + const SpaceHeight(12), + ], + Row( + children: [ + Expanded( + child: AppElevatedButton.outlined( + onPressed: () => Navigator.of(context).pop(), + label: 'Tutup', + ), + ), + if (isDone && state.hasFailure) ...[ + const SpaceWidth(12), + Expanded( + flex: 2, + child: AppElevatedButton.filled( + onPressed: () => context.read().add( + const PrintStruckEvent.retryFailed(), + ), + label: 'Cetak Ulang yang Gagal', + icon: const Icon( + Icons.refresh_rounded, + color: AppColor.white, + size: 18, + ), + ), + ), + ], + ], + ), + ], + ), + ); + } +} + +class _StationRow extends StatelessWidget { + final PrintStationResult result; + + const _StationRow({required this.result}); + + @override + Widget build(BuildContext context) { + final color = _color; + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: result.isFailed ? color.withOpacity(0.4) : AppColor.border, + ), + color: result.isFailed ? color.withOpacity(0.05) : Colors.transparent, + ), + child: Row( + children: [ + SizedBox(width: 20, height: 20, child: Center(child: _icon)), + const SpaceWidth(12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + result.station.label, + style: AppStyle.md.copyWith(fontWeight: FontWeight.w600), + ), + const SpaceHeight(2), + Text( + _statusText, + style: AppStyle.sm.copyWith(color: color), + ), + ], + ), + ), + if (result.elapsed > Duration.zero) ...[ + const SpaceWidth(8), + Text( + '${(result.elapsed.inMilliseconds / 1000).toStringAsFixed(1)}s', + style: AppStyle.sm.copyWith(color: AppColor.textSecondary), + ), + ], + ], + ), + ); + } + + Widget get _icon => switch (result.status) { + PrintStationStatus.printing => const SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator(strokeWidth: 2), + ), + PrintStationStatus.pending => const Icon( + Icons.schedule_rounded, + size: 18, + color: AppColor.textSecondary, + ), + PrintStationStatus.success => const Icon( + Icons.check_circle_rounded, + size: 20, + color: AppColor.success, + ), + PrintStationStatus.failed => const Icon( + Icons.error_rounded, + size: 20, + color: AppColor.error, + ), + PrintStationStatus.skipped => const Icon( + Icons.remove_circle_outline_rounded, + size: 18, + color: AppColor.textSecondary, + ), + }; + + Color get _color => switch (result.status) { + PrintStationStatus.printing => AppColor.primary, + PrintStationStatus.success => AppColor.success, + PrintStationStatus.failed => AppColor.error, + PrintStationStatus.pending || + PrintStationStatus.skipped => AppColor.textSecondary, + }; + + String get _statusText => switch (result.status) { + PrintStationStatus.pending => 'Menunggu giliran', + PrintStationStatus.printing => 'Mengirim ke printer…', + PrintStationStatus.success => 'Berhasil dicetak', + PrintStationStatus.failed => result.message ?? 'Gagal dicetak', + PrintStationStatus.skipped => result.message ?? 'Dilewati', + }; +} diff --git a/lib/presentation/components/print/print_ui.dart b/lib/presentation/components/print/print_ui.dart index 1fe4fc1..7dc58c0 100644 --- a/lib/presentation/components/print/print_ui.dart +++ b/lib/presentation/components/print/print_ui.dart @@ -6,6 +6,27 @@ import '../../../domain/outlet/outlet.dart'; import 'receipt_component_builder.dart'; class PrintUi { + /// `CapabilityProfile.load()` membaca dan mem-parse aset tiap kali dipanggil, + /// dan sekali cetak semua station memanggilnya sampai lima kali. Hasilnya + /// sama untuk seluruh aplikasi, jadi cukup dimuat satu kali. + static Future? _cachedProfile; + + static Future _capabilityProfile() async { + final cached = _cachedProfile; + if (cached != null) return cached; + + final future = CapabilityProfile.load(); + _cachedProfile = future; + + try { + return await future; + } catch (_) { + // Jangan kunci kegagalan sesaat — biarkan cetakan berikutnya mencoba lagi. + _cachedProfile = null; + rethrow; + } + } + Future> printOrder({ required Order order, required Outlet outlet, @@ -14,7 +35,7 @@ class PrintUi { }) async { List bytes = []; - final profile = await CapabilityProfile.load(); + final profile = await _capabilityProfile(); final generator = Generator( paper == 58 ? PaperSize.mm58 : PaperSize.mm80, profile, @@ -90,7 +111,7 @@ class PrintUi { }) async { List bytes = []; - final profile = await CapabilityProfile.load(); + final profile = await _capabilityProfile(); final generator = Generator( paper == 58 ? PaperSize.mm58 : PaperSize.mm80, profile, @@ -142,7 +163,7 @@ class PrintUi { }) async { List bytes = []; - final profile = await CapabilityProfile.load(); + final profile = await _capabilityProfile(); final generator = Generator( paper == 58 ? PaperSize.mm58 : PaperSize.mm80, profile, @@ -198,7 +219,7 @@ class PrintUi { }) async { List bytes = []; - final profile = await CapabilityProfile.load(); + final profile = await _capabilityProfile(); final generator = Generator( paper == 58 ? PaperSize.mm58 : PaperSize.mm80, profile, @@ -253,7 +274,7 @@ class PrintUi { }) async { List bytes = []; - final profile = await CapabilityProfile.load(); + final profile = await _capabilityProfile(); final generator = Generator( paper == 58 ? PaperSize.mm58 : PaperSize.mm80, profile, @@ -294,7 +315,7 @@ class PrintUi { }) async { List bytes = []; - final profile = await CapabilityProfile.load(); + final profile = await _capabilityProfile(); final generator = Generator( paper == 58 ? PaperSize.mm58 : PaperSize.mm80, profile, @@ -372,7 +393,7 @@ class PrintUi { }) async { List bytes = []; - final profile = await CapabilityProfile.load(); + final profile = await _capabilityProfile(); final generator = Generator( paper == 58 ? PaperSize.mm58 : PaperSize.mm80, profile, @@ -451,7 +472,7 @@ class PrintUi { }) async { List bytes = []; - final profile = await CapabilityProfile.load(); + final profile = await _capabilityProfile(); final generator = Generator( paper == 58 ? PaperSize.mm58 : PaperSize.mm80, profile, diff --git a/lib/presentation/pages/order/order_page.dart b/lib/presentation/pages/order/order_page.dart index 513666e..227745f 100644 --- a/lib/presentation/pages/order/order_page.dart +++ b/lib/presentation/pages/order/order_page.dart @@ -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( - 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( diff --git a/lib/presentation/pages/order/pages/success_add_item_order/success_add_item_order_page.dart b/lib/presentation/pages/order/pages/success_add_item_order/success_add_item_order_page.dart index 5a63058..ab0d2fe 100644 --- a/lib/presentation/pages/order/pages/success_add_item_order/success_add_item_order_page.dart +++ b/lib/presentation/pages/order/pages/success_add_item_order/success_add_item_order_page.dart @@ -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( - 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( - 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( + 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, ), - ], - ), - ); - }, - ), + ), + ], + ), + ); + }, ), ), ); diff --git a/lib/presentation/pages/order/pages/success_order/success_order_page.dart b/lib/presentation/pages/order/pages/success_order/success_order_page.dart index 6f15efb..9b82612 100644 --- a/lib/presentation/pages/order/pages/success_order/success_order_page.dart +++ b/lib/presentation/pages/order/pages/success_order/success_order_page.dart @@ -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( - 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( - 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( + 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), + ), + ], + ), + ); + }, ), ), ); diff --git a/lib/presentation/pages/order/widgets/order_right_panel.dart b/lib/presentation/pages/order/widgets/order_right_panel.dart index 578519c..f25209c 100644 --- a/lib/presentation/pages/order/widgets/order_right_panel.dart +++ b/lib/presentation/pages/order/widgets/order_right_panel.dart @@ -62,6 +62,11 @@ class OrderRightPanel extends StatelessWidget { color: AppColor.white, onSelected: (value) { switch (value) { + case 'all': + context.read().add( + PrintStruckEvent.order(state.selectedOrder!), + ); + break; case 'checker': context.read().add( PrintStruckEvent.checker(state.selectedOrder!), @@ -79,7 +84,23 @@ class OrderRightPanel extends StatelessWidget { break; } }, - itemBuilder: (context) => [ + itemBuilder: (context) => >[ + // 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( diff --git a/lib/presentation/pages/payment/pages/payment_success/payment_success_page.dart b/lib/presentation/pages/payment/pages/payment_success/payment_success_page.dart index 9ed3c22..d62821a 100644 --- a/lib/presentation/pages/payment/pages/payment_success/payment_success_page.dart +++ b/lib/presentation/pages/payment/pages/payment_success/payment_success_page.dart @@ -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( - 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( - 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( + 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().add( - OrderLoaderEvent.getById(orderId), - ); - }, - ), - ); - }, - ), + ), + (f) => OrderLoaderErrorStateWidget( + failure: f, + onRefresh: () { + context.read().add( + OrderLoaderEvent.getById(orderId), + ); + }, + ), + ); + }, ), ), ); diff --git a/lib/presentation/pages/payment/pages/payment_success_split_bill/payment_success_split_bill_page.dart b/lib/presentation/pages/payment/pages/payment_success_split_bill/payment_success_split_bill_page.dart index 7995df4..91fd46b 100644 --- a/lib/presentation/pages/payment/pages/payment_success_split_bill/payment_success_split_bill_page.dart +++ b/lib/presentation/pages/payment/pages/payment_success_split_bill/payment_success_split_bill_page.dart @@ -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( - 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( - 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( + 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().add( - OrderLoaderEvent.getById(orderId), - ); - }, - ), - ); - }, - ), + ), + (f) => OrderLoaderErrorStateWidget( + failure: f, + onRefresh: () { + context.read().add( + OrderLoaderEvent.getById(orderId), + ); + }, + ), + ); + }, ), ), ); diff --git a/lib/presentation/pages/void/pages/void_success/void_success_page.dart b/lib/presentation/pages/void/pages/void_success/void_success_page.dart index d760e24..9249d80 100644 --- a/lib/presentation/pages/void/pages/void_success/void_success_page.dart +++ b/lib/presentation/pages/void/pages/void_success/void_success_page.dart @@ -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( - 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( - 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( + 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), + ), + ], ), - ); - }, - ), + ), + ); + }, ), ); }