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), + ), + ], ), - ); - }, - ), + ), + ); + }, ), ); }