fix: cetak semua station tanpa saling membatalkan

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

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

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

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

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

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
efrilm
2026-09-02 20:44:14 +07:00
co-authored by Claude Opus 5
parent 038f4d65fe
commit 1b4caf45d5
21 changed files with 1963 additions and 1403 deletions
@@ -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;
}
+1
View File
@@ -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';
+210
View File
@@ -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<PrintStationResult> 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
@@ -15,25 +15,17 @@ abstract class IPrinterRepository {
List<int> printValue,
);
Future<Either<PrinterFailure, Unit>> printStruckOrder({required Order order});
Future<Either<PrinterFailure, Unit>> printStruckSaveOrder({
required Order order,
});
Future<Either<PrinterFailure, Unit>> printStruckCashier({
required Order order,
});
Future<Either<PrinterFailure, Unit>> printStruckPayment({
required Order order,
});
Future<Either<PrinterFailure, Unit>> printStruckChecker({
required Order order,
});
Future<Either<PrinterFailure, Unit>> printStruckKitchen({
required Order order,
});
Future<Either<PrinterFailure, Unit>> printStruckBar({required Order order});
Future<Either<PrinterFailure, Unit>> printStruckVoid({required Order order});
Future<Either<PrinterFailure, Unit>> 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<List<PrintStationResult>> printStations({
required Order order,
required List<PrintStation> stations,
});
}