Compare commits
8
Commits
865897fc04
...
9777f63426
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9777f63426 | ||
|
|
7986164a17 | ||
|
|
32f64a197a | ||
|
|
cb7139fc3e | ||
|
|
951e154623 | ||
|
|
ebcddca4bb | ||
|
|
1b4caf45d5 | ||
|
|
038f4d65fe |
@@ -32,6 +32,7 @@ migrate_working_dir/
|
|||||||
.pub-cache/
|
.pub-cache/
|
||||||
.pub/
|
.pub/
|
||||||
/build/
|
/build/
|
||||||
|
android/build/
|
||||||
|
|
||||||
# Symbolication related
|
# Symbolication related
|
||||||
app.*.symbols
|
app.*.symbols
|
||||||
@@ -43,3 +44,8 @@ app.*.map.json
|
|||||||
/android/app/debug
|
/android/app/debug
|
||||||
/android/app/profile
|
/android/app/profile
|
||||||
/android/app/release
|
/android/app/release
|
||||||
|
|
||||||
|
# FVM - versi Flutter dipin di .fvmrc (ikut di-commit), SDK-nya tidak
|
||||||
|
|
||||||
|
# FVM Version Cache
|
||||||
|
.fvm/
|
||||||
Vendored
+3
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"dart.flutterSdkPath": ".fvm/versions/3.41.9"
|
||||||
|
}
|
||||||
@@ -25,6 +25,7 @@ Aplikasi Point of Sale (POS) berbasis Flutter untuk manajemen kasir restoran. Me
|
|||||||
|
|
||||||
| Kategori | Library |
|
| Kategori | Library |
|
||||||
|---|---|
|
|---|---|
|
||||||
|
| Version Manager | `fvm` — Flutter dipin di `.fvmrc` |
|
||||||
| State Management | `flutter_bloc` + `bloc` |
|
| State Management | `flutter_bloc` + `bloc` |
|
||||||
| Dependency Injection | `get_it` + `injectable` |
|
| Dependency Injection | `get_it` + `injectable` |
|
||||||
| Navigation | `auto_route` |
|
| Navigation | `auto_route` |
|
||||||
@@ -78,11 +79,13 @@ feature/
|
|||||||
|
|
||||||
### Prasyarat
|
### Prasyarat
|
||||||
|
|
||||||
- Flutter SDK `^3.8.1`
|
- **FVM** — project ini mengunci versi Flutter, jangan pakai Flutter global
|
||||||
- Dart SDK `^3.8.1`
|
- **Flutter 3.41.9** (Dart 3.11.5) — dipin di `.fvmrc`
|
||||||
- Android SDK / Xcode (untuk iOS)
|
- Android SDK / Xcode (untuk iOS)
|
||||||
- Firebase project yang sudah dikonfigurasi
|
- 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
|
### Instalasi
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -90,44 +93,118 @@ feature/
|
|||||||
git clone <repo-url>
|
git clone <repo-url>
|
||||||
cd apskel-pos-flutter-v2
|
cd apskel-pos-flutter-v2
|
||||||
|
|
||||||
# Install dependencies
|
# Pasang FVM (sekali per mesin)
|
||||||
flutter pub get
|
dart pub global activate fvm
|
||||||
|
|
||||||
# Generate kode (freezed, injectable, auto_route, json_serializable)
|
# Ambil & pakai versi Flutter yang dipin di .fvmrc
|
||||||
dart run build_runner build --delete-conflicting-outputs
|
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
|
### Menjalankan App
|
||||||
|
|
||||||
|
Environment **tidak** ditentukan otomatis dari debug/release — harus dioper lewat `--dart-define`:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Development
|
# Development
|
||||||
flutter run
|
fvm flutter run --dart-define=ENV=dev
|
||||||
|
|
||||||
# Release
|
# Production
|
||||||
flutter run --release
|
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
|
## 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 |
|
| Environment | Database |
|
||||||
|---|---|---|
|
|---|---|
|
||||||
| `dev` | `https://api-pos.apskel.id` | `apskel_pos_dev.db` |
|
| `dev` | `apskel_pos_staging.db` |
|
||||||
| `prod` | `https://api-pos.apskel.id` | `apskel_pos_dev.db` |
|
| `prod` | `apskel_pos_prod.db` |
|
||||||
|
|
||||||
Environment dipilih otomatis di `main.dart`:
|
Environment dipilih dari `--dart-define=ENV=...` di `lib/main.dart`:
|
||||||
|
|
||||||
```dart
|
```dart
|
||||||
await configureDependencies(
|
const String env = String.fromEnvironment('ENV', defaultValue: 'dev');
|
||||||
kReleaseMode ? Environment.prod : Environment.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
|
## Dependency Injection
|
||||||
@@ -210,6 +287,43 @@ App dikunci ke mode **landscape** (kiri & kanan) karena didesain untuk tablet PO
|
|||||||
|
|
||||||
## Catatan Development
|
## 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
|
- 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()`
|
- Gunakan `log()` dari `dart:developer` untuk logging, bukan `print()`
|
||||||
- `print()` dinonaktifkan di release mode
|
- `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.
|
||||||
|
|||||||
@@ -21,3 +21,10 @@ analyzer:
|
|||||||
- test/generated/**
|
- test/generated/**
|
||||||
- "**/**.g.dart"
|
- "**/**.g.dart"
|
||||||
- "**/**.freezed.dart"
|
- "**/**.freezed.dart"
|
||||||
|
- build/**
|
||||||
|
- android/**
|
||||||
|
- ios/**
|
||||||
|
- web/**
|
||||||
|
- windows/**
|
||||||
|
- macos/**
|
||||||
|
- linux/**
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import 'package:bloc/bloc.dart';
|
import 'package:bloc/bloc.dart';
|
||||||
import 'package:dartz/dartz.dart' hide Order;
|
|
||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
import 'package:injectable/injectable.dart' hide Order;
|
import 'package:injectable/injectable.dart' hide Order;
|
||||||
|
|
||||||
@@ -21,136 +20,63 @@ class PrintStruckBloc extends Bloc<PrintStruckEvent, PrintStruckState> {
|
|||||||
Future<void> _onPrintStruckEvent(
|
Future<void> _onPrintStruckEvent(
|
||||||
PrintStruckEvent event,
|
PrintStruckEvent event,
|
||||||
Emitter<PrintStruckState> emit,
|
Emitter<PrintStruckState> emit,
|
||||||
) {
|
) => event.map(
|
||||||
return event.map(
|
order: (e) => _print(emit, e.order, _stationsForOrder(e.order)),
|
||||||
order: (e) async {
|
payment: (e) => _print(emit, e.order, const [PrintStation.receipt]),
|
||||||
Either<PrinterFailure, Unit> failureOrSuccess;
|
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(
|
return _print(emit, order, failed);
|
||||||
order: e.order,
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
emit(
|
/// Struk pelanggan hanya ikut dicetak kalau ordernya sudah dibayar.
|
||||||
state.copyWith(
|
List<PrintStation> _stationsForOrder(Order order) => [
|
||||||
isPrinting: false,
|
if (order.totalPaid > 0) PrintStation.receipt,
|
||||||
failureOrPrintStruck: optionOf(failureOrSuccess),
|
PrintStation.cashier,
|
||||||
),
|
PrintStation.checker,
|
||||||
);
|
PrintStation.kitchen,
|
||||||
},
|
PrintStation.bar,
|
||||||
cashier: (e) async {
|
];
|
||||||
Either<PrinterFailure, Unit> failureOrSuccess;
|
|
||||||
|
|
||||||
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(
|
emit(
|
||||||
order: e.order,
|
state.copyWith(
|
||||||
);
|
isPrinting: true,
|
||||||
|
order: order,
|
||||||
emit(
|
jobId: state.jobId + 1,
|
||||||
state.copyWith(
|
stations: [
|
||||||
isPrinting: false,
|
for (final station in stations) PrintStationResult.pending(station),
|
||||||
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),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
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
|
@freezed
|
||||||
class PrintStruckEvent with _$PrintStruckEvent {
|
class PrintStruckEvent with _$PrintStruckEvent {
|
||||||
|
/// Cetak seluruh station untuk satu order.
|
||||||
const factory PrintStruckEvent.order(Order order) = _Order;
|
const factory PrintStruckEvent.order(Order order) = _Order;
|
||||||
|
|
||||||
const factory PrintStruckEvent.cashier(Order order) = _Cashier;
|
const factory PrintStruckEvent.cashier(Order order) = _Cashier;
|
||||||
const factory PrintStruckEvent.payment(Order order) = _Payment;
|
const factory PrintStruckEvent.payment(Order order) = _Payment;
|
||||||
const factory PrintStruckEvent.voided(Order order) = _Voided;
|
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.kitchen(Order order) = _Kitchen;
|
||||||
const factory PrintStruckEvent.bar(Order order) = _Bar;
|
const factory PrintStruckEvent.bar(Order order) = _Bar;
|
||||||
const factory PrintStruckEvent.splitBill(Order order) = _SplitBill;
|
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
|
@freezed
|
||||||
class PrintStruckState with _$PrintStruckState {
|
class PrintStruckState with _$PrintStruckState {
|
||||||
factory PrintStruckState({
|
const PrintStruckState._();
|
||||||
required Option<Either<PrinterFailure, Unit>> failureOrPrintStruck,
|
|
||||||
|
const factory PrintStruckState({
|
||||||
|
/// Status seluruh station di job cetak terakhir, urut sesuai urutan cetak.
|
||||||
|
@Default(<PrintStationResult>[]) List<PrintStationResult> stations,
|
||||||
@Default(false) bool isPrinting,
|
@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;
|
}) = _PrintStruckState;
|
||||||
|
|
||||||
factory PrintStruckState.initial() =>
|
factory PrintStruckState.initial() => const PrintStruckState();
|
||||||
PrintStruckState(failureOrPrintStruck: none());
|
|
||||||
|
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 'printer.freezed.dart';
|
||||||
|
|
||||||
part 'entities/printer_entity.dart';
|
part 'entities/printer_entity.dart';
|
||||||
|
part 'entities/print_station_result.dart';
|
||||||
part 'failures/printer_failure.dart';
|
part 'failures/printer_failure.dart';
|
||||||
part 'repositories/i_printer_repository.dart';
|
part 'repositories/i_printer_repository.dart';
|
||||||
|
|||||||
@@ -307,6 +307,216 @@ abstract class _Printer implements Printer {
|
|||||||
throw _privateConstructorUsedError;
|
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
|
/// @nodoc
|
||||||
mixin _$PrinterFailure {
|
mixin _$PrinterFailure {
|
||||||
@optionalTypeArgs
|
@optionalTypeArgs
|
||||||
|
|||||||
@@ -15,25 +15,17 @@ abstract class IPrinterRepository {
|
|||||||
List<int> printValue,
|
List<int> printValue,
|
||||||
);
|
);
|
||||||
|
|
||||||
Future<Either<PrinterFailure, Unit>> printStruckOrder({required Order order});
|
/// Mencetak [stations] untuk [order], melaporkan status tiap station.
|
||||||
Future<Either<PrinterFailure, Unit>> printStruckSaveOrder({
|
///
|
||||||
required Order order,
|
/// Setiap emit berisi daftar lengkap semua station, jadi UI cukup
|
||||||
});
|
/// menggambar ulang apa adanya.
|
||||||
Future<Either<PrinterFailure, Unit>> printStruckCashier({
|
///
|
||||||
required Order order,
|
/// Satu station yang gagal **tidak** membatalkan station lainnya — semuanya
|
||||||
});
|
/// tetap dicoba. Station Bluetooth dijalankan berurutan karena radionya
|
||||||
Future<Either<PrinterFailure, Unit>> printStruckPayment({
|
/// hanya satu, station Network dijalankan paralel karena tidak berebut
|
||||||
required Order order,
|
/// sumber daya dengan Bluetooth.
|
||||||
});
|
Stream<List<PrintStationResult>> printStations({
|
||||||
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({
|
|
||||||
required Order order,
|
required Order order,
|
||||||
|
required List<PrintStation> stations,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -10,7 +10,7 @@ abstract class Env {
|
|||||||
@dev
|
@dev
|
||||||
class DevEnv implements Env {
|
class DevEnv implements Env {
|
||||||
@override
|
@override
|
||||||
String get baseUrl => 'http://194.233.78.1:4001';
|
String get baseUrl => 'https://enaklo-pos-api.altru.id';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get dbName => "apskel_pos_staging.db";
|
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
@@ -233,7 +233,7 @@ extension GetItInjectableX on _i174.GetIt {
|
|||||||
() => _i457.ApiClient(gh<_i361.Dio>(), gh<_i923.Env>()),
|
() => _i457.ApiClient(gh<_i361.Dio>(), gh<_i923.Env>()),
|
||||||
);
|
);
|
||||||
gh.factory<_i923.Env>(() => _i923.ProdEnv(), registerFor: {_prod});
|
gh.factory<_i923.Env>(() => _i923.ProdEnv(), registerFor: {_prod});
|
||||||
gh.factory<_i104.IPrinterRepository>(
|
gh.lazySingleton<_i104.IPrinterRepository>(
|
||||||
() => _i881.PrinterRepository(
|
() => _i881.PrinterRepository(
|
||||||
gh<_i149.PrinterLocalDataProvider>(),
|
gh<_i149.PrinterLocalDataProvider>(),
|
||||||
gh<_i693.OutletLocalDatasource>(),
|
gh<_i693.OutletLocalDatasource>(),
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import 'package:firebase_crashlytics/firebase_crashlytics.dart';
|
|||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:injectable/injectable.dart';
|
|
||||||
|
|
||||||
import 'common/service/fcm_service.dart';
|
import 'common/service/fcm_service.dart';
|
||||||
import 'injection.dart';
|
import 'injection.dart';
|
||||||
|
|||||||
@@ -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_connect/bluetooth_connect_bloc.dart';
|
||||||
import '../application/printer/bluetooth/bluetooth_loader/bluetooth_loader_bloc.dart';
|
import '../application/printer/bluetooth/bluetooth_loader/bluetooth_loader_bloc.dart';
|
||||||
import '../application/printer/print_struck/print_struck_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/product/product_loader/product_loader_bloc.dart';
|
||||||
import '../application/sync/sync_setting/sync_setting_bloc.dart';
|
import '../application/sync/sync_setting/sync_setting_bloc.dart';
|
||||||
import '../application/table/table_form/table_form_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/theme/theme.dart';
|
||||||
import '../common/constant/app_constant.dart';
|
import '../common/constant/app_constant.dart';
|
||||||
import '../injection.dart';
|
import '../injection.dart';
|
||||||
|
import 'components/dialog/dialog.dart';
|
||||||
import 'router/app_router.dart';
|
import 'router/app_router.dart';
|
||||||
import 'router/app_router_observer.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<VoidFormBloc>()),
|
||||||
BlocProvider(create: (context) => getIt<BluetoothLoaderBloc>()),
|
BlocProvider(create: (context) => getIt<BluetoothLoaderBloc>()),
|
||||||
BlocProvider(create: (context) => getIt<BluetoothConnectBloc>()),
|
BlocProvider(create: (context) => getIt<BluetoothConnectBloc>()),
|
||||||
BlocProvider(create: (context) => getIt<PrinterFormBloc>()),
|
// PrinterFormBloc dan PrinterLoaderBloc sengaja TIDAK global: tiap tab
|
||||||
BlocProvider(create: (context) => getIt<PrinterLoaderBloc>()),
|
// pengaturan printer memakai instance sendiri, lihat
|
||||||
|
// SettingPrinterSection.
|
||||||
BlocProvider(create: (context) => getIt<PrintStruckBloc>()),
|
BlocProvider(create: (context) => getIt<PrintStruckBloc>()),
|
||||||
BlocProvider(create: (context) => getIt<SyncSettingBloc>()),
|
BlocProvider(create: (context) => getIt<SyncSettingBloc>()),
|
||||||
BlocProvider(create: (context) => getIt<LogoutBloc>()),
|
BlocProvider(create: (context) => getIt<LogoutBloc>()),
|
||||||
@@ -67,6 +67,20 @@ class _AppWidgetState extends State<AppWidget> {
|
|||||||
routerConfig: _appRouter.config(
|
routerConfig: _appRouter.config(
|
||||||
navigatorObservers: () => <NavigatorObserver>[AppRouteObserver()],
|
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:auto_route/auto_route.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.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/outlet/outlet_loader/outlet_loader_bloc.dart';
|
||||||
import '../../../application/printer/bluetooth/bluetooth_connect/bluetooth_connect_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/bluetooth/bluetooth_loader/bluetooth_loader_bloc.dart';
|
||||||
|
import '../../../application/printer/print_struck/print_struck_bloc.dart';
|
||||||
import '../../../common/extension/extension.dart';
|
import '../../../common/extension/extension.dart';
|
||||||
import '../../../common/function/app_function.dart';
|
import '../../../common/function/app_function.dart';
|
||||||
import '../../../common/theme/theme.dart';
|
import '../../../common/theme/theme.dart';
|
||||||
import '../../../common/types/order_type.dart';
|
import '../../../common/types/order_type.dart';
|
||||||
import '../../../domain/delivery/delivery.dart';
|
import '../../../domain/delivery/delivery.dart';
|
||||||
|
import '../../../domain/printer/printer.dart';
|
||||||
import '../../../domain/product/product.dart';
|
import '../../../domain/product/product.dart';
|
||||||
import '../button/button.dart';
|
import '../button/button.dart';
|
||||||
import '../card/outlet_card.dart';
|
import '../card/outlet_card.dart';
|
||||||
@@ -23,3 +27,4 @@ part 'variant_dialog.dart';
|
|||||||
part 'delivery_dialog.dart';
|
part 'delivery_dialog.dart';
|
||||||
part 'order_type_dialog.dart';
|
part 'order_type_dialog.dart';
|
||||||
part 'printer_bluetooth_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';
|
import 'receipt_component_builder.dart';
|
||||||
|
|
||||||
class PrintUi {
|
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({
|
Future<List<int>> printOrder({
|
||||||
required Order order,
|
required Order order,
|
||||||
required Outlet outlet,
|
required Outlet outlet,
|
||||||
@@ -14,7 +35,7 @@ class PrintUi {
|
|||||||
}) async {
|
}) async {
|
||||||
List<int> bytes = [];
|
List<int> bytes = [];
|
||||||
|
|
||||||
final profile = await CapabilityProfile.load();
|
final profile = await _capabilityProfile();
|
||||||
final generator = Generator(
|
final generator = Generator(
|
||||||
paper == 58 ? PaperSize.mm58 : PaperSize.mm80,
|
paper == 58 ? PaperSize.mm58 : PaperSize.mm80,
|
||||||
profile,
|
profile,
|
||||||
@@ -90,7 +111,7 @@ class PrintUi {
|
|||||||
}) async {
|
}) async {
|
||||||
List<int> bytes = [];
|
List<int> bytes = [];
|
||||||
|
|
||||||
final profile = await CapabilityProfile.load();
|
final profile = await _capabilityProfile();
|
||||||
final generator = Generator(
|
final generator = Generator(
|
||||||
paper == 58 ? PaperSize.mm58 : PaperSize.mm80,
|
paper == 58 ? PaperSize.mm58 : PaperSize.mm80,
|
||||||
profile,
|
profile,
|
||||||
@@ -142,7 +163,7 @@ class PrintUi {
|
|||||||
}) async {
|
}) async {
|
||||||
List<int> bytes = [];
|
List<int> bytes = [];
|
||||||
|
|
||||||
final profile = await CapabilityProfile.load();
|
final profile = await _capabilityProfile();
|
||||||
final generator = Generator(
|
final generator = Generator(
|
||||||
paper == 58 ? PaperSize.mm58 : PaperSize.mm80,
|
paper == 58 ? PaperSize.mm58 : PaperSize.mm80,
|
||||||
profile,
|
profile,
|
||||||
@@ -198,7 +219,7 @@ class PrintUi {
|
|||||||
}) async {
|
}) async {
|
||||||
List<int> bytes = [];
|
List<int> bytes = [];
|
||||||
|
|
||||||
final profile = await CapabilityProfile.load();
|
final profile = await _capabilityProfile();
|
||||||
final generator = Generator(
|
final generator = Generator(
|
||||||
paper == 58 ? PaperSize.mm58 : PaperSize.mm80,
|
paper == 58 ? PaperSize.mm58 : PaperSize.mm80,
|
||||||
profile,
|
profile,
|
||||||
@@ -253,7 +274,7 @@ class PrintUi {
|
|||||||
}) async {
|
}) async {
|
||||||
List<int> bytes = [];
|
List<int> bytes = [];
|
||||||
|
|
||||||
final profile = await CapabilityProfile.load();
|
final profile = await _capabilityProfile();
|
||||||
final generator = Generator(
|
final generator = Generator(
|
||||||
paper == 58 ? PaperSize.mm58 : PaperSize.mm80,
|
paper == 58 ? PaperSize.mm58 : PaperSize.mm80,
|
||||||
profile,
|
profile,
|
||||||
@@ -294,7 +315,7 @@ class PrintUi {
|
|||||||
}) async {
|
}) async {
|
||||||
List<int> bytes = [];
|
List<int> bytes = [];
|
||||||
|
|
||||||
final profile = await CapabilityProfile.load();
|
final profile = await _capabilityProfile();
|
||||||
final generator = Generator(
|
final generator = Generator(
|
||||||
paper == 58 ? PaperSize.mm58 : PaperSize.mm80,
|
paper == 58 ? PaperSize.mm58 : PaperSize.mm80,
|
||||||
profile,
|
profile,
|
||||||
@@ -372,7 +393,7 @@ class PrintUi {
|
|||||||
}) async {
|
}) async {
|
||||||
List<int> bytes = [];
|
List<int> bytes = [];
|
||||||
|
|
||||||
final profile = await CapabilityProfile.load();
|
final profile = await _capabilityProfile();
|
||||||
final generator = Generator(
|
final generator = Generator(
|
||||||
paper == 58 ? PaperSize.mm58 : PaperSize.mm80,
|
paper == 58 ? PaperSize.mm58 : PaperSize.mm80,
|
||||||
profile,
|
profile,
|
||||||
@@ -451,7 +472,7 @@ class PrintUi {
|
|||||||
}) async {
|
}) async {
|
||||||
List<int> bytes = [];
|
List<int> bytes = [];
|
||||||
|
|
||||||
final profile = await CapabilityProfile.load();
|
final profile = await _capabilityProfile();
|
||||||
final generator = Generator(
|
final generator = Generator(
|
||||||
paper == 58 ? PaperSize.mm58 : PaperSize.mm80,
|
paper == 58 ? PaperSize.mm58 : PaperSize.mm80,
|
||||||
profile,
|
profile,
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
import 'package:flutter/material.dart';
|
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/page/page_title.dart';
|
||||||
import '../../../../../components/tab/custom_tabbar.dart';
|
import '../../../../../components/tab/custom_tabbar.dart';
|
||||||
import 'printer/setting_printer_bar.dart';
|
import 'printer/setting_printer_bar.dart';
|
||||||
@@ -11,18 +15,36 @@ import 'printer/setting_printer_ticket.dart';
|
|||||||
class SettingPrinterSection extends StatelessWidget {
|
class SettingPrinterSection extends StatelessWidget {
|
||||||
const SettingPrinterSection({super.key});
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return const Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
PageTitle(
|
const PageTitle(
|
||||||
isBack: false,
|
isBack: false,
|
||||||
title: 'Printer',
|
title: 'Printer',
|
||||||
subtitle: 'Kelola printer untuk cetak struk',
|
subtitle: 'Kelola printer untuk cetak struk',
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: CustomTabBar(
|
child: CustomTabBar(
|
||||||
tabTitles: [
|
tabTitles: const [
|
||||||
'Receipt Printer',
|
'Receipt Printer',
|
||||||
'Checker Printer',
|
'Checker Printer',
|
||||||
'Kitchen Printer',
|
'Kitchen Printer',
|
||||||
@@ -30,11 +52,11 @@ class SettingPrinterSection extends StatelessWidget {
|
|||||||
'Tiket Printer',
|
'Tiket Printer',
|
||||||
],
|
],
|
||||||
tabViews: [
|
tabViews: [
|
||||||
SettingPrinterReceipt(),
|
_scoped(const SettingPrinterReceipt()),
|
||||||
SettingPrinterChecker(),
|
_scoped(const SettingPrinterChecker()),
|
||||||
SettingPrinterKitchen(),
|
_scoped(const SettingPrinterKitchen()),
|
||||||
SettingPrinterBar(),
|
_scoped(const SettingPrinterBar()),
|
||||||
SettingPrinterTicket(),
|
_scoped(const SettingPrinterTicket()),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -3,10 +3,8 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
import '../../../application/order/order_loader/order_loader_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 '../../../common/theme/theme.dart';
|
||||||
import '../../../injection.dart';
|
import '../../../injection.dart';
|
||||||
import '../../components/toast/flushbar.dart';
|
|
||||||
import 'widgets/order_left_panel.dart';
|
import 'widgets/order_left_panel.dart';
|
||||||
import 'widgets/order_right_panel.dart';
|
import 'widgets/order_right_panel.dart';
|
||||||
|
|
||||||
@@ -37,20 +35,7 @@ class OrderPage extends StatelessWidget implements AutoRouteWrapper {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
BlocListener<PrintStruckBloc, PrintStruckState>(
|
// Hasil cetak dilaporkan lewat dialog progres global di `AppWidget`.
|
||||||
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: SafeArea(
|
child: SafeArea(
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
|
|||||||
+25
-40
@@ -3,10 +3,8 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
import '../../../../../application/checkout/checkout_form/checkout_form_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 '../../../../../common/theme/theme.dart';
|
||||||
import '../../../../components/spaces/space.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_left_panel.dart';
|
||||||
import 'widgets/success_add_item_order_right_panel.dart';
|
import 'widgets/success_add_item_order_right_panel.dart';
|
||||||
|
|
||||||
@@ -16,46 +14,33 @@ class SuccessAddItemOrderPage extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocListener<PrintStruckBloc, PrintStruckState>(
|
// Hasil cetak dilaporkan lewat dialog progres global di `AppWidget`.
|
||||||
listenWhen: (p, c) => p.failureOrPrintStruck != c.failureOrPrintStruck,
|
return Scaffold(
|
||||||
listener: (context, state) {
|
backgroundColor: AppColor.background,
|
||||||
state.failureOrPrintStruck.fold(
|
body: SafeArea(
|
||||||
() {},
|
child: BlocBuilder<CheckoutFormBloc, CheckoutFormState>(
|
||||||
(either) => either.fold(
|
builder: (context, checkoutState) {
|
||||||
(f) => AppFlushbar.showPrinterFailureToast(context, f),
|
return Padding(
|
||||||
(success) {
|
padding: const EdgeInsets.all(24.0),
|
||||||
AppFlushbar.showSuccess(context, "Struck berhasil dicetak");
|
child: Row(
|
||||||
},
|
children: [
|
||||||
),
|
Expanded(
|
||||||
);
|
flex: 35,
|
||||||
},
|
child: SuccessAddItemOrderLeftPanel(
|
||||||
child: Scaffold(
|
checkoutState: checkoutState,
|
||||||
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(
|
const SpaceWidth(16),
|
||||||
flex: 65,
|
Expanded(
|
||||||
child: SuccessAddItemOrderRightPanel(
|
flex: 65,
|
||||||
checkoutState: checkoutState,
|
child: SuccessAddItemOrderRightPanel(
|
||||||
),
|
checkoutState: checkoutState,
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
],
|
||||||
);
|
),
|
||||||
},
|
);
|
||||||
),
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,13 +3,11 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
import '../../../../../application/order/order_loader/order_loader_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 '../../../../../common/theme/theme.dart';
|
||||||
import '../../../../../domain/order/order.dart';
|
import '../../../../../domain/order/order.dart';
|
||||||
import '../../../../../injection.dart';
|
import '../../../../../injection.dart';
|
||||||
import '../../../../components/loader/loader_with_text.dart';
|
import '../../../../components/loader/loader_with_text.dart';
|
||||||
import '../../../../components/spaces/space.dart';
|
import '../../../../components/spaces/space.dart';
|
||||||
import '../../../../components/toast/flushbar.dart';
|
|
||||||
import 'widgets/success_order_left_panel.dart';
|
import 'widgets/success_order_left_panel.dart';
|
||||||
import 'widgets/success_order_right_panel.dart';
|
import 'widgets/success_order_right_panel.dart';
|
||||||
|
|
||||||
@@ -20,47 +18,33 @@ class SuccessOrderPage extends StatelessWidget implements AutoRouteWrapper {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocListener<PrintStruckBloc, PrintStruckState>(
|
// Hasil cetak dilaporkan lewat dialog progres global di `AppWidget`.
|
||||||
listenWhen: (previous, current) =>
|
return Scaffold(
|
||||||
previous.failureOrPrintStruck != current.failureOrPrintStruck,
|
backgroundColor: AppColor.background,
|
||||||
listener: (context, state) {
|
body: SafeArea(
|
||||||
state.failureOrPrintStruck.fold(
|
child: BlocBuilder<OrderLoaderBloc, OrderLoaderState>(
|
||||||
() {},
|
builder: (context, state) {
|
||||||
(either) => either.fold(
|
if (state.isFetchingById) {
|
||||||
(f) => AppFlushbar.showPrinterFailureToast(context, f),
|
return const Center(child: LoaderWithText());
|
||||||
(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 Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.all(24.0),
|
padding: const EdgeInsets.all(24.0),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
flex: 35,
|
flex: 35,
|
||||||
child: SuccessOrderLeftPanel(order: state.order),
|
child: SuccessOrderLeftPanel(order: state.order),
|
||||||
),
|
),
|
||||||
const SpaceWidth(16),
|
const SpaceWidth(16),
|
||||||
Expanded(
|
Expanded(
|
||||||
flex: 65,
|
flex: 65,
|
||||||
child: SuccessOrderRightPanel(order: state.order),
|
child: SuccessOrderRightPanel(order: state.order),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -62,6 +62,11 @@ class OrderRightPanel extends StatelessWidget {
|
|||||||
color: AppColor.white,
|
color: AppColor.white,
|
||||||
onSelected: (value) {
|
onSelected: (value) {
|
||||||
switch (value) {
|
switch (value) {
|
||||||
|
case 'all':
|
||||||
|
context.read<PrintStruckBloc>().add(
|
||||||
|
PrintStruckEvent.order(state.selectedOrder!),
|
||||||
|
);
|
||||||
|
break;
|
||||||
case 'checker':
|
case 'checker':
|
||||||
context.read<PrintStruckBloc>().add(
|
context.read<PrintStruckBloc>().add(
|
||||||
PrintStruckEvent.checker(state.selectedOrder!),
|
PrintStruckEvent.checker(state.selectedOrder!),
|
||||||
@@ -79,7 +84,23 @@ class OrderRightPanel extends StatelessWidget {
|
|||||||
break;
|
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(
|
const PopupMenuItem(
|
||||||
value: 'checker',
|
value: 'checker',
|
||||||
child: Row(
|
child: Row(
|
||||||
|
|||||||
@@ -3,12 +3,10 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
import '../../../../../application/order/order_loader/order_loader_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 '../../../../../common/theme/theme.dart';
|
||||||
import '../../../../../injection.dart';
|
import '../../../../../injection.dart';
|
||||||
import '../../../../components/error/order_loader_error_state_widget.dart';
|
import '../../../../components/error/order_loader_error_state_widget.dart';
|
||||||
import '../../../../components/loader/loader_with_text.dart';
|
import '../../../../components/loader/loader_with_text.dart';
|
||||||
import '../../../../components/toast/flushbar.dart';
|
|
||||||
import 'widgets/payment_success_left_panel.dart';
|
import 'widgets/payment_success_left_panel.dart';
|
||||||
import 'widgets/payment_success_right_panel.dart';
|
import 'widgets/payment_success_right_panel.dart';
|
||||||
|
|
||||||
@@ -19,58 +17,46 @@ class PaymentSuccessPage extends StatelessWidget implements AutoRouteWrapper {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocListener<PrintStruckBloc, PrintStruckState>(
|
// Hasil cetak dilaporkan lewat dialog progres global di `AppWidget`.
|
||||||
listenWhen: (previous, current) =>
|
return Scaffold(
|
||||||
previous.failureOrPrintStruck != current.failureOrPrintStruck,
|
backgroundColor: AppColor.background,
|
||||||
listener: (context, state) {
|
body: SafeArea(
|
||||||
state.failureOrPrintStruck.fold(() {}, (either) {
|
child: BlocBuilder<OrderLoaderBloc, OrderLoaderState>(
|
||||||
either.fold((f) => AppFlushbar.showPrinterFailureToast(context, f), (
|
builder: (context, state) {
|
||||||
success,
|
if (state.isFetchingById) {
|
||||||
) {
|
return const Center(child: LoaderWithText());
|
||||||
AppFlushbar.showSuccess(context, "Struck berhasil dicetak");
|
}
|
||||||
});
|
return state.failureOptionGetById.fold(
|
||||||
});
|
() => Container(
|
||||||
},
|
padding: const EdgeInsets.all(24.0),
|
||||||
child: Scaffold(
|
child: Row(
|
||||||
backgroundColor: AppColor.background,
|
children: [
|
||||||
body: SafeArea(
|
// Left Panel - Success Message & Order Info
|
||||||
child: BlocBuilder<OrderLoaderBloc, OrderLoaderState>(
|
Expanded(
|
||||||
builder: (context, state) {
|
flex: 35,
|
||||||
if (state.isFetchingById) {
|
child: PaymentSuccessLeftPanel(order: state.order),
|
||||||
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
|
// Right Panel - Order Details
|
||||||
Expanded(
|
Expanded(
|
||||||
flex: 65,
|
flex: 65,
|
||||||
child: PaymentSuccessRightPanel(order: state.order),
|
child: PaymentSuccessRightPanel(order: state.order),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
|
||||||
),
|
),
|
||||||
(f) => OrderLoaderErrorStateWidget(
|
),
|
||||||
failure: f,
|
(f) => OrderLoaderErrorStateWidget(
|
||||||
onRefresh: () {
|
failure: f,
|
||||||
context.read<OrderLoaderBloc>().add(
|
onRefresh: () {
|
||||||
OrderLoaderEvent.getById(orderId),
|
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 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
import '../../../../../application/order/order_loader/order_loader_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 '../../../../../common/theme/theme.dart';
|
||||||
import '../../../../../domain/order/order.dart';
|
import '../../../../../domain/order/order.dart';
|
||||||
import '../../../../../injection.dart';
|
import '../../../../../injection.dart';
|
||||||
import '../../../../components/error/order_loader_error_state_widget.dart';
|
import '../../../../components/error/order_loader_error_state_widget.dart';
|
||||||
import '../../../../components/loader/loader_with_text.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_left_panel.dart';
|
||||||
import 'widgets/payment_success_split_bill_right_panel.dart';
|
import 'widgets/payment_success_split_bill_right_panel.dart';
|
||||||
|
|
||||||
@@ -26,64 +24,50 @@ class PaymentSuccessSplitBillPage extends StatelessWidget
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocListener<PrintStruckBloc, PrintStruckState>(
|
// Hasil cetak dilaporkan lewat dialog progres global di `AppWidget`.
|
||||||
listenWhen: (previous, current) =>
|
return Scaffold(
|
||||||
previous.failureOrPrintStruck != current.failureOrPrintStruck,
|
backgroundColor: AppColor.background,
|
||||||
listener: (context, state) {
|
body: SafeArea(
|
||||||
state.failureOrPrintStruck.fold(
|
child: BlocBuilder<OrderLoaderBloc, OrderLoaderState>(
|
||||||
() {},
|
builder: (context, state) {
|
||||||
(either) => either.fold(
|
if (state.isFetchingById) {
|
||||||
(f) => AppFlushbar.showPrinterFailureToast(context, f),
|
return const Center(child: LoaderWithText());
|
||||||
(success) {
|
}
|
||||||
AppFlushbar.showSuccess(context, "Struck berhasil dicetak");
|
return state.failureOptionGetById.fold(
|
||||||
},
|
() => Container(
|
||||||
),
|
padding: const EdgeInsets.all(24.0),
|
||||||
);
|
child: Row(
|
||||||
},
|
children: [
|
||||||
child: Scaffold(
|
// Left Panel - Success Message & Order Info
|
||||||
backgroundColor: AppColor.background,
|
Expanded(
|
||||||
body: SafeArea(
|
flex: 35,
|
||||||
child: BlocBuilder<OrderLoaderBloc, OrderLoaderState>(
|
child: PaymentSuccessSplitBillLeftPanel(
|
||||||
builder: (context, state) {
|
order: state.order.copyWith(orderItems: orderItems),
|
||||||
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
|
// Right Panel - Order Details
|
||||||
Expanded(
|
Expanded(
|
||||||
flex: 65,
|
flex: 65,
|
||||||
child: PaymentSuccessSplitBillRightPanel(
|
child: PaymentSuccessSplitBillRightPanel(
|
||||||
order: state.order.copyWith(orderItems: orderItems),
|
order: state.order.copyWith(orderItems: orderItems),
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
],
|
||||||
),
|
),
|
||||||
(f) => OrderLoaderErrorStateWidget(
|
),
|
||||||
failure: f,
|
(f) => OrderLoaderErrorStateWidget(
|
||||||
onRefresh: () {
|
failure: f,
|
||||||
context.read<OrderLoaderBloc>().add(
|
onRefresh: () {
|
||||||
OrderLoaderEvent.getById(orderId),
|
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/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.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 '../../../../../application/void/void_form/void_form_bloc.dart';
|
||||||
import '../../../../../common/theme/theme.dart';
|
import '../../../../../common/theme/theme.dart';
|
||||||
import '../../../../components/spaces/space.dart';
|
import '../../../../components/spaces/space.dart';
|
||||||
import '../../../../components/toast/flushbar.dart';
|
|
||||||
import 'widgets/void_success_left_panel.dart';
|
import 'widgets/void_success_left_panel.dart';
|
||||||
import 'widgets/void_success_right_panel.dart';
|
import 'widgets/void_success_right_panel.dart';
|
||||||
|
|
||||||
@@ -16,44 +14,30 @@ class VoidSuccessPage extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocListener<PrintStruckBloc, PrintStruckState>(
|
// Hasil cetak dilaporkan lewat dialog progres global di `AppWidget`.
|
||||||
listenWhen: (previous, current) =>
|
return Scaffold(
|
||||||
previous.failureOrPrintStruck != current.failureOrPrintStruck,
|
backgroundColor: AppColor.background,
|
||||||
listener: (context, state) {
|
body: BlocBuilder<VoidFormBloc, VoidFormState>(
|
||||||
state.failureOrPrintStruck.fold(
|
builder: (context, state) {
|
||||||
() {},
|
return SafeArea(
|
||||||
(either) => either.fold(
|
child: Padding(
|
||||||
(f) => AppFlushbar.showPrinterFailureToast(context, f),
|
padding: const EdgeInsets.all(24.0),
|
||||||
(success) {
|
child: Row(
|
||||||
AppFlushbar.showSuccess(context, "Struck berhasil dicetak");
|
children: [
|
||||||
},
|
Expanded(
|
||||||
),
|
flex: 35,
|
||||||
);
|
child: VoidSuccessLeftPanel(state: state),
|
||||||
},
|
),
|
||||||
child: Scaffold(
|
const SpaceWidth(16),
|
||||||
backgroundColor: AppColor.background,
|
Expanded(
|
||||||
body: BlocBuilder<VoidFormBloc, VoidFormState>(
|
flex: 65,
|
||||||
builder: (context, state) {
|
child: VoidSuccessRightPanel(state: 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
@@ -852,10 +852,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: meta
|
name: meta
|
||||||
sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
|
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.18.0"
|
version: "1.17.0"
|
||||||
mime:
|
mime:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1337,10 +1337,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: test_api
|
name: test_api
|
||||||
sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
|
sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.11"
|
version: "0.7.10"
|
||||||
time:
|
time:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1502,5 +1502,5 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.3"
|
version: "3.1.3"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.10.0-0 <4.0.0"
|
dart: ">=3.9.0-0 <4.0.0"
|
||||||
flutter: ">=3.29.0"
|
flutter: ">=3.29.0"
|
||||||
|
|||||||
+1
-1
@@ -3,7 +3,7 @@ description: "A new Flutter project."
|
|||||||
|
|
||||||
publish_to: "none"
|
publish_to: "none"
|
||||||
|
|
||||||
version: 1.0.11+17
|
version: 1.0.12+19
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.8.1
|
sdk: ^3.8.1
|
||||||
|
|||||||
Reference in New Issue
Block a user