Compare commits

...
8 Commits
Author SHA1 Message Date
efrilm 9777f63426 chore: naikkan build number ke 1.0.12+19 2026-09-05 14:06:35 +07:00
efrilmandClaude Opus 5 7986164a17 fix: beri tiap tab pengaturan printer bloc sendiri
PrinterFormBloc dan PrinterLoaderBloc di-provide sekali secara global, sementara lima tab pengaturan printer hidup bersamaan di dalam TabBarView dan semuanya membaca-menulis state yang sama.

Karena created dan updated membangun entitas Printer sepenuhnya dari state itu (type, address, code, name, paper), tab yang terakhir menulis state akan menang, lalu tombol Simpan di tab mana pun menyimpan milik tab lain. Nilai awal state kebetulan type 'Bluetooth' — itu sebabnya printer kitchen tersimpan dengan alamat IP yang benar tapi tipe Bluetooth, dan cetakannya dikirim ke radio Bluetooth untuk mencari perangkat yang tidak pernah ada.

Provider global dihapus karena tidak ada tempat lain yang memakainya. Catatan: ini mencegah kerusakan baru, tidak memulihkan baris yang sudah terlanjur salah di SQLite — konfigurasi kelima tab perlu disimpan ulang.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-05 14:06:34 +07:00
efrilmandClaude Opus 5 32f64a197a fix: pulihkan koneksi Bluetooth dan deteksi kegagalan printer jaringan
Bluetooth: percobaan ulang connect sebelumnya tidak menutup socket lebih dulu, padahal 'read failed, socket might closed or timeout' justru berarti socket RFCOMM masih setengah terbuka — ketiga percobaan gagal identik dan retry-nya sia-sia. Tiap percobaan ulang kini didahului disconnect paksa.

connectBluetooth mengembalikan sukses begitu ADA koneksi tanpa memeriksa apakah itu printer yang diminta, dan tidak pernah mencatat MAC yang aktif. Kini memakai jalur yang sama dengan mencetak.

Izin runtime Bluetooth sebelumnya hanya diminta dari dialog pilih perangkat. Karena reinstall mencabut izin, kasir yang tidak pernah membuka dialog itu gagal cetak tanpa penjelasan. Status adapter, izin, dan pairing kini ikut di log, dan printer yang tidak ter-pairing digagalkan langsung dengan pesan yang menyebut tindakannya.

Jaringan: printTicket hanya menaruh byte di socket lalu menutup koneksi, tidak pernah membaca balik, sehingga kertas habis atau cover terbuka terlihat sama dengan sukses — itu sebabnya kegagalan kitchen tidak pernah muncul di Crashlytics. NetworkPrinterClient baru menanyakan kondisi printer lewat DLE EOT sebelum dan sesudah mencetak, dengan degradasi anggun untuk printer yang tidak mendukungnya.

Panggilan disconnect() ganda dihapus; printTicket sudah flush dan close sendiri, dan panggilan kedua tanpa await membuat error-nya tercatat sebagai crash fatal palsu. Ditambah jejak per station ke Crashlytics.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-05 14:06:22 +07:00
efrilmandClaude Opus 5 cb7139fc3e docs: tambahkan langkah PATH untuk fvm di bagian Instalasi
dart pub global activate memasang fvm ke folder bin pub yang belum tentu ada di PATH, dan setiap orang yang setup dari nol akan menabraknya tepat setelah langkah activate. Sebelumnya hanya disebut di Troubleshooting.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-02 20:46:47 +07:00
efrilm 951e154623 chore: base URL dev, exclude platform dir dari analyzer, dan naikkan versi ke 1.0.12+18
Perubahan ini sudah ada di working tree sebelum pekerjaan print dimulai; dikumpulkan di sini supaya tidak tercampur dengan commit perbaikan printer. Ditambah pembersihan import injectable yang tidak terpakai di main.dart.
2026-09-02 20:45:13 +07:00
efrilmandClaude Opus 5 ebcddca4bb docs: perbarui README untuk FVM, build_runner, dan cara run/build
Bagian setup lama tidak cuma kurang, tapi salah: diklaim environment dipilih otomatis dari debug/release, padahal datang dari --dart-define=ENV. Siapa pun yang mengikutinya akan menghasilkan APK 'production' yang menunjuk database dev. Tabel environment dan contoh kodenya juga sudah tidak sesuai isi lib/env.dart.

Ditambah: prasyarat FVM, langkah instalasi ber-fvm, perintah build per environment, bagian Code Generation (termasuk peringatan --delete-conflicting-outputs yang menghapus seluruh file generated sebelum build), dan Troubleshooting.

URL base sengaja tidak disalin ke README; cukup menunjuk lib/env.dart, karena menyalin nilai itulah yang membuat dokumen ini basi.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-02 20:44:24 +07:00
efrilmandClaude Opus 5 1b4caf45d5 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>
2026-09-02 20:44:14 +07:00
efrilmandClaude Opus 5 038f4d65fe chore: pin Flutter 3.41.9 lewat FVM
Stack codegen project ini (freezed 2.x, build_runner 2.5, analyzer 7.7.1) hanya paham bahasa Dart sampai 3.9. Flutter 3.44+ membawa framework yang ditulis di bahasa 3.10+, dan analyzer 7.7.1 crash saat men-serialize-nya (visitDotShorthandPropertyAccess) sehingga build_runner tidak bisa jalan sama sekali.

3.41.9 adalah rilis terbaru yang framework-nya masih di bahasa 3.9. Versi dipin per-project lewat .fvmrc supaya tidak mengganggu project lain di mesin yang sama.

android/build/ ikut di-ignore; isinya artefak Gradle.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-02 20:43:49 +07:00
32 changed files with 2458 additions and 1456 deletions
+3
View File
@@ -0,0 +1,3 @@
{
"flutter": "3.41.9"
}
+6
View File
@@ -32,6 +32,7 @@ migrate_working_dir/
.pub-cache/
.pub/
/build/
android/build/
# Symbolication related
app.*.symbols
@@ -43,3 +44,8 @@ app.*.map.json
/android/app/debug
/android/app/profile
/android/app/release
# FVM - versi Flutter dipin di .fvmrc (ikut di-commit), SDK-nya tidak
# FVM Version Cache
.fvm/
+3
View File
@@ -0,0 +1,3 @@
{
"dart.flutterSdkPath": ".fvm/versions/3.41.9"
}
+133 -19
View File
@@ -25,6 +25,7 @@ Aplikasi Point of Sale (POS) berbasis Flutter untuk manajemen kasir restoran. Me
| Kategori | Library |
|---|---|
| Version Manager | `fvm` — Flutter dipin di `.fvmrc` |
| State Management | `flutter_bloc` + `bloc` |
| Dependency Injection | `get_it` + `injectable` |
| Navigation | `auto_route` |
@@ -78,11 +79,13 @@ feature/
### Prasyarat
- Flutter SDK `^3.8.1`
- Dart SDK `^3.8.1`
- **FVM** — project ini mengunci versi Flutter, jangan pakai Flutter global
- **Flutter 3.41.9** (Dart 3.11.5) — dipin di `.fvmrc`
- Android SDK / Xcode (untuk iOS)
- Firebase project yang sudah dikonfigurasi
> **Kenapa versinya dikunci?** Stack code generation project ini (`freezed` 2.x, `build_runner` 2.5, `analyzer` 7.7.1) hanya paham bahasa Dart sampai 3.9. Flutter 3.44 ke atas membawa framework yang ditulis di bahasa 3.10+, dan `analyzer` 7.7.1 crash saat membacanya. Selama stack codegen belum dinaikkan, **Flutter 3.41.9 adalah versi terbaru yang masih bisa menjalankan `build_runner`.** Lihat [Troubleshooting](#troubleshooting).
### Instalasi
```bash
@@ -90,44 +93,118 @@ feature/
git clone <repo-url>
cd apskel-pos-flutter-v2
# Install dependencies
flutter pub get
# Pasang FVM (sekali per mesin)
dart pub global activate fvm
# Generate kode (freezed, injectable, auto_route, json_serializable)
dart run build_runner build --delete-conflicting-outputs
# Ambil & pakai versi Flutter yang dipin di .fvmrc
fvm install
fvm use
```
`dart pub global activate` memasang `fvm` ke folder bin milik pub yang **belum tentu ada di PATH**. Kalau muncul `fvm: command not found` / `The term 'fvm' is not recognized`, tambahkan foldernya sekali saja:
```powershell
# Windows PowerShell — permanen, buka terminal baru setelahnya
[Environment]::SetEnvironmentVariable('Path', [Environment]::GetEnvironmentVariable('Path','User') + ";$env:LOCALAPPDATA\Pub\Cache\bin", 'User')
# Untuk terminal yang sedang terbuka
$env:Path += ";$env:LOCALAPPDATA\Pub\Cache\bin"
```
```bash
# macOS / Linux — tambahkan ke ~/.zshrc atau ~/.bashrc
export PATH="$PATH:$HOME/.pub-cache/bin"
```
Setelah `fvm` bisa dipanggil, lanjutkan:
```bash
# Install dependencies
fvm flutter pub get
# Generate kode
fvm dart run build_runner build
```
> Semua perintah Flutter/Dart di project ini **harus diawali `fvm`**. `flutter run` tanpa `fvm` memakai Flutter global dan hasilnya bisa berbeda.
### Menjalankan App
Environment **tidak** ditentukan otomatis dari debug/release — harus dioper lewat `--dart-define`:
```bash
# Development
flutter run
fvm flutter run --dart-define=ENV=dev
# Release
flutter run --release
# Production
fvm flutter run --dart-define=ENV=prod
# Production, release mode
fvm flutter run --release --dart-define=ENV=prod
```
App otomatis menggunakan environment `dev` saat debug dan `prod` saat release.
Kalau `ENV` tidak dioper, nilainya jatuh ke `dev` (lihat `defaultValue` di `lib/main.dart`).
Di VSCode sudah tersedia konfigurasi **Dev** dan **Prod** di `.vscode/launch.json`, tinggal pilih di panel Run and Debug.
### Build
`--dart-define=ENV=prod` wajib ikut, kalau tidak APK-nya akan memakai database dan base URL milik dev:
```bash
# APK
fvm flutter build apk --release --dart-define=ENV=prod
# App Bundle (Play Store)
fvm flutter build appbundle --release --dart-define=ENV=prod
# iOS
fvm flutter build ipa --release --dart-define=ENV=prod
```
---
## Code Generation
Project ini banyak memakai kode generated (`freezed`, `injectable`, `auto_route`, `json_serializable`, `flutter_gen`). Kode itu **wajib di-generate ulang** setiap kali kamu:
- menambah/mengubah kelas `@freezed` (entity, event, state, DTO)
- menambah/mengubah anotasi DI (`@injectable`, `@LazySingleton`, `@Injectable(as: ...)`)
- menambah halaman baru dengan `@RoutePage()`
- menambah aset di `pubspec.yaml`
```bash
# Sekali jalan
fvm dart run build_runner build
# Mode watch — regenerate otomatis saat file berubah, enak dipakai saat ngoding
fvm dart run build_runner watch
```
> **Hindari `--delete-conflicting-outputs`.** Flag itu menghapus seluruh file generated **sebelum** mulai build. Kalau build-nya gagal di tengah jalan, kamu berakhir tanpa kode generated sama sekali dan project tidak bisa di-compile. Pakai hanya kalau build_runner benar-benar mengeluh soal konflik output, dan pastikan working tree-mu bersih dulu supaya bisa `git restore`.
---
## Environment
Konfigurasi environment ada di `lib/env.dart`:
Konfigurasi environment ada di `lib/env.dart` — **itu sumber kebenarannya**, jangan disalin ke sini karena gampang basi.
| Environment | Base URL | Database |
|---|---|---|
| `dev` | `https://api-pos.apskel.id` | `apskel_pos_dev.db` |
| `prod` | `https://api-pos.apskel.id` | `apskel_pos_dev.db` |
| Environment | Database |
|---|---|
| `dev` | `apskel_pos_staging.db` |
| `prod` | `apskel_pos_prod.db` |
Environment dipilih otomatis di `main.dart`:
Environment dipilih dari `--dart-define=ENV=...` di `lib/main.dart`:
```dart
await configureDependencies(
kReleaseMode ? Environment.prod : Environment.dev,
);
const String env = String.fromEnvironment('ENV', defaultValue: 'dev');
await configureDependencies(env);
```
Nilai `env` diteruskan ke `injectable`, yang lalu memilih antara `DevEnv` (`@dev`) dan `ProdEnv` (`@prod`). Menambah environment baru berarti menambah kelas ber-anotasi di `lib/env.dart`, lalu generate ulang.
Karena `dev` dan `prod` memakai **nama database berbeda**, berpindah environment di perangkat yang sama tidak akan mencampur datanya.
---
## Dependency Injection
@@ -210,6 +287,43 @@ App dikunci ke mode **landscape** (kiri & kanan) karena didesain untuk tablet PO
## Catatan Development
- Selalu pakai `fvm flutter` / `fvm dart`, bukan `flutter` / `dart` global
- Jangan edit `injection.config.dart` dan file `*.freezed.dart` / `*.g.dart` secara manual — file tersebut di-generate otomatis
- Setelah mengubah kelas `@freezed` atau anotasi DI, jalankan build_runner sebelum menjalankan app
- Gunakan `log()` dari `dart:developer` untuk logging, bukan `print()`
- `print()` dinonaktifkan di release mode
---
## Troubleshooting
### `build_runner` gagal: `Missing implementation of visitDotShorthandPropertyAccess`
Kamu sedang memakai Flutter yang terlalu baru. Cek:
```bash
fvm flutter --version # harus 3.41.9
flutter --version # ini Flutter global, boleh versi apa saja
```
Kalau versinya bukan 3.41.9, artinya perintahnya jalan tanpa `fvm`. Log build juga memberi petunjuk yang sama:
```
SDK language version 3.13.0 is newer than `analyzer` language version 3.9.0
```
Perbaikannya: `fvm use`, lalu ulangi dengan awalan `fvm`.
Menaikkan Flutter melewati 3.41.x mengharuskan seluruh stack codegen ikut naik — `freezed` 2 → 3/4, `build_runner` → 2.16, `injectable_generator` 2 → 3, `auto_route` 9 → 10. `freezed` 3 mewajibkan tiap kelas dideklarasikan `abstract`/`sealed` dan menghapus `when`/`map` (diganti pattern matching), jadi ini pekerjaan tersendiri, bukan sambilan.
### Project tidak bisa di-compile, banyak error `_$Something` tidak ditemukan
Kode generated-nya hilang atau belum dibuat. Jalankan:
```bash
fvm dart run build_runner build
```
### `fvm: command not found`
FVM terpasang di `~/AppData/Local/Pub/Cache/bin` (Windows) atau `~/.pub-cache/bin` (macOS/Linux) yang mungkin belum ada di `PATH`. Tambahkan direktori itu ke `PATH`, atau panggil dengan path lengkap.
+7
View File
@@ -21,3 +21,10 @@ analyzer:
- test/generated/**
- "**/**.g.dart"
- "**/**.freezed.dart"
- build/**
- android/**
- ios/**
- web/**
- windows/**
- macos/**
- linux/**
@@ -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;
}
+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,
});
}
+1 -1
View File
@@ -10,7 +10,7 @@ abstract class Env {
@dev
class DevEnv implements Env {
@override
String get baseUrl => 'http://194.233.78.1:4001';
String get baseUrl => 'https://enaklo-pos-api.altru.id';
@override
String get dbName => "apskel_pos_staging.db";
@@ -0,0 +1,271 @@
import 'dart:async';
import 'dart:developer';
import 'dart:io';
/// Kenapa sebuah cetakan jaringan dianggap gagal.
enum NetworkPrintFailure {
/// Socket tidak bisa dibuka — IP salah, printer mati, atau beda jaringan.
unreachable,
/// Printer menjawab bahwa kertasnya habis.
paperOut,
/// Printer menjawab bahwa cover-nya terbuka.
coverOpen,
/// Printer menjawab sedang offline / dalam kondisi error.
offline,
/// Koneksi terbuka tapi pengiriman data putus di tengah jalan.
writeFailed,
}
class NetworkPrintResult {
final NetworkPrintFailure? failure;
/// Printer membalas perintah status. Printer clone yang tidak mendukung
/// `DLE EOT` membuat ini `false`, dan pemeriksaan kondisi kertas dilewati.
final bool statusSupported;
/// Kertas menipis. Bukan kegagalan — cetakan tetap keluar, tapi layak
/// diberitahukan ke kasir.
final bool paperNearEnd;
const NetworkPrintResult({
this.failure,
this.statusSupported = false,
this.paperNearEnd = false,
});
bool get isSuccess => failure == null;
}
/// Mencetak ke printer thermal jaringan lewat raw socket port 9100.
///
/// Berbeda dari `flutter_esc_pos_network` yang hanya mengirim byte lalu menutup
/// koneksi, klien ini menanyakan kondisi printer dengan `DLE EOT` dan membaca
/// jawabannya. Tanpa itu, kertas habis atau cover terbuka tidak bisa dibedakan
/// dari cetakan yang berhasil — byte tetap diterima socket dan semuanya tampak
/// sukses.
class NetworkPrinterClient {
static const _logName = 'NetworkPrinterClient';
/// Perintah status real-time ESC/POS: `DLE EOT n`.
static const _statusPrinter = [0x10, 0x04, 1];
static const _statusOffline = [0x10, 0x04, 2];
static const _statusPaper = [0x10, 0x04, 4];
/// Jeda menunggu balasan status. Sengaja pendek: printer yang tidak
/// mendukung `DLE EOT` tidak akan pernah menjawab, dan kita tidak boleh
/// menahan kasir hanya untuk menunggu jawaban yang tidak akan datang.
static const _statusTimeout = Duration(milliseconds: 800);
/// Host yang terbukti tidak menjawab `DLE EOT`. Tanpa catatan ini, printer
/// clone membuat setiap cetakan membayar timeout di atas — mahal untuk kasir
/// yang sedang antre. Cukup dibuktikan sekali per sesi aplikasi.
final Set<String> _statusUnsupported = {};
Future<NetworkPrintResult> printTicket({
required String host,
required List<int> data,
int port = 9100,
Duration connectTimeout = const Duration(seconds: 10),
}) async {
Socket? socket;
try {
socket = await Socket.connect(host, port, timeout: connectTimeout);
} catch (e) {
log('Tidak bisa membuka socket ke $host:$port', name: _logName, error: e);
return const NetworkPrintResult(
failure: NetworkPrintFailure.unreachable,
);
}
final reader = _SocketReader();
late StreamSubscription<List<int>> subscription;
try {
subscription = socket.listen(
reader.add,
onError: (Object e) => log('Socket error', name: _logName, error: e),
cancelOnError: false,
);
final before = await _readStatus(socket, reader, host);
if (before.answered) {
final blocking = before.blockingFailure;
if (blocking != null) {
log('Printer $host menolak: ${blocking.name}', name: _logName);
return NetworkPrintResult(failure: blocking, statusSupported: true);
}
}
socket.add(data);
await socket.flush();
// Kertas bisa habis di tengah struk. Cetakan yang terpotong sama
// tidak bergunanya dengan yang tidak keluar sama sekali, jadi kondisi
// ini dilaporkan sebagai gagal supaya kasir mencetak ulang.
final after = before.answered
? await _readStatus(socket, reader, host)
: const _PrinterStatus.unanswered();
if (after.answered) {
final blocking = after.blockingFailure;
if (blocking != null) {
log('Printer $host bermasalah setelah cetak: ${blocking.name}',
name: _logName);
return NetworkPrintResult(failure: blocking, statusSupported: true);
}
}
return NetworkPrintResult(
statusSupported: before.answered,
paperNearEnd: after.paperNearEnd || before.paperNearEnd,
);
} catch (e) {
log('Gagal mengirim data ke $host:$port', name: _logName, error: e);
return const NetworkPrintResult(
failure: NetworkPrintFailure.writeFailed,
statusSupported: false,
);
} finally {
await subscription.cancel().catchError((_) {});
try {
await socket.close();
} catch (_) {
// Printer kadang menutup duluan; tidak ada yang bisa diperbuat.
}
socket.destroy();
}
}
Future<_PrinterStatus> _readStatus(
Socket socket,
_SocketReader reader,
String host,
) async {
if (_statusUnsupported.contains(host)) {
return const _PrinterStatus.unanswered();
}
final printer = await _query(socket, reader, _statusPrinter);
// Printer yang tidak mendukung DLE EOT tidak menjawab sama sekali.
// Perlakukan sebagai "tidak diketahui", bukan sebagai error — kalau tidak,
// printer semacam itu jadi tidak bisa dipakai sama sekali.
if (printer == null) {
log(
'Printer $host tidak menjawab status; pemeriksaan kondisi kertas '
'dilewati untuk seterusnya',
name: _logName,
);
_statusUnsupported.add(host);
return const _PrinterStatus.unanswered();
}
final offline = await _query(socket, reader, _statusOffline);
final paper = await _query(socket, reader, _statusPaper);
return _PrinterStatus(
answered: true,
offline: printer & 0x08 != 0,
coverOpen: (offline ?? 0) & 0x04 != 0,
paperOut: ((offline ?? 0) & 0x20 != 0) || ((paper ?? 0) & 0x60 != 0),
paperNearEnd: (paper ?? 0) & 0x0C != 0,
);
}
/// Mengirim satu perintah status dan menunggu satu byte balasan.
/// `null` berarti printer tidak menjawab atau menjawab dengan format asing.
Future<int?> _query(
Socket socket,
_SocketReader reader,
List<int> command,
) async {
reader.clear();
socket.add(command);
await socket.flush();
final byte = await reader.readByte(_statusTimeout);
if (byte == null) return null;
// Balasan DLE EOT selalu punya pola tetap: bit0=0, bit1=1, bit4=1, bit7=0.
// Byte yang tidak cocok berarti bukan balasan status, jadi lebih aman
// dianggap tidak menjawab daripada salah tafsir.
if (byte & 0x93 != 0x12) {
log('Balasan status tidak dikenali: 0x${byte.toRadixString(16)}',
name: _logName);
return null;
}
return byte;
}
}
class _PrinterStatus {
final bool answered;
final bool offline;
final bool coverOpen;
final bool paperOut;
final bool paperNearEnd;
const _PrinterStatus({
required this.answered,
this.offline = false,
this.coverOpen = false,
this.paperOut = false,
this.paperNearEnd = false,
});
const _PrinterStatus.unanswered()
: answered = false,
offline = false,
coverOpen = false,
paperOut = false,
paperNearEnd = false;
/// Kondisi yang membuat struk tidak akan keluar. Diperiksa berurutan dari
/// yang paling spesifik supaya pesan ke kasir langsung menyebut tindakannya.
NetworkPrintFailure? get blockingFailure {
if (paperOut) return NetworkPrintFailure.paperOut;
if (coverOpen) return NetworkPrintFailure.coverOpen;
if (offline) return NetworkPrintFailure.offline;
return null;
}
}
/// Menampung byte yang datang dari socket dan membiarkannya dibaca satu per
/// satu dengan batas waktu. Socket adalah stream sekali-langganan, jadi
/// pembacaannya harus lewat satu subscription untuk seluruh sesi.
class _SocketReader {
final List<int> _buffer = [];
Completer<void>? _waiter;
void add(List<int> data) {
_buffer.addAll(data);
_waiter?.complete();
_waiter = null;
}
void clear() => _buffer.clear();
Future<int?> readByte(Duration timeout) async {
if (_buffer.isEmpty) {
final waiter = Completer<void>();
_waiter = waiter;
try {
await waiter.future.timeout(timeout);
} on TimeoutException {
_waiter = null;
return null;
}
}
if (_buffer.isEmpty) return null;
return _buffer.removeAt(0);
}
}
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -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>(),
-1
View File
@@ -5,7 +5,6 @@ import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:injectable/injectable.dart';
import 'common/service/fcm_service.dart';
import 'injection.dart';
+18 -4
View File
@@ -13,8 +13,6 @@ import '../application/payment_method/payment_method_loader/payment_method_loade
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';
import '../application/sync/sync_setting/sync_setting_bloc.dart';
import '../application/table/table_form/table_form_bloc.dart';
@@ -23,6 +21,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';
@@ -54,8 +53,9 @@ class _AppWidgetState extends State<AppWidget> {
BlocProvider(create: (context) => getIt<VoidFormBloc>()),
BlocProvider(create: (context) => getIt<BluetoothLoaderBloc>()),
BlocProvider(create: (context) => getIt<BluetoothConnectBloc>()),
BlocProvider(create: (context) => getIt<PrinterFormBloc>()),
BlocProvider(create: (context) => getIt<PrinterLoaderBloc>()),
// PrinterFormBloc dan PrinterLoaderBloc sengaja TIDAK global: tiap tab
// pengaturan printer memakai instance sendiri, lihat
// SettingPrinterSection.
BlocProvider(create: (context) => getIt<PrintStruckBloc>()),
BlocProvider(create: (context) => getIt<SyncSettingBloc>()),
BlocProvider(create: (context) => getIt<LogoutBloc>()),
@@ -67,6 +67,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,
@@ -1,5 +1,9 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../../../application/printer/printer_form/printer_form_bloc.dart';
import '../../../../../../application/printer/printer_loader/printer_loader_bloc.dart';
import '../../../../../../injection.dart';
import '../../../../../components/page/page_title.dart';
import '../../../../../components/tab/custom_tabbar.dart';
import 'printer/setting_printer_bar.dart';
@@ -11,18 +15,36 @@ import 'printer/setting_printer_ticket.dart';
class SettingPrinterSection extends StatelessWidget {
const SettingPrinterSection({super.key});
/// Tiap tab wajib punya `PrinterFormBloc` dan `PrinterLoaderBloc` sendiri.
///
/// Kelima tab hidup bersamaan di dalam `TabBarView`, dan `PrinterFormBloc`
/// membangun entitas `Printer` sepenuhnya dari state-nya (`type`, `address`,
/// `code`, ...). Saat blocnya dibagi, tab yang terakhir menulis state akan
/// menang, lalu tombol Simpan di tab mana pun menyimpan state milik tab lain
/// — itu sebabnya printer kitchen bisa tersimpan dengan alamat IP yang benar
/// tapi `type` "Bluetooth" warisan tab Receipt.
Widget _scoped(Widget tab) {
return MultiBlocProvider(
providers: [
BlocProvider(create: (_) => getIt<PrinterFormBloc>()),
BlocProvider(create: (_) => getIt<PrinterLoaderBloc>()),
],
child: tab,
);
}
@override
Widget build(BuildContext context) {
return const Column(
return Column(
children: [
PageTitle(
const PageTitle(
isBack: false,
title: 'Printer',
subtitle: 'Kelola printer untuk cetak struk',
),
Expanded(
child: CustomTabBar(
tabTitles: [
tabTitles: const [
'Receipt Printer',
'Checker Printer',
'Kitchen Printer',
@@ -30,11 +52,11 @@ class SettingPrinterSection extends StatelessWidget {
'Tiket Printer',
],
tabViews: [
SettingPrinterReceipt(),
SettingPrinterChecker(),
SettingPrinterKitchen(),
SettingPrinterBar(),
SettingPrinterTicket(),
_scoped(const SettingPrinterReceipt()),
_scoped(const SettingPrinterChecker()),
_scoped(const SettingPrinterKitchen()),
_scoped(const SettingPrinterBar()),
_scoped(const SettingPrinterTicket()),
],
),
),
+1 -16
View File
@@ -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(
@@ -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),
);
},
),
);
},
),
),
);
@@ -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),
),
],
),
);
},
),
),
);
},
),
);
}
+5 -5
View File
@@ -852,10 +852,10 @@ packages:
dependency: transitive
description:
name: meta
sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
url: "https://pub.dev"
source: hosted
version: "1.18.0"
version: "1.17.0"
mime:
dependency: transitive
description:
@@ -1337,10 +1337,10 @@ packages:
dependency: transitive
description:
name: test_api
sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
url: "https://pub.dev"
source: hosted
version: "0.7.11"
version: "0.7.10"
time:
dependency: transitive
description:
@@ -1502,5 +1502,5 @@ packages:
source: hosted
version: "3.1.3"
sdks:
dart: ">=3.10.0-0 <4.0.0"
dart: ">=3.9.0-0 <4.0.0"
flutter: ">=3.29.0"
+1 -1
View File
@@ -3,7 +3,7 @@ description: "A new Flutter project."
publish_to: "none"
version: 1.0.11+17
version: 1.0.12+19
environment:
sdk: ^3.8.1