From 93560b85cb41723da3d961def8f63ddebe1f642a Mon Sep 17 00:00:00 2001 From: efrilm Date: Fri, 7 Nov 2025 16:29:26 +0700 Subject: [PATCH] print order --- .../print_struck/print_struck_bloc.dart | 60 ++ .../print_struck_bloc.freezed.dart | 572 ++++++++++++++++++ .../print_struck/print_struck_event.dart | 7 + .../print_struck/print_struck_state.dart | 12 + lib/domain/printer/printer.dart | 3 +- .../repositories/i_printer_repository.dart | 5 + .../repositories/printer_repository.dart | 369 ++++++++++- lib/injection.config.dart | 15 +- lib/presentation/app_widget.dart | 2 + .../components/print/print_ui.dart | 70 ++- .../success_order/success_order_page.dart | 67 +- .../widgets/success_order_left_panel.dart | 21 +- 12 files changed, 1154 insertions(+), 49 deletions(-) create mode 100644 lib/application/printer/print_struck/print_struck_bloc.dart create mode 100644 lib/application/printer/print_struck/print_struck_bloc.freezed.dart create mode 100644 lib/application/printer/print_struck/print_struck_event.dart create mode 100644 lib/application/printer/print_struck/print_struck_state.dart diff --git a/lib/application/printer/print_struck/print_struck_bloc.dart b/lib/application/printer/print_struck/print_struck_bloc.dart new file mode 100644 index 0000000..026f085 --- /dev/null +++ b/lib/application/printer/print_struck/print_struck_bloc.dart @@ -0,0 +1,60 @@ +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; + +import '../../../domain/order/order.dart'; +import '../../../domain/printer/printer.dart'; + +part 'print_struck_event.dart'; +part 'print_struck_state.dart'; +part 'print_struck_bloc.freezed.dart'; + +@injectable +class PrintStruckBloc extends Bloc { + final IPrinterRepository _printerRepository; + + PrintStruckBloc(this._printerRepository) : super(PrintStruckState.initial()) { + on(_onPrintStruckEvent); + } + + Future _onPrintStruckEvent( + PrintStruckEvent event, + Emitter emit, + ) { + return event.map( + order: (e) async { + Either failureOrSuccess; + + emit(state.copyWith(isPrinting: true, failureOrPrintStruck: none())); + + failureOrSuccess = await _printerRepository.printStruckOrder( + order: e.order, + ); + + emit( + state.copyWith( + isPrinting: false, + failureOrPrintStruck: optionOf(failureOrSuccess), + ), + ); + }, + saveOrder: (e) async { + Either failureOrSuccess; + + emit(state.copyWith(isPrinting: true, failureOrPrintStruck: none())); + + failureOrSuccess = await _printerRepository.printStruckSaveOrder( + order: e.order, + ); + + emit( + state.copyWith( + isPrinting: false, + failureOrPrintStruck: optionOf(failureOrSuccess), + ), + ); + }, + ); + } +} diff --git a/lib/application/printer/print_struck/print_struck_bloc.freezed.dart b/lib/application/printer/print_struck/print_struck_bloc.freezed.dart new file mode 100644 index 0000000..814aabf --- /dev/null +++ b/lib/application/printer/print_struck/print_struck_bloc.freezed.dart @@ -0,0 +1,572 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'print_struck_bloc.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models', +); + +/// @nodoc +mixin _$PrintStruckEvent { + Order get order => throw _privateConstructorUsedError; + @optionalTypeArgs + TResult when({ + required TResult Function(Order order) order, + required TResult Function(Order order) saveOrder, + }) => throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(Order order)? order, + TResult? Function(Order order)? saveOrder, + }) => throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(Order order)? order, + TResult Function(Order order)? saveOrder, + required TResult orElse(), + }) => throw _privateConstructorUsedError; + @optionalTypeArgs + TResult map({ + required TResult Function(_Order value) order, + required TResult Function(_SaveOrder value) saveOrder, + }) => throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_Order value)? order, + TResult? Function(_SaveOrder value)? saveOrder, + }) => throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_Order value)? order, + TResult Function(_SaveOrder value)? saveOrder, + 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 +abstract class $PrintStruckEventCopyWith<$Res> { + factory $PrintStruckEventCopyWith( + PrintStruckEvent value, + $Res Function(PrintStruckEvent) then, + ) = _$PrintStruckEventCopyWithImpl<$Res, PrintStruckEvent>; + @useResult + $Res call({Order order}); + + $OrderCopyWith<$Res> get order; +} + +/// @nodoc +class _$PrintStruckEventCopyWithImpl<$Res, $Val extends PrintStruckEvent> + implements $PrintStruckEventCopyWith<$Res> { + _$PrintStruckEventCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// 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> { + factory _$$OrderImplCopyWith( + _$OrderImpl value, + $Res Function(_$OrderImpl) then, + ) = __$$OrderImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({Order order}); + + @override + $OrderCopyWith<$Res> get order; +} + +/// @nodoc +class __$$OrderImplCopyWithImpl<$Res> + extends _$PrintStruckEventCopyWithImpl<$Res, _$OrderImpl> + implements _$$OrderImplCopyWith<$Res> { + __$$OrderImplCopyWithImpl( + _$OrderImpl _value, + $Res Function(_$OrderImpl) _then, + ) : super(_value, _then); + + /// 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( + _$OrderImpl( + null == order + ? _value.order + : order // ignore: cast_nullable_to_non_nullable + as Order, + ), + ); + } +} + +/// @nodoc + +class _$OrderImpl implements _Order { + const _$OrderImpl(this.order); + + @override + final Order order; + + @override + String toString() { + return 'PrintStruckEvent.order(order: $order)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$OrderImpl && + (identical(other.order, order) || other.order == order)); + } + + @override + int get hashCode => Object.hash(runtimeType, order); + + /// Create a copy of PrintStruckEvent + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$OrderImplCopyWith<_$OrderImpl> get copyWith => + __$$OrderImplCopyWithImpl<_$OrderImpl>(this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function(Order order) order, + required TResult Function(Order order) saveOrder, + }) { + return order(this.order); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(Order order)? order, + TResult? Function(Order order)? saveOrder, + }) { + return order?.call(this.order); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(Order order)? order, + TResult Function(Order order)? saveOrder, + required TResult orElse(), + }) { + if (order != null) { + return order(this.order); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_Order value) order, + required TResult Function(_SaveOrder value) saveOrder, + }) { + return order(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_Order value)? order, + TResult? Function(_SaveOrder value)? saveOrder, + }) { + return order?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_Order value)? order, + TResult Function(_SaveOrder value)? saveOrder, + required TResult orElse(), + }) { + if (order != null) { + return order(this); + } + return orElse(); + } +} + +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 _$$SaveOrderImplCopyWith<$Res> + implements $PrintStruckEventCopyWith<$Res> { + factory _$$SaveOrderImplCopyWith( + _$SaveOrderImpl value, + $Res Function(_$SaveOrderImpl) then, + ) = __$$SaveOrderImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({Order order}); + + @override + $OrderCopyWith<$Res> get order; +} + +/// @nodoc +class __$$SaveOrderImplCopyWithImpl<$Res> + extends _$PrintStruckEventCopyWithImpl<$Res, _$SaveOrderImpl> + implements _$$SaveOrderImplCopyWith<$Res> { + __$$SaveOrderImplCopyWithImpl( + _$SaveOrderImpl _value, + $Res Function(_$SaveOrderImpl) _then, + ) : super(_value, _then); + + /// 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( + _$SaveOrderImpl( + null == order + ? _value.order + : order // ignore: cast_nullable_to_non_nullable + as Order, + ), + ); + } +} + +/// @nodoc + +class _$SaveOrderImpl implements _SaveOrder { + const _$SaveOrderImpl(this.order); + + @override + final Order order; + + @override + String toString() { + return 'PrintStruckEvent.saveOrder(order: $order)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$SaveOrderImpl && + (identical(other.order, order) || other.order == order)); + } + + @override + int get hashCode => Object.hash(runtimeType, order); + + /// Create a copy of PrintStruckEvent + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$SaveOrderImplCopyWith<_$SaveOrderImpl> get copyWith => + __$$SaveOrderImplCopyWithImpl<_$SaveOrderImpl>(this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function(Order order) order, + required TResult Function(Order order) saveOrder, + }) { + return saveOrder(this.order); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(Order order)? order, + TResult? Function(Order order)? saveOrder, + }) { + return saveOrder?.call(this.order); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(Order order)? order, + TResult Function(Order order)? saveOrder, + required TResult orElse(), + }) { + if (saveOrder != null) { + return saveOrder(this.order); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_Order value) order, + required TResult Function(_SaveOrder value) saveOrder, + }) { + return saveOrder(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_Order value)? order, + TResult? Function(_SaveOrder value)? saveOrder, + }) { + return saveOrder?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_Order value)? order, + TResult Function(_SaveOrder value)? saveOrder, + required TResult orElse(), + }) { + if (saveOrder != null) { + return saveOrder(this); + } + return orElse(); + } +} + +abstract class _SaveOrder implements PrintStruckEvent { + const factory _SaveOrder(final Order order) = _$SaveOrderImpl; + + @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) + _$$SaveOrderImplCopyWith<_$SaveOrderImpl> get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +mixin _$PrintStruckState { + Option> get failureOrPrintStruck => + throw _privateConstructorUsedError; + bool get isPrinting => throw _privateConstructorUsedError; + + /// Create a copy of PrintStruckState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $PrintStruckStateCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $PrintStruckStateCopyWith<$Res> { + factory $PrintStruckStateCopyWith( + PrintStruckState value, + $Res Function(PrintStruckState) then, + ) = _$PrintStruckStateCopyWithImpl<$Res, PrintStruckState>; + @useResult + $Res call({ + Option> failureOrPrintStruck, + bool isPrinting, + }); +} + +/// @nodoc +class _$PrintStruckStateCopyWithImpl<$Res, $Val extends PrintStruckState> + implements $PrintStruckStateCopyWith<$Res> { + _$PrintStruckStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of 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}) { + return _then( + _value.copyWith( + failureOrPrintStruck: null == failureOrPrintStruck + ? _value.failureOrPrintStruck + : failureOrPrintStruck // ignore: cast_nullable_to_non_nullable + as Option>, + isPrinting: null == isPrinting + ? _value.isPrinting + : isPrinting // ignore: cast_nullable_to_non_nullable + as bool, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$PrintStruckStateImplCopyWith<$Res> + implements $PrintStruckStateCopyWith<$Res> { + factory _$$PrintStruckStateImplCopyWith( + _$PrintStruckStateImpl value, + $Res Function(_$PrintStruckStateImpl) then, + ) = __$$PrintStruckStateImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + Option> failureOrPrintStruck, + bool isPrinting, + }); +} + +/// @nodoc +class __$$PrintStruckStateImplCopyWithImpl<$Res> + extends _$PrintStruckStateCopyWithImpl<$Res, _$PrintStruckStateImpl> + implements _$$PrintStruckStateImplCopyWith<$Res> { + __$$PrintStruckStateImplCopyWithImpl( + _$PrintStruckStateImpl _value, + $Res Function(_$PrintStruckStateImpl) _then, + ) : super(_value, _then); + + /// Create a copy of 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}) { + return _then( + _$PrintStruckStateImpl( + failureOrPrintStruck: null == failureOrPrintStruck + ? _value.failureOrPrintStruck + : failureOrPrintStruck // ignore: cast_nullable_to_non_nullable + as Option>, + isPrinting: null == isPrinting + ? _value.isPrinting + : isPrinting // ignore: cast_nullable_to_non_nullable + as bool, + ), + ); + } +} + +/// @nodoc + +class _$PrintStruckStateImpl implements _PrintStruckState { + _$PrintStruckStateImpl({ + required this.failureOrPrintStruck, + this.isPrinting = false, + }); + + @override + final Option> failureOrPrintStruck; + @override + @JsonKey() + final bool isPrinting; + + @override + String toString() { + return 'PrintStruckState(failureOrPrintStruck: $failureOrPrintStruck, isPrinting: $isPrinting)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$PrintStruckStateImpl && + (identical(other.failureOrPrintStruck, failureOrPrintStruck) || + other.failureOrPrintStruck == failureOrPrintStruck) && + (identical(other.isPrinting, isPrinting) || + other.isPrinting == isPrinting)); + } + + @override + int get hashCode => + Object.hash(runtimeType, failureOrPrintStruck, isPrinting); + + /// Create a copy of PrintStruckState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$PrintStruckStateImplCopyWith<_$PrintStruckStateImpl> get copyWith => + __$$PrintStruckStateImplCopyWithImpl<_$PrintStruckStateImpl>( + this, + _$identity, + ); +} + +abstract class _PrintStruckState implements PrintStruckState { + factory _PrintStruckState({ + required final Option> failureOrPrintStruck, + final bool isPrinting, + }) = _$PrintStruckStateImpl; + + @override + Option> get failureOrPrintStruck; + @override + bool get isPrinting; + + /// Create a copy of PrintStruckState + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$PrintStruckStateImplCopyWith<_$PrintStruckStateImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/application/printer/print_struck/print_struck_event.dart b/lib/application/printer/print_struck/print_struck_event.dart new file mode 100644 index 0000000..af6f4a0 --- /dev/null +++ b/lib/application/printer/print_struck/print_struck_event.dart @@ -0,0 +1,7 @@ +part of 'print_struck_bloc.dart'; + +@freezed +class PrintStruckEvent with _$PrintStruckEvent { + const factory PrintStruckEvent.order(Order order) = _Order; + const factory PrintStruckEvent.saveOrder(Order order) = _SaveOrder; +} diff --git a/lib/application/printer/print_struck/print_struck_state.dart b/lib/application/printer/print_struck/print_struck_state.dart new file mode 100644 index 0000000..2aeec9c --- /dev/null +++ b/lib/application/printer/print_struck/print_struck_state.dart @@ -0,0 +1,12 @@ +part of 'print_struck_bloc.dart'; + +@freezed +class PrintStruckState with _$PrintStruckState { + factory PrintStruckState({ + required Option> failureOrPrintStruck, + @Default(false) bool isPrinting, + }) = _PrintStruckState; + + factory PrintStruckState.initial() => + PrintStruckState(failureOrPrintStruck: none()); +} diff --git a/lib/domain/printer/printer.dart b/lib/domain/printer/printer.dart index dc20b0f..2d1fc04 100644 --- a/lib/domain/printer/printer.dart +++ b/lib/domain/printer/printer.dart @@ -1,9 +1,10 @@ -import 'package:dartz/dartz.dart'; +import 'package:dartz/dartz.dart' hide Order; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:print_bluetooth_thermal/print_bluetooth_thermal.dart'; import '../../common/api/api_failure.dart'; import '../../common/function/app_function.dart'; +import '../order/order.dart'; part 'printer.freezed.dart'; diff --git a/lib/domain/printer/repositories/i_printer_repository.dart b/lib/domain/printer/repositories/i_printer_repository.dart index 7f5b016..79d5715 100644 --- a/lib/domain/printer/repositories/i_printer_repository.dart +++ b/lib/domain/printer/repositories/i_printer_repository.dart @@ -14,4 +14,9 @@ abstract class IPrinterRepository { Printer printer, List printValue, ); + + Future> printStruckOrder({required Order order}); + Future> printStruckSaveOrder({ + required Order order, + }); } diff --git a/lib/infrastructure/printer/repositories/printer_repository.dart b/lib/infrastructure/printer/repositories/printer_repository.dart index 01f9b36..5c953a7 100644 --- a/lib/infrastructure/printer/repositories/printer_repository.dart +++ b/lib/infrastructure/printer/repositories/printer_repository.dart @@ -1,20 +1,32 @@ import 'dart:developer'; -import 'package:dartz/dartz.dart'; +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'; +import 'package:injectable/injectable.dart' hide Order; import 'package:print_bluetooth_thermal/print_bluetooth_thermal.dart'; +import '../../../domain/order/order.dart'; +import '../../../domain/outlet/outlet.dart'; import '../../../domain/printer/printer.dart'; +import '../../../presentation/components/print/print_ui.dart'; +import '../../auth/datasources/local_data_provider.dart'; +import '../../outlet/datasources/local_data_provider.dart'; import '../datasource/local_data_provider.dart'; import '../printer_dtos.dart'; @Injectable(as: IPrinterRepository) class PrinterRepository implements IPrinterRepository { final PrinterLocalDataProvider _localDataProvider; + final OutletLocalDatasource _outletLocalDatasource; + final AuthLocalDataProvider _authLocalDataProvider; final _logName = 'PrinterRepository'; - PrinterRepository(this._localDataProvider); + + PrinterRepository( + this._localDataProvider, + this._outletLocalDatasource, + this._authLocalDataProvider, + ); @override Future> connectBluetooth( @@ -385,4 +397,355 @@ class PrinterRepository implements IPrinterRepository { return false; } } + + @override + Future> printStruckOrder({ + required Order order, + }) async { + try { + final outlet = await _outletLocalDatasource.currentOutlet(); + final user = await _authLocalDataProvider.currentUser(); + + // Struck for customer + if (order.totalPaid > 0) { + _printReceipt(order: order, outlet: outlet, cashieName: user.name); + } + // Struck for cashier + _printCashier(order: order, outlet: outlet, cashieName: user.name); + // Struck for kitchen + _printKitchen(order: order, outlet: outlet, cashieName: user.name); + // Struck for checker + _printChecker(order: order, outlet: outlet, cashieName: user.name); + // Struck for bar if exist product bar + final itemsBar = order.orderItems.where( + (item) => item.printerType == 'bar', + ); + + if (itemsBar.isNotEmpty) { + _printBar(order: order, outlet: outlet, cashieName: user.name); + } + + 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'), + ); + } + } + + @override + Future> printStruckSaveOrder({ + required Order order, + }) async { + try { + final outlet = await _outletLocalDatasource.currentOutlet(); + final user = await _authLocalDataProvider.currentUser(); + + // Struck for cashier + _printCashier(order: order, outlet: outlet, cashieName: user.name); + // Struck for kitchen + _printKitchen(order: order, outlet: outlet, cashieName: user.name); + // Struck for checker + _printChecker(order: order, outlet: outlet, cashieName: user.name); + // Struck for bar if exist product bar + final itemsBar = order.orderItems.where( + (item) => item.printerType == 'bar', + ); + + if (itemsBar.isNotEmpty) { + _printBar(order: order, outlet: outlet, cashieName: user.name); + } + + 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'), + ); + } + } + + 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, + ); + + await printStruct(printer, printValue); + + log('Finished printed receipt', name: _logName); + + 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 { + 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 not found'), + ); + } + } + + 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, + ); + + await printStruct(printer, printValue); + + 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('Cashier printer not found'), + ); + } + } + + 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 printer = kitchenPrinter.data!.toDomain(); + + final printValue = await PrintUi().printKitchen( + order: order, + outlet: outlet, + cashierName: cashieName, + ); + + await printStruct(printer, printValue); + + 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 not found'), + ); + } + } + + 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, + ); + + await printStruct(printer, printValue); + + 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 { + 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 not found'), + ); + } + } + + 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, + outlet: outlet, + cashierName: cashieName, + ); + + await printStruct(printer, printValue); + + 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 not found'), + ); + } + } } diff --git a/lib/injection.config.dart b/lib/injection.config.dart index b7c200b..64feb5a 100644 --- a/lib/injection.config.dart +++ b/lib/injection.config.dart @@ -46,6 +46,8 @@ import 'package:apskel_pos_flutter_v2/application/printer/bluetooth/bluetooth_co as _i489; import 'package:apskel_pos_flutter_v2/application/printer/bluetooth/bluetooth_loader/bluetooth_loader_bloc.dart' as _i903; +import 'package:apskel_pos_flutter_v2/application/printer/print_struck/print_struck_bloc.dart' + as _i21; import 'package:apskel_pos_flutter_v2/application/printer/printer_bloc.dart' as _i96; import 'package:apskel_pos_flutter_v2/application/printer/printer_form/printer_form_bloc.dart' @@ -195,10 +197,14 @@ extension GetItInjectableX on _i174.GetIt { gh.lazySingleton<_i457.ApiClient>( () => _i457.ApiClient(gh<_i361.Dio>(), gh<_i923.Env>()), ); - gh.factory<_i104.IPrinterRepository>( - () => _i881.PrinterRepository(gh<_i149.PrinterLocalDataProvider>()), - ); gh.factory<_i923.Env>(() => _i923.ProdEnv(), registerFor: {_prod}); + gh.factory<_i104.IPrinterRepository>( + () => _i881.PrinterRepository( + gh<_i149.PrinterLocalDataProvider>(), + gh<_i693.OutletLocalDatasource>(), + gh<_i204.AuthLocalDataProvider>(), + ), + ); gh.factory<_i903.BluetoothLoaderBloc>( () => _i903.BluetoothLoaderBloc(gh<_i104.IPrinterRepository>()), ); @@ -208,6 +214,9 @@ extension GetItInjectableX on _i174.GetIt { gh.factory<_i1028.PrinterLoaderBloc>( () => _i1028.PrinterLoaderBloc(gh<_i104.IPrinterRepository>()), ); + gh.factory<_i21.PrintStruckBloc>( + () => _i21.PrintStruckBloc(gh<_i104.IPrinterRepository>()), + ); gh.factory<_i360.OrderRemoteDataProvider>( () => _i360.OrderRemoteDataProvider(gh<_i457.ApiClient>()), ); diff --git a/lib/presentation/app_widget.dart b/lib/presentation/app_widget.dart index 60a1659..7f562ee 100644 --- a/lib/presentation/app_widget.dart +++ b/lib/presentation/app_widget.dart @@ -11,6 +11,7 @@ import '../application/outlet/outlet_loader/outlet_loader_bloc.dart'; import '../application/payment_method/payment_method_loader/payment_method_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 '../application/printer/printer_form/printer_form_bloc.dart'; import '../application/printer/printer_loader/printer_loader_bloc.dart'; import '../application/product/product_loader/product_loader_bloc.dart'; @@ -53,6 +54,7 @@ class _AppWidgetState extends State { BlocProvider(create: (context) => getIt()), BlocProvider(create: (context) => getIt()), BlocProvider(create: (context) => getIt()), + BlocProvider(create: (context) => getIt()), ], child: MaterialApp.router( debugShowCheckedModeBanner: false, diff --git a/lib/presentation/components/print/print_ui.dart b/lib/presentation/components/print/print_ui.dart index b2823a5..1bba6ab 100644 --- a/lib/presentation/components/print/print_ui.dart +++ b/lib/presentation/components/print/print_ui.dart @@ -105,7 +105,9 @@ class PrintUi { orderNumber: order.orderNumber, customerName: order.metadata['customer_name'] ?? 'John Doe', cashierName: cashierName, - paymentMethod: order.payments.last.paymentMethodName, + paymentMethod: order.payments.isEmpty + ? '' + : order.payments.last.paymentMethodName, tableNumber: order.tableNumber, ); @@ -280,4 +282,70 @@ class PrintUi { return bytes; } + + Future> printCashier({ + required Order order, + required Outlet outlet, + required String cashierName, + int paper = 58, + }) async { + List bytes = []; + + final profile = await CapabilityProfile.load(); + final generator = Generator( + paper == 58 ? PaperSize.mm58 : PaperSize.mm80, + profile, + ); + final builder = ReceiptComponentBuilder( + generator: generator, + paperSize: 58, + ); + + bytes += generator.reset(); + + bytes += builder.header( + outletName: outlet.name, + address: outlet.address, + phoneNumber: outlet.phoneNumber, + ); + + bytes += builder.dateTime(DateTime.now()); + + bytes += builder.orderInfo( + orderNumber: order.orderNumber, + customerName: order.metadata['customer_name'] ?? 'John Doe', + cashierName: cashierName, + paymentMethod: order.payments.isEmpty + ? null + : order.payments.last.paymentMethodName, + tableNumber: order.tableNumber, + ); + + bytes += builder.orderType(order.orderType); + + bytes += builder.emptyLines(1); + + for (final item in order.orderItems) { + bytes += builder.orderItem( + productName: item.productName, + quantity: item.quantity, + unitPrice: item.unitPrice.currencyFormatRpV2, + totalPrice: item.totalPrice.currencyFormatRpV2, + variantName: item.productVariantName, + notes: item.notes, + ); + } + + bytes += builder.summary( + totalItems: order.orderItems.length, + subtotal: order.subtotal.currencyFormatRpV2, + discount: order.discountAmount.currencyFormatRpV2, + total: order.totalAmount.currencyFormatRpV2, + paid: order.totalPaid.currencyFormatRpV2, + ); + + bytes += builder.footer(message: 'Kasir'); + + return bytes; + } } 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 417e9fa..b6cc5eb 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,11 +3,13 @@ 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'; @@ -18,32 +20,47 @@ class SuccessOrderPage extends StatelessWidget implements AutoRouteWrapper { @override Widget build(BuildContext context) { - return Scaffold( - backgroundColor: AppColor.background, - body: SafeArea( - child: BlocBuilder( - builder: (context, state) { - if (state.isFetchingById) { - return const Center(child: LoaderWithText()); - } + 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 Padding( - padding: const EdgeInsets.all(24.0), - child: Row( - children: [ - Expanded( - flex: 35, - child: SuccessOrderLeftPanel(order: state.order), - ), - 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), + ), + SpaceWidth(16), + Expanded( + flex: 65, + child: SuccessOrderRightPanel(order: state.order), + ), + ], + ), + ); + }, + ), ), ), ); diff --git a/lib/presentation/pages/order/pages/success_order/widgets/success_order_left_panel.dart b/lib/presentation/pages/order/pages/success_order/widgets/success_order_left_panel.dart index 7084501..7111e65 100644 --- a/lib/presentation/pages/order/pages/success_order/widgets/success_order_left_panel.dart +++ b/lib/presentation/pages/order/pages/success_order/widgets/success_order_left_panel.dart @@ -1,6 +1,8 @@ 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 '../../../../../../common/extension/extension.dart'; import '../../../../../../common/theme/theme.dart'; import '../../../../../../domain/order/order.dart'; @@ -169,22 +171,9 @@ class SuccessOrderLeftPanel extends StatelessWidget { Expanded( child: AppElevatedButton.filled( onPressed: () { - // onPrintRecipt( - // context, - // order: widget.order, - // paymentMethod: widget.paymentMethod, - // nominalBayar: widget.paymentMethod == "Cash" - // ? widget.nominalBayar - // : widget.order.totalAmount ?? 0, - // kembalian: widget.nominalBayar - - // (widget.order.totalAmount ?? 0), - // productQuantity: widget.productQuantity, - // ); - // onPrint( - // context, - // productQuantity: widget.productQuantity, - // order: widget.order, - // ); + context.read().add( + PrintStruckEvent.order(order), + ); }, label: 'Cetak Struk', icon: Icon(Icons.print_rounded, color: AppColor.white),