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:
@@ -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<PrintStruckEvent, PrintStruckState> {
|
||||
Future<void> _onPrintStruckEvent(
|
||||
PrintStruckEvent event,
|
||||
Emitter<PrintStruckState> emit,
|
||||
) {
|
||||
return event.map(
|
||||
order: (e) async {
|
||||
Either<PrinterFailure, Unit> 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<PrinterFailure, Unit> failureOrSuccess;
|
||||
/// Struk pelanggan hanya ikut dicetak kalau ordernya sudah dibayar.
|
||||
List<PrintStation> _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<void> _print(
|
||||
Emitter<PrintStruckState> emit,
|
||||
Order order,
|
||||
List<PrintStation> 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<PrinterFailure, Unit> 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<PrinterFailure, Unit> 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<PrinterFailure, Unit> 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<PrinterFailure, Unit> 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<PrinterFailure, Unit> 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<PrinterFailure, Unit> 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<List<PrintStationResult>>(
|
||||
_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));
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
}
|
||||
|
||||
@@ -2,11 +2,33 @@ part of 'print_struck_bloc.dart';
|
||||
|
||||
@freezed
|
||||
class PrintStruckState with _$PrintStruckState {
|
||||
factory PrintStruckState({
|
||||
required Option<Either<PrinterFailure, Unit>> failureOrPrintStruck,
|
||||
const PrintStruckState._();
|
||||
|
||||
const factory PrintStruckState({
|
||||
/// Status seluruh station di job cetak terakhir, urut sesuai urutan cetak.
|
||||
@Default(<PrintStationResult>[]) List<PrintStationResult> 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<PrintStationResult> 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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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>(),
|
||||
|
||||
@@ -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<AppWidget> {
|
||||
routerConfig: _appRouter.config(
|
||||
navigatorObservers: () => <NavigatorObserver>[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<PrintStruckBloc, PrintStruckState>(
|
||||
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(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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<void> showPrintProgressDialog(BuildContext context) async {
|
||||
if (_isPrintProgressDialogOpen) return;
|
||||
_isPrintProgressDialogOpen = true;
|
||||
|
||||
try {
|
||||
await showDialog<void>(
|
||||
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<PrintProgressDialog> createState() => _PrintProgressDialogState();
|
||||
}
|
||||
|
||||
class _PrintProgressDialogState extends State<PrintProgressDialog> {
|
||||
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<PrintStruckBloc, PrintStruckState>(
|
||||
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<PrintStruckBloc>().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',
|
||||
};
|
||||
}
|
||||
@@ -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<CapabilityProfile>? _cachedProfile;
|
||||
|
||||
static Future<CapabilityProfile> _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<List<int>> printOrder({
|
||||
required Order order,
|
||||
required Outlet outlet,
|
||||
@@ -14,7 +35,7 @@ class PrintUi {
|
||||
}) async {
|
||||
List<int> 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<int> 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<int> 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<int> 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<int> 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<int> 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<int> 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<int> bytes = [];
|
||||
|
||||
final profile = await CapabilityProfile.load();
|
||||
final profile = await _capabilityProfile();
|
||||
final generator = Generator(
|
||||
paper == 58 ? PaperSize.mm58 : PaperSize.mm80,
|
||||
profile,
|
||||
|
||||
@@ -3,10 +3,8 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../../../application/order/order_loader/order_loader_bloc.dart';
|
||||
import '../../../application/printer/print_struck/print_struck_bloc.dart';
|
||||
import '../../../common/theme/theme.dart';
|
||||
import '../../../injection.dart';
|
||||
import '../../components/toast/flushbar.dart';
|
||||
import 'widgets/order_left_panel.dart';
|
||||
import 'widgets/order_right_panel.dart';
|
||||
|
||||
@@ -37,20 +35,7 @@ class OrderPage extends StatelessWidget implements AutoRouteWrapper {
|
||||
);
|
||||
},
|
||||
),
|
||||
BlocListener<PrintStruckBloc, PrintStruckState>(
|
||||
listenWhen: (previous, current) =>
|
||||
previous.failureOrPrintStruck != current.failureOrPrintStruck,
|
||||
listener: (context, state) {
|
||||
state.failureOrPrintStruck.fold(() {}, (either) {
|
||||
either.fold(
|
||||
(f) => AppFlushbar.showPrinterFailureToast(context, f),
|
||||
(success) {
|
||||
AppFlushbar.showSuccess(context, "Struck berhasil dicetak");
|
||||
},
|
||||
);
|
||||
});
|
||||
},
|
||||
),
|
||||
// Hasil cetak dilaporkan lewat dialog progres global di `AppWidget`.
|
||||
],
|
||||
child: SafeArea(
|
||||
child: Scaffold(
|
||||
|
||||
+25
-40
@@ -3,10 +3,8 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../../../../../application/checkout/checkout_form/checkout_form_bloc.dart';
|
||||
import '../../../../../application/printer/print_struck/print_struck_bloc.dart';
|
||||
import '../../../../../common/theme/theme.dart';
|
||||
import '../../../../components/spaces/space.dart';
|
||||
import '../../../../components/toast/flushbar.dart';
|
||||
import 'widgets/success_add_item_order_left_panel.dart';
|
||||
import 'widgets/success_add_item_order_right_panel.dart';
|
||||
|
||||
@@ -16,46 +14,33 @@ class SuccessAddItemOrderPage extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocListener<PrintStruckBloc, PrintStruckState>(
|
||||
listenWhen: (p, c) => p.failureOrPrintStruck != c.failureOrPrintStruck,
|
||||
listener: (context, state) {
|
||||
state.failureOrPrintStruck.fold(
|
||||
() {},
|
||||
(either) => either.fold(
|
||||
(f) => AppFlushbar.showPrinterFailureToast(context, f),
|
||||
(success) {
|
||||
AppFlushbar.showSuccess(context, "Struck berhasil dicetak");
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Scaffold(
|
||||
backgroundColor: AppColor.background,
|
||||
body: SafeArea(
|
||||
child: BlocBuilder<CheckoutFormBloc, CheckoutFormState>(
|
||||
builder: (context, checkoutState) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 35,
|
||||
child: SuccessAddItemOrderLeftPanel(
|
||||
checkoutState: checkoutState,
|
||||
),
|
||||
// Hasil cetak dilaporkan lewat dialog progres global di `AppWidget`.
|
||||
return Scaffold(
|
||||
backgroundColor: AppColor.background,
|
||||
body: SafeArea(
|
||||
child: BlocBuilder<CheckoutFormBloc, CheckoutFormState>(
|
||||
builder: (context, checkoutState) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 35,
|
||||
child: SuccessAddItemOrderLeftPanel(
|
||||
checkoutState: checkoutState,
|
||||
),
|
||||
const SpaceWidth(16),
|
||||
Expanded(
|
||||
flex: 65,
|
||||
child: SuccessAddItemOrderRightPanel(
|
||||
checkoutState: checkoutState,
|
||||
),
|
||||
),
|
||||
const SpaceWidth(16),
|
||||
Expanded(
|
||||
flex: 65,
|
||||
child: SuccessAddItemOrderRightPanel(
|
||||
checkoutState: checkoutState,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -3,13 +3,11 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../../../../../application/order/order_loader/order_loader_bloc.dart';
|
||||
import '../../../../../application/printer/print_struck/print_struck_bloc.dart';
|
||||
import '../../../../../common/theme/theme.dart';
|
||||
import '../../../../../domain/order/order.dart';
|
||||
import '../../../../../injection.dart';
|
||||
import '../../../../components/loader/loader_with_text.dart';
|
||||
import '../../../../components/spaces/space.dart';
|
||||
import '../../../../components/toast/flushbar.dart';
|
||||
import 'widgets/success_order_left_panel.dart';
|
||||
import 'widgets/success_order_right_panel.dart';
|
||||
|
||||
@@ -20,47 +18,33 @@ class SuccessOrderPage extends StatelessWidget implements AutoRouteWrapper {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocListener<PrintStruckBloc, PrintStruckState>(
|
||||
listenWhen: (previous, current) =>
|
||||
previous.failureOrPrintStruck != current.failureOrPrintStruck,
|
||||
listener: (context, state) {
|
||||
state.failureOrPrintStruck.fold(
|
||||
() {},
|
||||
(either) => either.fold(
|
||||
(f) => AppFlushbar.showPrinterFailureToast(context, f),
|
||||
(success) {
|
||||
AppFlushbar.showSuccess(context, "Struck berhasil dicetak");
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Scaffold(
|
||||
backgroundColor: AppColor.background,
|
||||
body: SafeArea(
|
||||
child: BlocBuilder<OrderLoaderBloc, OrderLoaderState>(
|
||||
builder: (context, state) {
|
||||
if (state.isFetchingById) {
|
||||
return const Center(child: LoaderWithText());
|
||||
}
|
||||
// Hasil cetak dilaporkan lewat dialog progres global di `AppWidget`.
|
||||
return Scaffold(
|
||||
backgroundColor: AppColor.background,
|
||||
body: SafeArea(
|
||||
child: BlocBuilder<OrderLoaderBloc, OrderLoaderState>(
|
||||
builder: (context, state) {
|
||||
if (state.isFetchingById) {
|
||||
return const Center(child: LoaderWithText());
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 35,
|
||||
child: SuccessOrderLeftPanel(order: state.order),
|
||||
),
|
||||
const SpaceWidth(16),
|
||||
Expanded(
|
||||
flex: 65,
|
||||
child: SuccessOrderRightPanel(order: state.order),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 35,
|
||||
child: SuccessOrderLeftPanel(order: state.order),
|
||||
),
|
||||
const SpaceWidth(16),
|
||||
Expanded(
|
||||
flex: 65,
|
||||
child: SuccessOrderRightPanel(order: state.order),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -62,6 +62,11 @@ class OrderRightPanel extends StatelessWidget {
|
||||
color: AppColor.white,
|
||||
onSelected: (value) {
|
||||
switch (value) {
|
||||
case 'all':
|
||||
context.read<PrintStruckBloc>().add(
|
||||
PrintStruckEvent.order(state.selectedOrder!),
|
||||
);
|
||||
break;
|
||||
case 'checker':
|
||||
context.read<PrintStruckBloc>().add(
|
||||
PrintStruckEvent.checker(state.selectedOrder!),
|
||||
@@ -79,7 +84,23 @@ class OrderRightPanel extends StatelessWidget {
|
||||
break;
|
||||
}
|
||||
},
|
||||
itemBuilder: (context) => [
|
||||
itemBuilder: (context) => <PopupMenuEntry<String>>[
|
||||
// Cetak semua station sekaligus — dipisah dari cetak per
|
||||
// station karena sifatnya berbeda.
|
||||
const PopupMenuItem(
|
||||
value: 'all',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.done_all_rounded, color: AppColor.primary),
|
||||
SizedBox(width: 8),
|
||||
Text(
|
||||
'Print All',
|
||||
style: TextStyle(fontWeight: FontWeight.w600),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const PopupMenuDivider(),
|
||||
const PopupMenuItem(
|
||||
value: 'checker',
|
||||
child: Row(
|
||||
|
||||
@@ -3,12 +3,10 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../../../../../application/order/order_loader/order_loader_bloc.dart';
|
||||
import '../../../../../application/printer/print_struck/print_struck_bloc.dart';
|
||||
import '../../../../../common/theme/theme.dart';
|
||||
import '../../../../../injection.dart';
|
||||
import '../../../../components/error/order_loader_error_state_widget.dart';
|
||||
import '../../../../components/loader/loader_with_text.dart';
|
||||
import '../../../../components/toast/flushbar.dart';
|
||||
import 'widgets/payment_success_left_panel.dart';
|
||||
import 'widgets/payment_success_right_panel.dart';
|
||||
|
||||
@@ -19,58 +17,46 @@ class PaymentSuccessPage extends StatelessWidget implements AutoRouteWrapper {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocListener<PrintStruckBloc, PrintStruckState>(
|
||||
listenWhen: (previous, current) =>
|
||||
previous.failureOrPrintStruck != current.failureOrPrintStruck,
|
||||
listener: (context, state) {
|
||||
state.failureOrPrintStruck.fold(() {}, (either) {
|
||||
either.fold((f) => AppFlushbar.showPrinterFailureToast(context, f), (
|
||||
success,
|
||||
) {
|
||||
AppFlushbar.showSuccess(context, "Struck berhasil dicetak");
|
||||
});
|
||||
});
|
||||
},
|
||||
child: Scaffold(
|
||||
backgroundColor: AppColor.background,
|
||||
body: SafeArea(
|
||||
child: BlocBuilder<OrderLoaderBloc, OrderLoaderState>(
|
||||
builder: (context, state) {
|
||||
if (state.isFetchingById) {
|
||||
return const Center(child: LoaderWithText());
|
||||
}
|
||||
return state.failureOptionGetById.fold(
|
||||
() => Container(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Row(
|
||||
children: [
|
||||
// Left Panel - Success Message & Order Info
|
||||
Expanded(
|
||||
flex: 35,
|
||||
child: PaymentSuccessLeftPanel(order: state.order),
|
||||
),
|
||||
// Hasil cetak dilaporkan lewat dialog progres global di `AppWidget`.
|
||||
return Scaffold(
|
||||
backgroundColor: AppColor.background,
|
||||
body: SafeArea(
|
||||
child: BlocBuilder<OrderLoaderBloc, OrderLoaderState>(
|
||||
builder: (context, state) {
|
||||
if (state.isFetchingById) {
|
||||
return const Center(child: LoaderWithText());
|
||||
}
|
||||
return state.failureOptionGetById.fold(
|
||||
() => Container(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Row(
|
||||
children: [
|
||||
// Left Panel - Success Message & Order Info
|
||||
Expanded(
|
||||
flex: 35,
|
||||
child: PaymentSuccessLeftPanel(order: state.order),
|
||||
),
|
||||
|
||||
const SizedBox(width: 16),
|
||||
const SizedBox(width: 16),
|
||||
|
||||
// Right Panel - Order Details
|
||||
Expanded(
|
||||
flex: 65,
|
||||
child: PaymentSuccessRightPanel(order: state.order),
|
||||
),
|
||||
],
|
||||
),
|
||||
// Right Panel - Order Details
|
||||
Expanded(
|
||||
flex: 65,
|
||||
child: PaymentSuccessRightPanel(order: state.order),
|
||||
),
|
||||
],
|
||||
),
|
||||
(f) => OrderLoaderErrorStateWidget(
|
||||
failure: f,
|
||||
onRefresh: () {
|
||||
context.read<OrderLoaderBloc>().add(
|
||||
OrderLoaderEvent.getById(orderId),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
(f) => OrderLoaderErrorStateWidget(
|
||||
failure: f,
|
||||
onRefresh: () {
|
||||
context.read<OrderLoaderBloc>().add(
|
||||
OrderLoaderEvent.getById(orderId),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
+39
-55
@@ -3,13 +3,11 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../../../../../application/order/order_loader/order_loader_bloc.dart';
|
||||
import '../../../../../application/printer/print_struck/print_struck_bloc.dart';
|
||||
import '../../../../../common/theme/theme.dart';
|
||||
import '../../../../../domain/order/order.dart';
|
||||
import '../../../../../injection.dart';
|
||||
import '../../../../components/error/order_loader_error_state_widget.dart';
|
||||
import '../../../../components/loader/loader_with_text.dart';
|
||||
import '../../../../components/toast/flushbar.dart';
|
||||
import 'widgets/payment_success_split_bill_left_panel.dart';
|
||||
import 'widgets/payment_success_split_bill_right_panel.dart';
|
||||
|
||||
@@ -26,64 +24,50 @@ class PaymentSuccessSplitBillPage extends StatelessWidget
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocListener<PrintStruckBloc, PrintStruckState>(
|
||||
listenWhen: (previous, current) =>
|
||||
previous.failureOrPrintStruck != current.failureOrPrintStruck,
|
||||
listener: (context, state) {
|
||||
state.failureOrPrintStruck.fold(
|
||||
() {},
|
||||
(either) => either.fold(
|
||||
(f) => AppFlushbar.showPrinterFailureToast(context, f),
|
||||
(success) {
|
||||
AppFlushbar.showSuccess(context, "Struck berhasil dicetak");
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Scaffold(
|
||||
backgroundColor: AppColor.background,
|
||||
body: SafeArea(
|
||||
child: BlocBuilder<OrderLoaderBloc, OrderLoaderState>(
|
||||
builder: (context, state) {
|
||||
if (state.isFetchingById) {
|
||||
return const Center(child: LoaderWithText());
|
||||
}
|
||||
return state.failureOptionGetById.fold(
|
||||
() => Container(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Row(
|
||||
children: [
|
||||
// Left Panel - Success Message & Order Info
|
||||
Expanded(
|
||||
flex: 35,
|
||||
child: PaymentSuccessSplitBillLeftPanel(
|
||||
order: state.order.copyWith(orderItems: orderItems),
|
||||
),
|
||||
// Hasil cetak dilaporkan lewat dialog progres global di `AppWidget`.
|
||||
return Scaffold(
|
||||
backgroundColor: AppColor.background,
|
||||
body: SafeArea(
|
||||
child: BlocBuilder<OrderLoaderBloc, OrderLoaderState>(
|
||||
builder: (context, state) {
|
||||
if (state.isFetchingById) {
|
||||
return const Center(child: LoaderWithText());
|
||||
}
|
||||
return state.failureOptionGetById.fold(
|
||||
() => Container(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Row(
|
||||
children: [
|
||||
// Left Panel - Success Message & Order Info
|
||||
Expanded(
|
||||
flex: 35,
|
||||
child: PaymentSuccessSplitBillLeftPanel(
|
||||
order: state.order.copyWith(orderItems: orderItems),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(width: 16),
|
||||
const SizedBox(width: 16),
|
||||
|
||||
// Right Panel - Order Details
|
||||
Expanded(
|
||||
flex: 65,
|
||||
child: PaymentSuccessSplitBillRightPanel(
|
||||
order: state.order.copyWith(orderItems: orderItems),
|
||||
),
|
||||
// Right Panel - Order Details
|
||||
Expanded(
|
||||
flex: 65,
|
||||
child: PaymentSuccessSplitBillRightPanel(
|
||||
order: state.order.copyWith(orderItems: orderItems),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
(f) => OrderLoaderErrorStateWidget(
|
||||
failure: f,
|
||||
onRefresh: () {
|
||||
context.read<OrderLoaderBloc>().add(
|
||||
OrderLoaderEvent.getById(orderId),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
(f) => OrderLoaderErrorStateWidget(
|
||||
failure: f,
|
||||
onRefresh: () {
|
||||
context.read<OrderLoaderBloc>().add(
|
||||
OrderLoaderEvent.getById(orderId),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -2,11 +2,9 @@ import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../../../../../application/printer/print_struck/print_struck_bloc.dart';
|
||||
import '../../../../../application/void/void_form/void_form_bloc.dart';
|
||||
import '../../../../../common/theme/theme.dart';
|
||||
import '../../../../components/spaces/space.dart';
|
||||
import '../../../../components/toast/flushbar.dart';
|
||||
import 'widgets/void_success_left_panel.dart';
|
||||
import 'widgets/void_success_right_panel.dart';
|
||||
|
||||
@@ -16,44 +14,30 @@ class VoidSuccessPage extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocListener<PrintStruckBloc, PrintStruckState>(
|
||||
listenWhen: (previous, current) =>
|
||||
previous.failureOrPrintStruck != current.failureOrPrintStruck,
|
||||
listener: (context, state) {
|
||||
state.failureOrPrintStruck.fold(
|
||||
() {},
|
||||
(either) => either.fold(
|
||||
(f) => AppFlushbar.showPrinterFailureToast(context, f),
|
||||
(success) {
|
||||
AppFlushbar.showSuccess(context, "Struck berhasil dicetak");
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Scaffold(
|
||||
backgroundColor: AppColor.background,
|
||||
body: BlocBuilder<VoidFormBloc, VoidFormState>(
|
||||
builder: (context, state) {
|
||||
return SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 35,
|
||||
child: VoidSuccessLeftPanel(state: state),
|
||||
),
|
||||
const SpaceWidth(16),
|
||||
Expanded(
|
||||
flex: 65,
|
||||
child: VoidSuccessRightPanel(state: state),
|
||||
),
|
||||
],
|
||||
),
|
||||
// Hasil cetak dilaporkan lewat dialog progres global di `AppWidget`.
|
||||
return Scaffold(
|
||||
backgroundColor: AppColor.background,
|
||||
body: BlocBuilder<VoidFormBloc, VoidFormState>(
|
||||
builder: (context, state) {
|
||||
return SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 35,
|
||||
child: VoidSuccessLeftPanel(state: state),
|
||||
),
|
||||
const SpaceWidth(16),
|
||||
Expanded(
|
||||
flex: 65,
|
||||
child: VoidSuccessRightPanel(state: state),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user