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/
|
||||
/build/
|
||||
android/build/
|
||||
|
||||
# Symbolication related
|
||||
app.*.symbols
|
||||
@@ -43,3 +44,8 @@ app.*.map.json
|
||||
/android/app/debug
|
||||
/android/app/profile
|
||||
/android/app/release
|
||||
|
||||
# FVM - versi Flutter dipin di .fvmrc (ikut di-commit), SDK-nya tidak
|
||||
|
||||
# FVM Version Cache
|
||||
.fvm/
|
||||
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 |
|
||||
|---|---|
|
||||
| Version Manager | `fvm` — Flutter dipin di `.fvmrc` |
|
||||
| State Management | `flutter_bloc` + `bloc` |
|
||||
| Dependency Injection | `get_it` + `injectable` |
|
||||
| Navigation | `auto_route` |
|
||||
@@ -78,11 +79,13 @@ feature/
|
||||
|
||||
### Prasyarat
|
||||
|
||||
- Flutter SDK `^3.8.1`
|
||||
- Dart SDK `^3.8.1`
|
||||
- **FVM** — project ini mengunci versi Flutter, jangan pakai Flutter global
|
||||
- **Flutter 3.41.9** (Dart 3.11.5) — dipin di `.fvmrc`
|
||||
- Android SDK / Xcode (untuk iOS)
|
||||
- Firebase project yang sudah dikonfigurasi
|
||||
|
||||
> **Kenapa versinya dikunci?** Stack code generation project ini (`freezed` 2.x, `build_runner` 2.5, `analyzer` 7.7.1) hanya paham bahasa Dart sampai 3.9. Flutter 3.44 ke atas membawa framework yang ditulis di bahasa 3.10+, dan `analyzer` 7.7.1 crash saat membacanya. Selama stack codegen belum dinaikkan, **Flutter 3.41.9 adalah versi terbaru yang masih bisa menjalankan `build_runner`.** Lihat [Troubleshooting](#troubleshooting).
|
||||
|
||||
### Instalasi
|
||||
|
||||
```bash
|
||||
@@ -90,44 +93,118 @@ feature/
|
||||
git clone <repo-url>
|
||||
cd apskel-pos-flutter-v2
|
||||
|
||||
# Install dependencies
|
||||
flutter pub get
|
||||
# Pasang FVM (sekali per mesin)
|
||||
dart pub global activate fvm
|
||||
|
||||
# Generate kode (freezed, injectable, auto_route, json_serializable)
|
||||
dart run build_runner build --delete-conflicting-outputs
|
||||
# Ambil & pakai versi Flutter yang dipin di .fvmrc
|
||||
fvm install
|
||||
fvm use
|
||||
```
|
||||
|
||||
`dart pub global activate` memasang `fvm` ke folder bin milik pub yang **belum tentu ada di PATH**. Kalau muncul `fvm: command not found` / `The term 'fvm' is not recognized`, tambahkan foldernya sekali saja:
|
||||
|
||||
```powershell
|
||||
# Windows PowerShell — permanen, buka terminal baru setelahnya
|
||||
[Environment]::SetEnvironmentVariable('Path', [Environment]::GetEnvironmentVariable('Path','User') + ";$env:LOCALAPPDATA\Pub\Cache\bin", 'User')
|
||||
|
||||
# Untuk terminal yang sedang terbuka
|
||||
$env:Path += ";$env:LOCALAPPDATA\Pub\Cache\bin"
|
||||
```
|
||||
|
||||
```bash
|
||||
# macOS / Linux — tambahkan ke ~/.zshrc atau ~/.bashrc
|
||||
export PATH="$PATH:$HOME/.pub-cache/bin"
|
||||
```
|
||||
|
||||
Setelah `fvm` bisa dipanggil, lanjutkan:
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
fvm flutter pub get
|
||||
|
||||
# Generate kode
|
||||
fvm dart run build_runner build
|
||||
```
|
||||
|
||||
> Semua perintah Flutter/Dart di project ini **harus diawali `fvm`**. `flutter run` tanpa `fvm` memakai Flutter global dan hasilnya bisa berbeda.
|
||||
|
||||
### Menjalankan App
|
||||
|
||||
Environment **tidak** ditentukan otomatis dari debug/release — harus dioper lewat `--dart-define`:
|
||||
|
||||
```bash
|
||||
# Development
|
||||
flutter run
|
||||
fvm flutter run --dart-define=ENV=dev
|
||||
|
||||
# Release
|
||||
flutter run --release
|
||||
# Production
|
||||
fvm flutter run --dart-define=ENV=prod
|
||||
|
||||
# Production, release mode
|
||||
fvm flutter run --release --dart-define=ENV=prod
|
||||
```
|
||||
|
||||
App otomatis menggunakan environment `dev` saat debug dan `prod` saat release.
|
||||
Kalau `ENV` tidak dioper, nilainya jatuh ke `dev` (lihat `defaultValue` di `lib/main.dart`).
|
||||
|
||||
Di VSCode sudah tersedia konfigurasi **Dev** dan **Prod** di `.vscode/launch.json`, tinggal pilih di panel Run and Debug.
|
||||
|
||||
### Build
|
||||
|
||||
`--dart-define=ENV=prod` wajib ikut, kalau tidak APK-nya akan memakai database dan base URL milik dev:
|
||||
|
||||
```bash
|
||||
# APK
|
||||
fvm flutter build apk --release --dart-define=ENV=prod
|
||||
|
||||
# App Bundle (Play Store)
|
||||
fvm flutter build appbundle --release --dart-define=ENV=prod
|
||||
|
||||
# iOS
|
||||
fvm flutter build ipa --release --dart-define=ENV=prod
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Code Generation
|
||||
|
||||
Project ini banyak memakai kode generated (`freezed`, `injectable`, `auto_route`, `json_serializable`, `flutter_gen`). Kode itu **wajib di-generate ulang** setiap kali kamu:
|
||||
|
||||
- menambah/mengubah kelas `@freezed` (entity, event, state, DTO)
|
||||
- menambah/mengubah anotasi DI (`@injectable`, `@LazySingleton`, `@Injectable(as: ...)`)
|
||||
- menambah halaman baru dengan `@RoutePage()`
|
||||
- menambah aset di `pubspec.yaml`
|
||||
|
||||
```bash
|
||||
# Sekali jalan
|
||||
fvm dart run build_runner build
|
||||
|
||||
# Mode watch — regenerate otomatis saat file berubah, enak dipakai saat ngoding
|
||||
fvm dart run build_runner watch
|
||||
```
|
||||
|
||||
> **Hindari `--delete-conflicting-outputs`.** Flag itu menghapus seluruh file generated **sebelum** mulai build. Kalau build-nya gagal di tengah jalan, kamu berakhir tanpa kode generated sama sekali dan project tidak bisa di-compile. Pakai hanya kalau build_runner benar-benar mengeluh soal konflik output, dan pastikan working tree-mu bersih dulu supaya bisa `git restore`.
|
||||
|
||||
---
|
||||
|
||||
## Environment
|
||||
|
||||
Konfigurasi environment ada di `lib/env.dart`:
|
||||
Konfigurasi environment ada di `lib/env.dart` — **itu sumber kebenarannya**, jangan disalin ke sini karena gampang basi.
|
||||
|
||||
| Environment | Base URL | Database |
|
||||
|---|---|---|
|
||||
| `dev` | `https://api-pos.apskel.id` | `apskel_pos_dev.db` |
|
||||
| `prod` | `https://api-pos.apskel.id` | `apskel_pos_dev.db` |
|
||||
| Environment | Database |
|
||||
|---|---|
|
||||
| `dev` | `apskel_pos_staging.db` |
|
||||
| `prod` | `apskel_pos_prod.db` |
|
||||
|
||||
Environment dipilih otomatis di `main.dart`:
|
||||
Environment dipilih dari `--dart-define=ENV=...` di `lib/main.dart`:
|
||||
|
||||
```dart
|
||||
await configureDependencies(
|
||||
kReleaseMode ? Environment.prod : Environment.dev,
|
||||
);
|
||||
const String env = String.fromEnvironment('ENV', defaultValue: 'dev');
|
||||
await configureDependencies(env);
|
||||
```
|
||||
|
||||
Nilai `env` diteruskan ke `injectable`, yang lalu memilih antara `DevEnv` (`@dev`) dan `ProdEnv` (`@prod`). Menambah environment baru berarti menambah kelas ber-anotasi di `lib/env.dart`, lalu generate ulang.
|
||||
|
||||
Karena `dev` dan `prod` memakai **nama database berbeda**, berpindah environment di perangkat yang sama tidak akan mencampur datanya.
|
||||
|
||||
---
|
||||
|
||||
## Dependency Injection
|
||||
@@ -210,6 +287,43 @@ App dikunci ke mode **landscape** (kiri & kanan) karena didesain untuk tablet PO
|
||||
|
||||
## Catatan Development
|
||||
|
||||
- Selalu pakai `fvm flutter` / `fvm dart`, bukan `flutter` / `dart` global
|
||||
- Jangan edit `injection.config.dart` dan file `*.freezed.dart` / `*.g.dart` secara manual — file tersebut di-generate otomatis
|
||||
- Setelah mengubah kelas `@freezed` atau anotasi DI, jalankan build_runner sebelum menjalankan app
|
||||
- Gunakan `log()` dari `dart:developer` untuk logging, bukan `print()`
|
||||
- `print()` dinonaktifkan di release mode
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### `build_runner` gagal: `Missing implementation of visitDotShorthandPropertyAccess`
|
||||
|
||||
Kamu sedang memakai Flutter yang terlalu baru. Cek:
|
||||
|
||||
```bash
|
||||
fvm flutter --version # harus 3.41.9
|
||||
flutter --version # ini Flutter global, boleh versi apa saja
|
||||
```
|
||||
|
||||
Kalau versinya bukan 3.41.9, artinya perintahnya jalan tanpa `fvm`. Log build juga memberi petunjuk yang sama:
|
||||
|
||||
```
|
||||
SDK language version 3.13.0 is newer than `analyzer` language version 3.9.0
|
||||
```
|
||||
|
||||
Perbaikannya: `fvm use`, lalu ulangi dengan awalan `fvm`.
|
||||
|
||||
Menaikkan Flutter melewati 3.41.x mengharuskan seluruh stack codegen ikut naik — `freezed` 2 → 3/4, `build_runner` → 2.16, `injectable_generator` 2 → 3, `auto_route` 9 → 10. `freezed` 3 mewajibkan tiap kelas dideklarasikan `abstract`/`sealed` dan menghapus `when`/`map` (diganti pattern matching), jadi ini pekerjaan tersendiri, bukan sambilan.
|
||||
|
||||
### Project tidak bisa di-compile, banyak error `_$Something` tidak ditemukan
|
||||
|
||||
Kode generated-nya hilang atau belum dibuat. Jalankan:
|
||||
|
||||
```bash
|
||||
fvm dart run build_runner build
|
||||
```
|
||||
|
||||
### `fvm: command not found`
|
||||
|
||||
FVM terpasang di `~/AppData/Local/Pub/Cache/bin` (Windows) atau `~/.pub-cache/bin` (macOS/Linux) yang mungkin belum ada di `PATH`. Tambahkan direktori itu ke `PATH`, atau panggil dengan path lengkap.
|
||||
|
||||
@@ -21,3 +21,10 @@ analyzer:
|
||||
- test/generated/**
|
||||
- "**/**.g.dart"
|
||||
- "**/**.freezed.dart"
|
||||
- build/**
|
||||
- android/**
|
||||
- ios/**
|
||||
- web/**
|
||||
- windows/**
|
||||
- macos/**
|
||||
- linux/**
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:dartz/dartz.dart' hide Order;
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:injectable/injectable.dart' hide Order;
|
||||
|
||||
@@ -21,136 +20,63 @@ class PrintStruckBloc extends Bloc<PrintStruckEvent, PrintStruckState> {
|
||||
Future<void> _onPrintStruckEvent(
|
||||
PrintStruckEvent event,
|
||||
Emitter<PrintStruckState> emit,
|
||||
) {
|
||||
return event.map(
|
||||
order: (e) async {
|
||||
Either<PrinterFailure, Unit> failureOrSuccess;
|
||||
) => event.map(
|
||||
order: (e) => _print(emit, e.order, _stationsForOrder(e.order)),
|
||||
payment: (e) => _print(emit, e.order, const [PrintStation.receipt]),
|
||||
cashier: (e) => _print(emit, e.order, const [PrintStation.cashier]),
|
||||
checker: (e) => _print(emit, e.order, const [PrintStation.checker]),
|
||||
kitchen: (e) => _print(emit, e.order, const [PrintStation.kitchen]),
|
||||
bar: (e) => _print(emit, e.order, const [PrintStation.bar]),
|
||||
voided: (e) => _print(emit, e.order, const [PrintStation.voided]),
|
||||
splitBill: (e) => _print(emit, e.order, const [PrintStation.splitBill]),
|
||||
retryFailed: (_) {
|
||||
final order = state.order;
|
||||
final failed = [
|
||||
for (final station in state.failedStations) station.station,
|
||||
];
|
||||
|
||||
emit(state.copyWith(isPrinting: true, failureOrPrintStruck: none()));
|
||||
if (order == null || failed.isEmpty) return Future.value();
|
||||
|
||||
failureOrSuccess = await _printerRepository.printStruckOrder(
|
||||
order: e.order,
|
||||
);
|
||||
return _print(emit, order, failed);
|
||||
},
|
||||
);
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
isPrinting: false,
|
||||
failureOrPrintStruck: optionOf(failureOrSuccess),
|
||||
),
|
||||
);
|
||||
},
|
||||
cashier: (e) async {
|
||||
Either<PrinterFailure, Unit> failureOrSuccess;
|
||||
/// Struk pelanggan hanya ikut dicetak kalau ordernya sudah dibayar.
|
||||
List<PrintStation> _stationsForOrder(Order order) => [
|
||||
if (order.totalPaid > 0) PrintStation.receipt,
|
||||
PrintStation.cashier,
|
||||
PrintStation.checker,
|
||||
PrintStation.kitchen,
|
||||
PrintStation.bar,
|
||||
];
|
||||
|
||||
emit(state.copyWith(isPrinting: true, failureOrPrintStruck: none()));
|
||||
Future<void> _print(
|
||||
Emitter<PrintStruckState> emit,
|
||||
Order order,
|
||||
List<PrintStation> stations,
|
||||
) async {
|
||||
// Tombol yang tertekan dua kali tidak boleh memulai job kedua — repository
|
||||
// akan mengantrikannya dan progres yang tampil jadi membingungkan.
|
||||
if (state.isPrinting) return;
|
||||
|
||||
failureOrSuccess = await _printerRepository.printStruckCashier(
|
||||
order: e.order,
|
||||
);
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
isPrinting: false,
|
||||
failureOrPrintStruck: optionOf(failureOrSuccess),
|
||||
),
|
||||
);
|
||||
},
|
||||
payment: (e) async {
|
||||
Either<PrinterFailure, Unit> failureOrSuccess;
|
||||
|
||||
emit(state.copyWith(isPrinting: true, failureOrPrintStruck: none()));
|
||||
|
||||
failureOrSuccess = await _printerRepository.printStruckPayment(
|
||||
order: e.order,
|
||||
);
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
isPrinting: false,
|
||||
failureOrPrintStruck: optionOf(failureOrSuccess),
|
||||
),
|
||||
);
|
||||
},
|
||||
voided: (e) async {
|
||||
Either<PrinterFailure, Unit> failureOrSuccess;
|
||||
|
||||
emit(state.copyWith(isPrinting: true, failureOrPrintStruck: none()));
|
||||
|
||||
failureOrSuccess = await _printerRepository.printStruckVoid(
|
||||
order: e.order,
|
||||
);
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
isPrinting: false,
|
||||
failureOrPrintStruck: optionOf(failureOrSuccess),
|
||||
),
|
||||
);
|
||||
},
|
||||
splitBill: (e) async {
|
||||
Either<PrinterFailure, Unit> failureOrSuccess;
|
||||
|
||||
emit(state.copyWith(isPrinting: true, failureOrPrintStruck: none()));
|
||||
|
||||
failureOrSuccess = await _printerRepository.printStruckSplitBill(
|
||||
order: e.order,
|
||||
);
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
isPrinting: false,
|
||||
failureOrPrintStruck: optionOf(failureOrSuccess),
|
||||
),
|
||||
);
|
||||
},
|
||||
kitchen: (e) async {
|
||||
Either<PrinterFailure, Unit> failureOrSuccess;
|
||||
|
||||
emit(state.copyWith(isPrinting: true, failureOrPrintStruck: none()));
|
||||
|
||||
failureOrSuccess = await _printerRepository.printStruckKitchen(
|
||||
order: e.order,
|
||||
);
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
isPrinting: false,
|
||||
failureOrPrintStruck: optionOf(failureOrSuccess),
|
||||
),
|
||||
);
|
||||
},
|
||||
bar: (e) async {
|
||||
Either<PrinterFailure, Unit> failureOrSuccess;
|
||||
|
||||
emit(state.copyWith(isPrinting: true, failureOrPrintStruck: none()));
|
||||
|
||||
failureOrSuccess = await _printerRepository.printStruckBar(
|
||||
order: e.order,
|
||||
);
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
isPrinting: false,
|
||||
failureOrPrintStruck: optionOf(failureOrSuccess),
|
||||
),
|
||||
);
|
||||
},
|
||||
checker: (e) async {
|
||||
Either<PrinterFailure, Unit> failureOrSuccess;
|
||||
|
||||
emit(state.copyWith(isPrinting: true, failureOrPrintStruck: none()));
|
||||
|
||||
failureOrSuccess = await _printerRepository.printStruckChecker(
|
||||
order: e.order,
|
||||
);
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
isPrinting: false,
|
||||
failureOrPrintStruck: optionOf(failureOrSuccess),
|
||||
),
|
||||
);
|
||||
},
|
||||
emit(
|
||||
state.copyWith(
|
||||
isPrinting: true,
|
||||
order: order,
|
||||
jobId: state.jobId + 1,
|
||||
stations: [
|
||||
for (final station in stations) PrintStationResult.pending(station),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
await emit.forEach<List<PrintStationResult>>(
|
||||
_printerRepository.printStations(order: order, stations: stations),
|
||||
onData: (results) => state.copyWith(stations: results),
|
||||
);
|
||||
|
||||
// Cetak bisa selesai setelah bloc ditutup (kasir keluar dari aplikasi).
|
||||
if (isClosed) return;
|
||||
emit(state.copyWith(isPrinting: false));
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,9 @@ part of 'print_struck_bloc.dart';
|
||||
|
||||
@freezed
|
||||
class PrintStruckEvent with _$PrintStruckEvent {
|
||||
/// Cetak seluruh station untuk satu order.
|
||||
const factory PrintStruckEvent.order(Order order) = _Order;
|
||||
|
||||
const factory PrintStruckEvent.cashier(Order order) = _Cashier;
|
||||
const factory PrintStruckEvent.payment(Order order) = _Payment;
|
||||
const factory PrintStruckEvent.voided(Order order) = _Voided;
|
||||
@@ -10,4 +12,8 @@ class PrintStruckEvent with _$PrintStruckEvent {
|
||||
const factory PrintStruckEvent.kitchen(Order order) = _Kitchen;
|
||||
const factory PrintStruckEvent.bar(Order order) = _Bar;
|
||||
const factory PrintStruckEvent.splitBill(Order order) = _SplitBill;
|
||||
|
||||
/// Ulangi hanya station yang gagal di job terakhir, tanpa mencetak ulang
|
||||
/// station yang sudah keluar.
|
||||
const factory PrintStruckEvent.retryFailed() = _RetryFailed;
|
||||
}
|
||||
|
||||
@@ -2,11 +2,33 @@ part of 'print_struck_bloc.dart';
|
||||
|
||||
@freezed
|
||||
class PrintStruckState with _$PrintStruckState {
|
||||
factory PrintStruckState({
|
||||
required Option<Either<PrinterFailure, Unit>> failureOrPrintStruck,
|
||||
const PrintStruckState._();
|
||||
|
||||
const factory PrintStruckState({
|
||||
/// Status seluruh station di job cetak terakhir, urut sesuai urutan cetak.
|
||||
@Default(<PrintStationResult>[]) List<PrintStationResult> stations,
|
||||
@Default(false) bool isPrinting,
|
||||
|
||||
/// Order yang sedang dicetak, disimpan supaya station yang gagal bisa
|
||||
/// diulang tanpa perlu dikirim ulang dari halaman pemanggil.
|
||||
Order? order,
|
||||
|
||||
/// Naik tiap job baru dimulai. UI memakainya untuk tahu kapan harus
|
||||
/// membuka dialog progres.
|
||||
@Default(0) int jobId,
|
||||
}) = _PrintStruckState;
|
||||
|
||||
factory PrintStruckState.initial() =>
|
||||
PrintStruckState(failureOrPrintStruck: none());
|
||||
factory PrintStruckState.initial() => const PrintStruckState();
|
||||
|
||||
List<PrintStationResult> get failedStations =>
|
||||
stations.where((station) => station.isFailed).toList();
|
||||
|
||||
bool get hasFailure => failedStations.isNotEmpty;
|
||||
|
||||
/// Station yang benar-benar dicetak — yang dilewati tidak ikut dihitung.
|
||||
int get plannedCount =>
|
||||
stations.where((station) => !station.isSkipped).length;
|
||||
|
||||
int get successCount =>
|
||||
stations.where((station) => station.isSuccess).length;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
part of '../printer.dart';
|
||||
|
||||
/// Satu tujuan cetak dalam sekali proses print.
|
||||
///
|
||||
/// Perhatikan kasir, void, dan split bill sengaja menumpang printer `receipt`
|
||||
/// — ketiganya tidak punya printer sendiri di halaman pengaturan.
|
||||
enum PrintStation {
|
||||
receipt,
|
||||
cashier,
|
||||
checker,
|
||||
kitchen,
|
||||
bar,
|
||||
voided,
|
||||
splitBill;
|
||||
|
||||
String get label => switch (this) {
|
||||
PrintStation.receipt => 'Struk Customer',
|
||||
PrintStation.cashier => 'Struk Kasir',
|
||||
PrintStation.checker => 'Checker',
|
||||
PrintStation.kitchen => 'Kitchen',
|
||||
PrintStation.bar => 'Bar',
|
||||
PrintStation.voided => 'Struk Void',
|
||||
PrintStation.splitBill => 'Struk Split Bill',
|
||||
};
|
||||
|
||||
/// Kode printer di tabel `printers`.
|
||||
String get printerCode => switch (this) {
|
||||
PrintStation.receipt ||
|
||||
PrintStation.cashier ||
|
||||
PrintStation.voided ||
|
||||
PrintStation.splitBill => 'receipt',
|
||||
PrintStation.checker => 'checker',
|
||||
PrintStation.kitchen => 'kitchen',
|
||||
PrintStation.bar => 'bar',
|
||||
};
|
||||
|
||||
/// Station yang hanya mencetak item dengan `printerType` tertentu.
|
||||
/// Station lain mencetak seluruh item order.
|
||||
String? get itemPrinterType => switch (this) {
|
||||
PrintStation.kitchen => 'kitchen',
|
||||
PrintStation.bar => 'bar',
|
||||
_ => null,
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
enum PrintStationStatus {
|
||||
/// Belum dijalankan.
|
||||
pending,
|
||||
|
||||
/// Sedang dikirim ke printer.
|
||||
printing,
|
||||
|
||||
/// Berhasil dicetak.
|
||||
success,
|
||||
|
||||
/// Gagal — lihat `message`.
|
||||
failed,
|
||||
|
||||
/// Sengaja dilewati, misal tidak ada item untuk station tersebut.
|
||||
/// Ini bukan kegagalan dan tidak perlu dicetak ulang.
|
||||
skipped,
|
||||
}
|
||||
|
||||
@freezed
|
||||
class PrintStationResult with _$PrintStationResult {
|
||||
const PrintStationResult._();
|
||||
|
||||
const factory PrintStationResult({
|
||||
required PrintStation station,
|
||||
required PrintStationStatus status,
|
||||
String? message,
|
||||
@Default(Duration.zero) Duration elapsed,
|
||||
}) = _PrintStationResult;
|
||||
|
||||
factory PrintStationResult.pending(PrintStation station) =>
|
||||
PrintStationResult(station: station, status: PrintStationStatus.pending);
|
||||
|
||||
bool get isSuccess => status == PrintStationStatus.success;
|
||||
bool get isFailed => status == PrintStationStatus.failed;
|
||||
bool get isSkipped => status == PrintStationStatus.skipped;
|
||||
bool get isFinished => isSuccess || isFailed || isSkipped;
|
||||
}
|
||||
@@ -9,5 +9,6 @@ import '../order/order.dart';
|
||||
part 'printer.freezed.dart';
|
||||
|
||||
part 'entities/printer_entity.dart';
|
||||
part 'entities/print_station_result.dart';
|
||||
part 'failures/printer_failure.dart';
|
||||
part 'repositories/i_printer_repository.dart';
|
||||
|
||||
@@ -307,6 +307,216 @@ abstract class _Printer implements Printer {
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
mixin _$PrintStationResult {
|
||||
PrintStation get station => throw _privateConstructorUsedError;
|
||||
PrintStationStatus get status => throw _privateConstructorUsedError;
|
||||
String? get message => throw _privateConstructorUsedError;
|
||||
Duration get elapsed => throw _privateConstructorUsedError;
|
||||
|
||||
/// Create a copy of PrintStationResult
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
$PrintStationResultCopyWith<PrintStationResult> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $PrintStationResultCopyWith<$Res> {
|
||||
factory $PrintStationResultCopyWith(
|
||||
PrintStationResult value,
|
||||
$Res Function(PrintStationResult) then,
|
||||
) = _$PrintStationResultCopyWithImpl<$Res, PrintStationResult>;
|
||||
@useResult
|
||||
$Res call({
|
||||
PrintStation station,
|
||||
PrintStationStatus status,
|
||||
String? message,
|
||||
Duration elapsed,
|
||||
});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$PrintStationResultCopyWithImpl<$Res, $Val extends PrintStationResult>
|
||||
implements $PrintStationResultCopyWith<$Res> {
|
||||
_$PrintStationResultCopyWithImpl(this._value, this._then);
|
||||
|
||||
// ignore: unused_field
|
||||
final $Val _value;
|
||||
// ignore: unused_field
|
||||
final $Res Function($Val) _then;
|
||||
|
||||
/// Create a copy of PrintStationResult
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? station = null,
|
||||
Object? status = null,
|
||||
Object? message = freezed,
|
||||
Object? elapsed = null,
|
||||
}) {
|
||||
return _then(
|
||||
_value.copyWith(
|
||||
station: null == station
|
||||
? _value.station
|
||||
: station // ignore: cast_nullable_to_non_nullable
|
||||
as PrintStation,
|
||||
status: null == status
|
||||
? _value.status
|
||||
: status // ignore: cast_nullable_to_non_nullable
|
||||
as PrintStationStatus,
|
||||
message: freezed == message
|
||||
? _value.message
|
||||
: message // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
elapsed: null == elapsed
|
||||
? _value.elapsed
|
||||
: elapsed // ignore: cast_nullable_to_non_nullable
|
||||
as Duration,
|
||||
)
|
||||
as $Val,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class _$$PrintStationResultImplCopyWith<$Res>
|
||||
implements $PrintStationResultCopyWith<$Res> {
|
||||
factory _$$PrintStationResultImplCopyWith(
|
||||
_$PrintStationResultImpl value,
|
||||
$Res Function(_$PrintStationResultImpl) then,
|
||||
) = __$$PrintStationResultImplCopyWithImpl<$Res>;
|
||||
@override
|
||||
@useResult
|
||||
$Res call({
|
||||
PrintStation station,
|
||||
PrintStationStatus status,
|
||||
String? message,
|
||||
Duration elapsed,
|
||||
});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$$PrintStationResultImplCopyWithImpl<$Res>
|
||||
extends _$PrintStationResultCopyWithImpl<$Res, _$PrintStationResultImpl>
|
||||
implements _$$PrintStationResultImplCopyWith<$Res> {
|
||||
__$$PrintStationResultImplCopyWithImpl(
|
||||
_$PrintStationResultImpl _value,
|
||||
$Res Function(_$PrintStationResultImpl) _then,
|
||||
) : super(_value, _then);
|
||||
|
||||
/// Create a copy of PrintStationResult
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? station = null,
|
||||
Object? status = null,
|
||||
Object? message = freezed,
|
||||
Object? elapsed = null,
|
||||
}) {
|
||||
return _then(
|
||||
_$PrintStationResultImpl(
|
||||
station: null == station
|
||||
? _value.station
|
||||
: station // ignore: cast_nullable_to_non_nullable
|
||||
as PrintStation,
|
||||
status: null == status
|
||||
? _value.status
|
||||
: status // ignore: cast_nullable_to_non_nullable
|
||||
as PrintStationStatus,
|
||||
message: freezed == message
|
||||
? _value.message
|
||||
: message // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
elapsed: null == elapsed
|
||||
? _value.elapsed
|
||||
: elapsed // ignore: cast_nullable_to_non_nullable
|
||||
as Duration,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
|
||||
class _$PrintStationResultImpl extends _PrintStationResult {
|
||||
const _$PrintStationResultImpl({
|
||||
required this.station,
|
||||
required this.status,
|
||||
this.message,
|
||||
this.elapsed = Duration.zero,
|
||||
}) : super._();
|
||||
|
||||
@override
|
||||
final PrintStation station;
|
||||
@override
|
||||
final PrintStationStatus status;
|
||||
@override
|
||||
final String? message;
|
||||
@override
|
||||
@JsonKey()
|
||||
final Duration elapsed;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'PrintStationResult(station: $station, status: $status, message: $message, elapsed: $elapsed)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$PrintStationResultImpl &&
|
||||
(identical(other.station, station) || other.station == station) &&
|
||||
(identical(other.status, status) || other.status == status) &&
|
||||
(identical(other.message, message) || other.message == message) &&
|
||||
(identical(other.elapsed, elapsed) || other.elapsed == elapsed));
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
Object.hash(runtimeType, station, status, message, elapsed);
|
||||
|
||||
/// Create a copy of PrintStationResult
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
_$$PrintStationResultImplCopyWith<_$PrintStationResultImpl> get copyWith =>
|
||||
__$$PrintStationResultImplCopyWithImpl<_$PrintStationResultImpl>(
|
||||
this,
|
||||
_$identity,
|
||||
);
|
||||
}
|
||||
|
||||
abstract class _PrintStationResult extends PrintStationResult {
|
||||
const factory _PrintStationResult({
|
||||
required final PrintStation station,
|
||||
required final PrintStationStatus status,
|
||||
final String? message,
|
||||
final Duration elapsed,
|
||||
}) = _$PrintStationResultImpl;
|
||||
const _PrintStationResult._() : super._();
|
||||
|
||||
@override
|
||||
PrintStation get station;
|
||||
@override
|
||||
PrintStationStatus get status;
|
||||
@override
|
||||
String? get message;
|
||||
@override
|
||||
Duration get elapsed;
|
||||
|
||||
/// Create a copy of PrintStationResult
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
_$$PrintStationResultImplCopyWith<_$PrintStationResultImpl> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
mixin _$PrinterFailure {
|
||||
@optionalTypeArgs
|
||||
|
||||
@@ -15,25 +15,17 @@ abstract class IPrinterRepository {
|
||||
List<int> printValue,
|
||||
);
|
||||
|
||||
Future<Either<PrinterFailure, Unit>> printStruckOrder({required Order order});
|
||||
Future<Either<PrinterFailure, Unit>> printStruckSaveOrder({
|
||||
required Order order,
|
||||
});
|
||||
Future<Either<PrinterFailure, Unit>> printStruckCashier({
|
||||
required Order order,
|
||||
});
|
||||
Future<Either<PrinterFailure, Unit>> printStruckPayment({
|
||||
required Order order,
|
||||
});
|
||||
Future<Either<PrinterFailure, Unit>> printStruckChecker({
|
||||
required Order order,
|
||||
});
|
||||
Future<Either<PrinterFailure, Unit>> printStruckKitchen({
|
||||
required Order order,
|
||||
});
|
||||
Future<Either<PrinterFailure, Unit>> printStruckBar({required Order order});
|
||||
Future<Either<PrinterFailure, Unit>> printStruckVoid({required Order order});
|
||||
Future<Either<PrinterFailure, Unit>> printStruckSplitBill({
|
||||
/// Mencetak [stations] untuk [order], melaporkan status tiap station.
|
||||
///
|
||||
/// Setiap emit berisi daftar lengkap semua station, jadi UI cukup
|
||||
/// menggambar ulang apa adanya.
|
||||
///
|
||||
/// Satu station yang gagal **tidak** membatalkan station lainnya — semuanya
|
||||
/// tetap dicoba. Station Bluetooth dijalankan berurutan karena radionya
|
||||
/// hanya satu, station Network dijalankan paralel karena tidak berebut
|
||||
/// sumber daya dengan Bluetooth.
|
||||
Stream<List<PrintStationResult>> printStations({
|
||||
required Order order,
|
||||
required List<PrintStation> stations,
|
||||
});
|
||||
}
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ abstract class Env {
|
||||
@dev
|
||||
class DevEnv implements Env {
|
||||
@override
|
||||
String get baseUrl => 'http://194.233.78.1:4001';
|
||||
String get baseUrl => 'https://enaklo-pos-api.altru.id';
|
||||
|
||||
@override
|
||||
String get dbName => "apskel_pos_staging.db";
|
||||
|
||||
@@ -0,0 +1,271 @@
|
||||
import 'dart:async';
|
||||
import 'dart:developer';
|
||||
import 'dart:io';
|
||||
|
||||
/// Kenapa sebuah cetakan jaringan dianggap gagal.
|
||||
enum NetworkPrintFailure {
|
||||
/// Socket tidak bisa dibuka — IP salah, printer mati, atau beda jaringan.
|
||||
unreachable,
|
||||
|
||||
/// Printer menjawab bahwa kertasnya habis.
|
||||
paperOut,
|
||||
|
||||
/// Printer menjawab bahwa cover-nya terbuka.
|
||||
coverOpen,
|
||||
|
||||
/// Printer menjawab sedang offline / dalam kondisi error.
|
||||
offline,
|
||||
|
||||
/// Koneksi terbuka tapi pengiriman data putus di tengah jalan.
|
||||
writeFailed,
|
||||
}
|
||||
|
||||
class NetworkPrintResult {
|
||||
final NetworkPrintFailure? failure;
|
||||
|
||||
/// Printer membalas perintah status. Printer clone yang tidak mendukung
|
||||
/// `DLE EOT` membuat ini `false`, dan pemeriksaan kondisi kertas dilewati.
|
||||
final bool statusSupported;
|
||||
|
||||
/// Kertas menipis. Bukan kegagalan — cetakan tetap keluar, tapi layak
|
||||
/// diberitahukan ke kasir.
|
||||
final bool paperNearEnd;
|
||||
|
||||
const NetworkPrintResult({
|
||||
this.failure,
|
||||
this.statusSupported = false,
|
||||
this.paperNearEnd = false,
|
||||
});
|
||||
|
||||
bool get isSuccess => failure == null;
|
||||
}
|
||||
|
||||
/// Mencetak ke printer thermal jaringan lewat raw socket port 9100.
|
||||
///
|
||||
/// Berbeda dari `flutter_esc_pos_network` yang hanya mengirim byte lalu menutup
|
||||
/// koneksi, klien ini menanyakan kondisi printer dengan `DLE EOT` dan membaca
|
||||
/// jawabannya. Tanpa itu, kertas habis atau cover terbuka tidak bisa dibedakan
|
||||
/// dari cetakan yang berhasil — byte tetap diterima socket dan semuanya tampak
|
||||
/// sukses.
|
||||
class NetworkPrinterClient {
|
||||
static const _logName = 'NetworkPrinterClient';
|
||||
|
||||
/// Perintah status real-time ESC/POS: `DLE EOT n`.
|
||||
static const _statusPrinter = [0x10, 0x04, 1];
|
||||
static const _statusOffline = [0x10, 0x04, 2];
|
||||
static const _statusPaper = [0x10, 0x04, 4];
|
||||
|
||||
/// Jeda menunggu balasan status. Sengaja pendek: printer yang tidak
|
||||
/// mendukung `DLE EOT` tidak akan pernah menjawab, dan kita tidak boleh
|
||||
/// menahan kasir hanya untuk menunggu jawaban yang tidak akan datang.
|
||||
static const _statusTimeout = Duration(milliseconds: 800);
|
||||
|
||||
/// Host yang terbukti tidak menjawab `DLE EOT`. Tanpa catatan ini, printer
|
||||
/// clone membuat setiap cetakan membayar timeout di atas — mahal untuk kasir
|
||||
/// yang sedang antre. Cukup dibuktikan sekali per sesi aplikasi.
|
||||
final Set<String> _statusUnsupported = {};
|
||||
|
||||
Future<NetworkPrintResult> printTicket({
|
||||
required String host,
|
||||
required List<int> data,
|
||||
int port = 9100,
|
||||
Duration connectTimeout = const Duration(seconds: 10),
|
||||
}) async {
|
||||
Socket? socket;
|
||||
|
||||
try {
|
||||
socket = await Socket.connect(host, port, timeout: connectTimeout);
|
||||
} catch (e) {
|
||||
log('Tidak bisa membuka socket ke $host:$port', name: _logName, error: e);
|
||||
return const NetworkPrintResult(
|
||||
failure: NetworkPrintFailure.unreachable,
|
||||
);
|
||||
}
|
||||
|
||||
final reader = _SocketReader();
|
||||
late StreamSubscription<List<int>> subscription;
|
||||
|
||||
try {
|
||||
subscription = socket.listen(
|
||||
reader.add,
|
||||
onError: (Object e) => log('Socket error', name: _logName, error: e),
|
||||
cancelOnError: false,
|
||||
);
|
||||
|
||||
final before = await _readStatus(socket, reader, host);
|
||||
|
||||
if (before.answered) {
|
||||
final blocking = before.blockingFailure;
|
||||
if (blocking != null) {
|
||||
log('Printer $host menolak: ${blocking.name}', name: _logName);
|
||||
return NetworkPrintResult(failure: blocking, statusSupported: true);
|
||||
}
|
||||
}
|
||||
|
||||
socket.add(data);
|
||||
await socket.flush();
|
||||
|
||||
// Kertas bisa habis di tengah struk. Cetakan yang terpotong sama
|
||||
// tidak bergunanya dengan yang tidak keluar sama sekali, jadi kondisi
|
||||
// ini dilaporkan sebagai gagal supaya kasir mencetak ulang.
|
||||
final after = before.answered
|
||||
? await _readStatus(socket, reader, host)
|
||||
: const _PrinterStatus.unanswered();
|
||||
|
||||
if (after.answered) {
|
||||
final blocking = after.blockingFailure;
|
||||
if (blocking != null) {
|
||||
log('Printer $host bermasalah setelah cetak: ${blocking.name}',
|
||||
name: _logName);
|
||||
return NetworkPrintResult(failure: blocking, statusSupported: true);
|
||||
}
|
||||
}
|
||||
|
||||
return NetworkPrintResult(
|
||||
statusSupported: before.answered,
|
||||
paperNearEnd: after.paperNearEnd || before.paperNearEnd,
|
||||
);
|
||||
} catch (e) {
|
||||
log('Gagal mengirim data ke $host:$port', name: _logName, error: e);
|
||||
return const NetworkPrintResult(
|
||||
failure: NetworkPrintFailure.writeFailed,
|
||||
statusSupported: false,
|
||||
);
|
||||
} finally {
|
||||
await subscription.cancel().catchError((_) {});
|
||||
try {
|
||||
await socket.close();
|
||||
} catch (_) {
|
||||
// Printer kadang menutup duluan; tidak ada yang bisa diperbuat.
|
||||
}
|
||||
socket.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
Future<_PrinterStatus> _readStatus(
|
||||
Socket socket,
|
||||
_SocketReader reader,
|
||||
String host,
|
||||
) async {
|
||||
if (_statusUnsupported.contains(host)) {
|
||||
return const _PrinterStatus.unanswered();
|
||||
}
|
||||
|
||||
final printer = await _query(socket, reader, _statusPrinter);
|
||||
|
||||
// Printer yang tidak mendukung DLE EOT tidak menjawab sama sekali.
|
||||
// Perlakukan sebagai "tidak diketahui", bukan sebagai error — kalau tidak,
|
||||
// printer semacam itu jadi tidak bisa dipakai sama sekali.
|
||||
if (printer == null) {
|
||||
log(
|
||||
'Printer $host tidak menjawab status; pemeriksaan kondisi kertas '
|
||||
'dilewati untuk seterusnya',
|
||||
name: _logName,
|
||||
);
|
||||
_statusUnsupported.add(host);
|
||||
return const _PrinterStatus.unanswered();
|
||||
}
|
||||
|
||||
final offline = await _query(socket, reader, _statusOffline);
|
||||
final paper = await _query(socket, reader, _statusPaper);
|
||||
|
||||
return _PrinterStatus(
|
||||
answered: true,
|
||||
offline: printer & 0x08 != 0,
|
||||
coverOpen: (offline ?? 0) & 0x04 != 0,
|
||||
paperOut: ((offline ?? 0) & 0x20 != 0) || ((paper ?? 0) & 0x60 != 0),
|
||||
paperNearEnd: (paper ?? 0) & 0x0C != 0,
|
||||
);
|
||||
}
|
||||
|
||||
/// Mengirim satu perintah status dan menunggu satu byte balasan.
|
||||
/// `null` berarti printer tidak menjawab atau menjawab dengan format asing.
|
||||
Future<int?> _query(
|
||||
Socket socket,
|
||||
_SocketReader reader,
|
||||
List<int> command,
|
||||
) async {
|
||||
reader.clear();
|
||||
socket.add(command);
|
||||
await socket.flush();
|
||||
|
||||
final byte = await reader.readByte(_statusTimeout);
|
||||
if (byte == null) return null;
|
||||
|
||||
// Balasan DLE EOT selalu punya pola tetap: bit0=0, bit1=1, bit4=1, bit7=0.
|
||||
// Byte yang tidak cocok berarti bukan balasan status, jadi lebih aman
|
||||
// dianggap tidak menjawab daripada salah tafsir.
|
||||
if (byte & 0x93 != 0x12) {
|
||||
log('Balasan status tidak dikenali: 0x${byte.toRadixString(16)}',
|
||||
name: _logName);
|
||||
return null;
|
||||
}
|
||||
|
||||
return byte;
|
||||
}
|
||||
}
|
||||
|
||||
class _PrinterStatus {
|
||||
final bool answered;
|
||||
final bool offline;
|
||||
final bool coverOpen;
|
||||
final bool paperOut;
|
||||
final bool paperNearEnd;
|
||||
|
||||
const _PrinterStatus({
|
||||
required this.answered,
|
||||
this.offline = false,
|
||||
this.coverOpen = false,
|
||||
this.paperOut = false,
|
||||
this.paperNearEnd = false,
|
||||
});
|
||||
|
||||
const _PrinterStatus.unanswered()
|
||||
: answered = false,
|
||||
offline = false,
|
||||
coverOpen = false,
|
||||
paperOut = false,
|
||||
paperNearEnd = false;
|
||||
|
||||
/// Kondisi yang membuat struk tidak akan keluar. Diperiksa berurutan dari
|
||||
/// yang paling spesifik supaya pesan ke kasir langsung menyebut tindakannya.
|
||||
NetworkPrintFailure? get blockingFailure {
|
||||
if (paperOut) return NetworkPrintFailure.paperOut;
|
||||
if (coverOpen) return NetworkPrintFailure.coverOpen;
|
||||
if (offline) return NetworkPrintFailure.offline;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Menampung byte yang datang dari socket dan membiarkannya dibaca satu per
|
||||
/// satu dengan batas waktu. Socket adalah stream sekali-langganan, jadi
|
||||
/// pembacaannya harus lewat satu subscription untuk seluruh sesi.
|
||||
class _SocketReader {
|
||||
final List<int> _buffer = [];
|
||||
Completer<void>? _waiter;
|
||||
|
||||
void add(List<int> data) {
|
||||
_buffer.addAll(data);
|
||||
_waiter?.complete();
|
||||
_waiter = null;
|
||||
}
|
||||
|
||||
void clear() => _buffer.clear();
|
||||
|
||||
Future<int?> readByte(Duration timeout) async {
|
||||
if (_buffer.isEmpty) {
|
||||
final waiter = Completer<void>();
|
||||
_waiter = waiter;
|
||||
|
||||
try {
|
||||
await waiter.future.timeout(timeout);
|
||||
} on TimeoutException {
|
||||
_waiter = null;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (_buffer.isEmpty) return null;
|
||||
return _buffer.removeAt(0);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -233,7 +233,7 @@ extension GetItInjectableX on _i174.GetIt {
|
||||
() => _i457.ApiClient(gh<_i361.Dio>(), gh<_i923.Env>()),
|
||||
);
|
||||
gh.factory<_i923.Env>(() => _i923.ProdEnv(), registerFor: {_prod});
|
||||
gh.factory<_i104.IPrinterRepository>(
|
||||
gh.lazySingleton<_i104.IPrinterRepository>(
|
||||
() => _i881.PrinterRepository(
|
||||
gh<_i149.PrinterLocalDataProvider>(),
|
||||
gh<_i693.OutletLocalDatasource>(),
|
||||
|
||||
@@ -5,7 +5,6 @@ import 'package:firebase_crashlytics/firebase_crashlytics.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
|
||||
import 'common/service/fcm_service.dart';
|
||||
import 'injection.dart';
|
||||
|
||||
@@ -13,8 +13,6 @@ import '../application/payment_method/payment_method_loader/payment_method_loade
|
||||
import '../application/printer/bluetooth/bluetooth_connect/bluetooth_connect_bloc.dart';
|
||||
import '../application/printer/bluetooth/bluetooth_loader/bluetooth_loader_bloc.dart';
|
||||
import '../application/printer/print_struck/print_struck_bloc.dart';
|
||||
import '../application/printer/printer_form/printer_form_bloc.dart';
|
||||
import '../application/printer/printer_loader/printer_loader_bloc.dart';
|
||||
import '../application/product/product_loader/product_loader_bloc.dart';
|
||||
import '../application/sync/sync_setting/sync_setting_bloc.dart';
|
||||
import '../application/table/table_form/table_form_bloc.dart';
|
||||
@@ -23,6 +21,7 @@ import '../application/void/void_form/void_form_bloc.dart';
|
||||
import '../common/theme/theme.dart';
|
||||
import '../common/constant/app_constant.dart';
|
||||
import '../injection.dart';
|
||||
import 'components/dialog/dialog.dart';
|
||||
import 'router/app_router.dart';
|
||||
import 'router/app_router_observer.dart';
|
||||
|
||||
@@ -54,8 +53,9 @@ class _AppWidgetState extends State<AppWidget> {
|
||||
BlocProvider(create: (context) => getIt<VoidFormBloc>()),
|
||||
BlocProvider(create: (context) => getIt<BluetoothLoaderBloc>()),
|
||||
BlocProvider(create: (context) => getIt<BluetoothConnectBloc>()),
|
||||
BlocProvider(create: (context) => getIt<PrinterFormBloc>()),
|
||||
BlocProvider(create: (context) => getIt<PrinterLoaderBloc>()),
|
||||
// PrinterFormBloc dan PrinterLoaderBloc sengaja TIDAK global: tiap tab
|
||||
// pengaturan printer memakai instance sendiri, lihat
|
||||
// SettingPrinterSection.
|
||||
BlocProvider(create: (context) => getIt<PrintStruckBloc>()),
|
||||
BlocProvider(create: (context) => getIt<SyncSettingBloc>()),
|
||||
BlocProvider(create: (context) => getIt<LogoutBloc>()),
|
||||
@@ -67,6 +67,20 @@ class _AppWidgetState extends State<AppWidget> {
|
||||
routerConfig: _appRouter.config(
|
||||
navigatorObservers: () => <NavigatorObserver>[AppRouteObserver()],
|
||||
),
|
||||
// Satu listener untuk seluruh aplikasi: cetak bisa dipicu dari banyak
|
||||
// halaman, dan `PrintStruckBloc` hidup di atas router jadi prosesnya
|
||||
// tetap jalan walau kasir sudah pindah halaman.
|
||||
builder: (context, child) => BlocListener<PrintStruckBloc, PrintStruckState>(
|
||||
listenWhen: (previous, current) => previous.jobId != current.jobId,
|
||||
listener: (_, __) {
|
||||
// Dialog harus dibuka dari context di bawah Navigator, sementara
|
||||
// builder ini berada di atasnya.
|
||||
final navigatorContext = _appRouter.navigatorKey.currentContext;
|
||||
if (navigatorContext == null) return;
|
||||
showPrintProgressDialog(navigatorContext);
|
||||
},
|
||||
child: child ?? const SizedBox.shrink(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
@@ -6,11 +8,13 @@ import '../../../application/checkout/checkout_form/checkout_form_bloc.dart';
|
||||
import '../../../application/outlet/outlet_loader/outlet_loader_bloc.dart';
|
||||
import '../../../application/printer/bluetooth/bluetooth_connect/bluetooth_connect_bloc.dart';
|
||||
import '../../../application/printer/bluetooth/bluetooth_loader/bluetooth_loader_bloc.dart';
|
||||
import '../../../application/printer/print_struck/print_struck_bloc.dart';
|
||||
import '../../../common/extension/extension.dart';
|
||||
import '../../../common/function/app_function.dart';
|
||||
import '../../../common/theme/theme.dart';
|
||||
import '../../../common/types/order_type.dart';
|
||||
import '../../../domain/delivery/delivery.dart';
|
||||
import '../../../domain/printer/printer.dart';
|
||||
import '../../../domain/product/product.dart';
|
||||
import '../button/button.dart';
|
||||
import '../card/outlet_card.dart';
|
||||
@@ -23,3 +27,4 @@ part 'variant_dialog.dart';
|
||||
part 'delivery_dialog.dart';
|
||||
part 'order_type_dialog.dart';
|
||||
part 'printer_bluetooth_dialog.dart';
|
||||
part 'print_progress_dialog.dart';
|
||||
|
||||
@@ -0,0 +1,273 @@
|
||||
part of 'dialog.dart';
|
||||
|
||||
/// Dijaga supaya satu job cetak hanya membuka satu dialog, walau listener
|
||||
/// terpicu lebih dari sekali.
|
||||
bool _isPrintProgressDialogOpen = false;
|
||||
|
||||
Future<void> showPrintProgressDialog(BuildContext context) async {
|
||||
if (_isPrintProgressDialogOpen) return;
|
||||
_isPrintProgressDialogOpen = true;
|
||||
|
||||
try {
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
// Cetak jalan di latar belakang, kasir tidak perlu menunggui dialognya.
|
||||
barrierDismissible: true,
|
||||
builder: (_) => const PrintProgressDialog(),
|
||||
);
|
||||
} finally {
|
||||
_isPrintProgressDialogOpen = false;
|
||||
}
|
||||
}
|
||||
|
||||
class PrintProgressDialog extends StatefulWidget {
|
||||
const PrintProgressDialog({super.key});
|
||||
|
||||
@override
|
||||
State<PrintProgressDialog> createState() => _PrintProgressDialogState();
|
||||
}
|
||||
|
||||
class _PrintProgressDialogState extends State<PrintProgressDialog> {
|
||||
Timer? _autoClose;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_autoClose?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/// Semua station keluar — tutup sendiri supaya antrean kasir tidak tertahan,
|
||||
/// tapi beri jeda sebentar agar centang hijaunya sempat terbaca.
|
||||
void _scheduleAutoClose() {
|
||||
_autoClose?.cancel();
|
||||
_autoClose = Timer(const Duration(milliseconds: 1500), () {
|
||||
if (mounted) Navigator.of(context).pop();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocConsumer<PrintStruckBloc, PrintStruckState>(
|
||||
listenWhen: (previous, current) =>
|
||||
previous.isPrinting && !current.isPrinting,
|
||||
listener: (context, state) {
|
||||
if (state.hasFailure) return;
|
||||
_scheduleAutoClose();
|
||||
},
|
||||
builder: (context, state) {
|
||||
final isDone = !state.isPrinting;
|
||||
|
||||
return CustomModalDialog(
|
||||
title: isDone ? 'Hasil Cetak' : 'Mencetak Struk',
|
||||
maxWidth: 480,
|
||||
minHeight: 0,
|
||||
contentPadding: const EdgeInsets.all(20),
|
||||
bottom: _buildFooter(context, state),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_buildSummary(state),
|
||||
const SpaceHeight(16),
|
||||
for (final result in state.stations) ...[
|
||||
_StationRow(result: result),
|
||||
const SpaceHeight(8),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSummary(PrintStruckState state) {
|
||||
final isDone = !state.isPrinting;
|
||||
final total = state.plannedCount;
|
||||
final success = state.successCount;
|
||||
|
||||
final Color color;
|
||||
final String message;
|
||||
|
||||
if (!isDone) {
|
||||
color = AppColor.primary;
|
||||
message = 'Mengirim ke printer…';
|
||||
} else if (state.hasFailure) {
|
||||
color = AppColor.error;
|
||||
message = '${state.failedStations.length} station gagal dicetak';
|
||||
} else {
|
||||
color = AppColor.success;
|
||||
message = 'Semua struk berhasil dicetak';
|
||||
}
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.08),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: color.withOpacity(0.25)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
message,
|
||||
style: AppStyle.md.copyWith(
|
||||
color: color,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'$success dari $total',
|
||||
style: AppStyle.md.copyWith(
|
||||
color: color,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFooter(BuildContext context, PrintStruckState state) {
|
||||
final isDone = !state.isPrinting;
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.fromLTRB(20, 0, 20, 20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
if (!isDone) ...[
|
||||
Text(
|
||||
'Cetak berjalan di latar belakang. Dialog ini boleh ditutup.',
|
||||
style: AppStyle.sm.copyWith(color: AppColor.textSecondary),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SpaceHeight(12),
|
||||
],
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: AppElevatedButton.outlined(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
label: 'Tutup',
|
||||
),
|
||||
),
|
||||
if (isDone && state.hasFailure) ...[
|
||||
const SpaceWidth(12),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: AppElevatedButton.filled(
|
||||
onPressed: () => context.read<PrintStruckBloc>().add(
|
||||
const PrintStruckEvent.retryFailed(),
|
||||
),
|
||||
label: 'Cetak Ulang yang Gagal',
|
||||
icon: const Icon(
|
||||
Icons.refresh_rounded,
|
||||
color: AppColor.white,
|
||||
size: 18,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _StationRow extends StatelessWidget {
|
||||
final PrintStationResult result;
|
||||
|
||||
const _StationRow({required this.result});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final color = _color;
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: result.isFailed ? color.withOpacity(0.4) : AppColor.border,
|
||||
),
|
||||
color: result.isFailed ? color.withOpacity(0.05) : Colors.transparent,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
SizedBox(width: 20, height: 20, child: Center(child: _icon)),
|
||||
const SpaceWidth(12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
result.station.label,
|
||||
style: AppStyle.md.copyWith(fontWeight: FontWeight.w600),
|
||||
),
|
||||
const SpaceHeight(2),
|
||||
Text(
|
||||
_statusText,
|
||||
style: AppStyle.sm.copyWith(color: color),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (result.elapsed > Duration.zero) ...[
|
||||
const SpaceWidth(8),
|
||||
Text(
|
||||
'${(result.elapsed.inMilliseconds / 1000).toStringAsFixed(1)}s',
|
||||
style: AppStyle.sm.copyWith(color: AppColor.textSecondary),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget get _icon => switch (result.status) {
|
||||
PrintStationStatus.printing => const SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
),
|
||||
PrintStationStatus.pending => const Icon(
|
||||
Icons.schedule_rounded,
|
||||
size: 18,
|
||||
color: AppColor.textSecondary,
|
||||
),
|
||||
PrintStationStatus.success => const Icon(
|
||||
Icons.check_circle_rounded,
|
||||
size: 20,
|
||||
color: AppColor.success,
|
||||
),
|
||||
PrintStationStatus.failed => const Icon(
|
||||
Icons.error_rounded,
|
||||
size: 20,
|
||||
color: AppColor.error,
|
||||
),
|
||||
PrintStationStatus.skipped => const Icon(
|
||||
Icons.remove_circle_outline_rounded,
|
||||
size: 18,
|
||||
color: AppColor.textSecondary,
|
||||
),
|
||||
};
|
||||
|
||||
Color get _color => switch (result.status) {
|
||||
PrintStationStatus.printing => AppColor.primary,
|
||||
PrintStationStatus.success => AppColor.success,
|
||||
PrintStationStatus.failed => AppColor.error,
|
||||
PrintStationStatus.pending ||
|
||||
PrintStationStatus.skipped => AppColor.textSecondary,
|
||||
};
|
||||
|
||||
String get _statusText => switch (result.status) {
|
||||
PrintStationStatus.pending => 'Menunggu giliran',
|
||||
PrintStationStatus.printing => 'Mengirim ke printer…',
|
||||
PrintStationStatus.success => 'Berhasil dicetak',
|
||||
PrintStationStatus.failed => result.message ?? 'Gagal dicetak',
|
||||
PrintStationStatus.skipped => result.message ?? 'Dilewati',
|
||||
};
|
||||
}
|
||||
@@ -6,6 +6,27 @@ import '../../../domain/outlet/outlet.dart';
|
||||
import 'receipt_component_builder.dart';
|
||||
|
||||
class PrintUi {
|
||||
/// `CapabilityProfile.load()` membaca dan mem-parse aset tiap kali dipanggil,
|
||||
/// dan sekali cetak semua station memanggilnya sampai lima kali. Hasilnya
|
||||
/// sama untuk seluruh aplikasi, jadi cukup dimuat satu kali.
|
||||
static Future<CapabilityProfile>? _cachedProfile;
|
||||
|
||||
static Future<CapabilityProfile> _capabilityProfile() async {
|
||||
final cached = _cachedProfile;
|
||||
if (cached != null) return cached;
|
||||
|
||||
final future = CapabilityProfile.load();
|
||||
_cachedProfile = future;
|
||||
|
||||
try {
|
||||
return await future;
|
||||
} catch (_) {
|
||||
// Jangan kunci kegagalan sesaat — biarkan cetakan berikutnya mencoba lagi.
|
||||
_cachedProfile = null;
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<int>> printOrder({
|
||||
required Order order,
|
||||
required Outlet outlet,
|
||||
@@ -14,7 +35,7 @@ class PrintUi {
|
||||
}) async {
|
||||
List<int> bytes = [];
|
||||
|
||||
final profile = await CapabilityProfile.load();
|
||||
final profile = await _capabilityProfile();
|
||||
final generator = Generator(
|
||||
paper == 58 ? PaperSize.mm58 : PaperSize.mm80,
|
||||
profile,
|
||||
@@ -90,7 +111,7 @@ class PrintUi {
|
||||
}) async {
|
||||
List<int> bytes = [];
|
||||
|
||||
final profile = await CapabilityProfile.load();
|
||||
final profile = await _capabilityProfile();
|
||||
final generator = Generator(
|
||||
paper == 58 ? PaperSize.mm58 : PaperSize.mm80,
|
||||
profile,
|
||||
@@ -142,7 +163,7 @@ class PrintUi {
|
||||
}) async {
|
||||
List<int> bytes = [];
|
||||
|
||||
final profile = await CapabilityProfile.load();
|
||||
final profile = await _capabilityProfile();
|
||||
final generator = Generator(
|
||||
paper == 58 ? PaperSize.mm58 : PaperSize.mm80,
|
||||
profile,
|
||||
@@ -198,7 +219,7 @@ class PrintUi {
|
||||
}) async {
|
||||
List<int> bytes = [];
|
||||
|
||||
final profile = await CapabilityProfile.load();
|
||||
final profile = await _capabilityProfile();
|
||||
final generator = Generator(
|
||||
paper == 58 ? PaperSize.mm58 : PaperSize.mm80,
|
||||
profile,
|
||||
@@ -253,7 +274,7 @@ class PrintUi {
|
||||
}) async {
|
||||
List<int> bytes = [];
|
||||
|
||||
final profile = await CapabilityProfile.load();
|
||||
final profile = await _capabilityProfile();
|
||||
final generator = Generator(
|
||||
paper == 58 ? PaperSize.mm58 : PaperSize.mm80,
|
||||
profile,
|
||||
@@ -294,7 +315,7 @@ class PrintUi {
|
||||
}) async {
|
||||
List<int> bytes = [];
|
||||
|
||||
final profile = await CapabilityProfile.load();
|
||||
final profile = await _capabilityProfile();
|
||||
final generator = Generator(
|
||||
paper == 58 ? PaperSize.mm58 : PaperSize.mm80,
|
||||
profile,
|
||||
@@ -372,7 +393,7 @@ class PrintUi {
|
||||
}) async {
|
||||
List<int> bytes = [];
|
||||
|
||||
final profile = await CapabilityProfile.load();
|
||||
final profile = await _capabilityProfile();
|
||||
final generator = Generator(
|
||||
paper == 58 ? PaperSize.mm58 : PaperSize.mm80,
|
||||
profile,
|
||||
@@ -451,7 +472,7 @@ class PrintUi {
|
||||
}) async {
|
||||
List<int> bytes = [];
|
||||
|
||||
final profile = await CapabilityProfile.load();
|
||||
final profile = await _capabilityProfile();
|
||||
final generator = Generator(
|
||||
paper == 58 ? PaperSize.mm58 : PaperSize.mm80,
|
||||
profile,
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../../../../../../application/printer/printer_form/printer_form_bloc.dart';
|
||||
import '../../../../../../application/printer/printer_loader/printer_loader_bloc.dart';
|
||||
import '../../../../../../injection.dart';
|
||||
import '../../../../../components/page/page_title.dart';
|
||||
import '../../../../../components/tab/custom_tabbar.dart';
|
||||
import 'printer/setting_printer_bar.dart';
|
||||
@@ -11,18 +15,36 @@ import 'printer/setting_printer_ticket.dart';
|
||||
class SettingPrinterSection extends StatelessWidget {
|
||||
const SettingPrinterSection({super.key});
|
||||
|
||||
/// Tiap tab wajib punya `PrinterFormBloc` dan `PrinterLoaderBloc` sendiri.
|
||||
///
|
||||
/// Kelima tab hidup bersamaan di dalam `TabBarView`, dan `PrinterFormBloc`
|
||||
/// membangun entitas `Printer` sepenuhnya dari state-nya (`type`, `address`,
|
||||
/// `code`, ...). Saat blocnya dibagi, tab yang terakhir menulis state akan
|
||||
/// menang, lalu tombol Simpan di tab mana pun menyimpan state milik tab lain
|
||||
/// — itu sebabnya printer kitchen bisa tersimpan dengan alamat IP yang benar
|
||||
/// tapi `type` "Bluetooth" warisan tab Receipt.
|
||||
Widget _scoped(Widget tab) {
|
||||
return MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider(create: (_) => getIt<PrinterFormBloc>()),
|
||||
BlocProvider(create: (_) => getIt<PrinterLoaderBloc>()),
|
||||
],
|
||||
child: tab,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const Column(
|
||||
return Column(
|
||||
children: [
|
||||
PageTitle(
|
||||
const PageTitle(
|
||||
isBack: false,
|
||||
title: 'Printer',
|
||||
subtitle: 'Kelola printer untuk cetak struk',
|
||||
),
|
||||
Expanded(
|
||||
child: CustomTabBar(
|
||||
tabTitles: [
|
||||
tabTitles: const [
|
||||
'Receipt Printer',
|
||||
'Checker Printer',
|
||||
'Kitchen Printer',
|
||||
@@ -30,11 +52,11 @@ class SettingPrinterSection extends StatelessWidget {
|
||||
'Tiket Printer',
|
||||
],
|
||||
tabViews: [
|
||||
SettingPrinterReceipt(),
|
||||
SettingPrinterChecker(),
|
||||
SettingPrinterKitchen(),
|
||||
SettingPrinterBar(),
|
||||
SettingPrinterTicket(),
|
||||
_scoped(const SettingPrinterReceipt()),
|
||||
_scoped(const SettingPrinterChecker()),
|
||||
_scoped(const SettingPrinterKitchen()),
|
||||
_scoped(const SettingPrinterBar()),
|
||||
_scoped(const SettingPrinterTicket()),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -3,10 +3,8 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../../../application/order/order_loader/order_loader_bloc.dart';
|
||||
import '../../../application/printer/print_struck/print_struck_bloc.dart';
|
||||
import '../../../common/theme/theme.dart';
|
||||
import '../../../injection.dart';
|
||||
import '../../components/toast/flushbar.dart';
|
||||
import 'widgets/order_left_panel.dart';
|
||||
import 'widgets/order_right_panel.dart';
|
||||
|
||||
@@ -37,20 +35,7 @@ class OrderPage extends StatelessWidget implements AutoRouteWrapper {
|
||||
);
|
||||
},
|
||||
),
|
||||
BlocListener<PrintStruckBloc, PrintStruckState>(
|
||||
listenWhen: (previous, current) =>
|
||||
previous.failureOrPrintStruck != current.failureOrPrintStruck,
|
||||
listener: (context, state) {
|
||||
state.failureOrPrintStruck.fold(() {}, (either) {
|
||||
either.fold(
|
||||
(f) => AppFlushbar.showPrinterFailureToast(context, f),
|
||||
(success) {
|
||||
AppFlushbar.showSuccess(context, "Struck berhasil dicetak");
|
||||
},
|
||||
);
|
||||
});
|
||||
},
|
||||
),
|
||||
// Hasil cetak dilaporkan lewat dialog progres global di `AppWidget`.
|
||||
],
|
||||
child: SafeArea(
|
||||
child: Scaffold(
|
||||
|
||||
+25
-40
@@ -3,10 +3,8 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../../../../../application/checkout/checkout_form/checkout_form_bloc.dart';
|
||||
import '../../../../../application/printer/print_struck/print_struck_bloc.dart';
|
||||
import '../../../../../common/theme/theme.dart';
|
||||
import '../../../../components/spaces/space.dart';
|
||||
import '../../../../components/toast/flushbar.dart';
|
||||
import 'widgets/success_add_item_order_left_panel.dart';
|
||||
import 'widgets/success_add_item_order_right_panel.dart';
|
||||
|
||||
@@ -16,46 +14,33 @@ class SuccessAddItemOrderPage extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocListener<PrintStruckBloc, PrintStruckState>(
|
||||
listenWhen: (p, c) => p.failureOrPrintStruck != c.failureOrPrintStruck,
|
||||
listener: (context, state) {
|
||||
state.failureOrPrintStruck.fold(
|
||||
() {},
|
||||
(either) => either.fold(
|
||||
(f) => AppFlushbar.showPrinterFailureToast(context, f),
|
||||
(success) {
|
||||
AppFlushbar.showSuccess(context, "Struck berhasil dicetak");
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Scaffold(
|
||||
backgroundColor: AppColor.background,
|
||||
body: SafeArea(
|
||||
child: BlocBuilder<CheckoutFormBloc, CheckoutFormState>(
|
||||
builder: (context, checkoutState) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 35,
|
||||
child: SuccessAddItemOrderLeftPanel(
|
||||
checkoutState: checkoutState,
|
||||
),
|
||||
// Hasil cetak dilaporkan lewat dialog progres global di `AppWidget`.
|
||||
return Scaffold(
|
||||
backgroundColor: AppColor.background,
|
||||
body: SafeArea(
|
||||
child: BlocBuilder<CheckoutFormBloc, CheckoutFormState>(
|
||||
builder: (context, checkoutState) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 35,
|
||||
child: SuccessAddItemOrderLeftPanel(
|
||||
checkoutState: checkoutState,
|
||||
),
|
||||
const SpaceWidth(16),
|
||||
Expanded(
|
||||
flex: 65,
|
||||
child: SuccessAddItemOrderRightPanel(
|
||||
checkoutState: checkoutState,
|
||||
),
|
||||
),
|
||||
const SpaceWidth(16),
|
||||
Expanded(
|
||||
flex: 65,
|
||||
child: SuccessAddItemOrderRightPanel(
|
||||
checkoutState: checkoutState,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -3,13 +3,11 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../../../../../application/order/order_loader/order_loader_bloc.dart';
|
||||
import '../../../../../application/printer/print_struck/print_struck_bloc.dart';
|
||||
import '../../../../../common/theme/theme.dart';
|
||||
import '../../../../../domain/order/order.dart';
|
||||
import '../../../../../injection.dart';
|
||||
import '../../../../components/loader/loader_with_text.dart';
|
||||
import '../../../../components/spaces/space.dart';
|
||||
import '../../../../components/toast/flushbar.dart';
|
||||
import 'widgets/success_order_left_panel.dart';
|
||||
import 'widgets/success_order_right_panel.dart';
|
||||
|
||||
@@ -20,47 +18,33 @@ class SuccessOrderPage extends StatelessWidget implements AutoRouteWrapper {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocListener<PrintStruckBloc, PrintStruckState>(
|
||||
listenWhen: (previous, current) =>
|
||||
previous.failureOrPrintStruck != current.failureOrPrintStruck,
|
||||
listener: (context, state) {
|
||||
state.failureOrPrintStruck.fold(
|
||||
() {},
|
||||
(either) => either.fold(
|
||||
(f) => AppFlushbar.showPrinterFailureToast(context, f),
|
||||
(success) {
|
||||
AppFlushbar.showSuccess(context, "Struck berhasil dicetak");
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Scaffold(
|
||||
backgroundColor: AppColor.background,
|
||||
body: SafeArea(
|
||||
child: BlocBuilder<OrderLoaderBloc, OrderLoaderState>(
|
||||
builder: (context, state) {
|
||||
if (state.isFetchingById) {
|
||||
return const Center(child: LoaderWithText());
|
||||
}
|
||||
// Hasil cetak dilaporkan lewat dialog progres global di `AppWidget`.
|
||||
return Scaffold(
|
||||
backgroundColor: AppColor.background,
|
||||
body: SafeArea(
|
||||
child: BlocBuilder<OrderLoaderBloc, OrderLoaderState>(
|
||||
builder: (context, state) {
|
||||
if (state.isFetchingById) {
|
||||
return const Center(child: LoaderWithText());
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 35,
|
||||
child: SuccessOrderLeftPanel(order: state.order),
|
||||
),
|
||||
const SpaceWidth(16),
|
||||
Expanded(
|
||||
flex: 65,
|
||||
child: SuccessOrderRightPanel(order: state.order),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 35,
|
||||
child: SuccessOrderLeftPanel(order: state.order),
|
||||
),
|
||||
const SpaceWidth(16),
|
||||
Expanded(
|
||||
flex: 65,
|
||||
child: SuccessOrderRightPanel(order: state.order),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -62,6 +62,11 @@ class OrderRightPanel extends StatelessWidget {
|
||||
color: AppColor.white,
|
||||
onSelected: (value) {
|
||||
switch (value) {
|
||||
case 'all':
|
||||
context.read<PrintStruckBloc>().add(
|
||||
PrintStruckEvent.order(state.selectedOrder!),
|
||||
);
|
||||
break;
|
||||
case 'checker':
|
||||
context.read<PrintStruckBloc>().add(
|
||||
PrintStruckEvent.checker(state.selectedOrder!),
|
||||
@@ -79,7 +84,23 @@ class OrderRightPanel extends StatelessWidget {
|
||||
break;
|
||||
}
|
||||
},
|
||||
itemBuilder: (context) => [
|
||||
itemBuilder: (context) => <PopupMenuEntry<String>>[
|
||||
// Cetak semua station sekaligus — dipisah dari cetak per
|
||||
// station karena sifatnya berbeda.
|
||||
const PopupMenuItem(
|
||||
value: 'all',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.done_all_rounded, color: AppColor.primary),
|
||||
SizedBox(width: 8),
|
||||
Text(
|
||||
'Print All',
|
||||
style: TextStyle(fontWeight: FontWeight.w600),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const PopupMenuDivider(),
|
||||
const PopupMenuItem(
|
||||
value: 'checker',
|
||||
child: Row(
|
||||
|
||||
@@ -3,12 +3,10 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../../../../../application/order/order_loader/order_loader_bloc.dart';
|
||||
import '../../../../../application/printer/print_struck/print_struck_bloc.dart';
|
||||
import '../../../../../common/theme/theme.dart';
|
||||
import '../../../../../injection.dart';
|
||||
import '../../../../components/error/order_loader_error_state_widget.dart';
|
||||
import '../../../../components/loader/loader_with_text.dart';
|
||||
import '../../../../components/toast/flushbar.dart';
|
||||
import 'widgets/payment_success_left_panel.dart';
|
||||
import 'widgets/payment_success_right_panel.dart';
|
||||
|
||||
@@ -19,58 +17,46 @@ class PaymentSuccessPage extends StatelessWidget implements AutoRouteWrapper {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocListener<PrintStruckBloc, PrintStruckState>(
|
||||
listenWhen: (previous, current) =>
|
||||
previous.failureOrPrintStruck != current.failureOrPrintStruck,
|
||||
listener: (context, state) {
|
||||
state.failureOrPrintStruck.fold(() {}, (either) {
|
||||
either.fold((f) => AppFlushbar.showPrinterFailureToast(context, f), (
|
||||
success,
|
||||
) {
|
||||
AppFlushbar.showSuccess(context, "Struck berhasil dicetak");
|
||||
});
|
||||
});
|
||||
},
|
||||
child: Scaffold(
|
||||
backgroundColor: AppColor.background,
|
||||
body: SafeArea(
|
||||
child: BlocBuilder<OrderLoaderBloc, OrderLoaderState>(
|
||||
builder: (context, state) {
|
||||
if (state.isFetchingById) {
|
||||
return const Center(child: LoaderWithText());
|
||||
}
|
||||
return state.failureOptionGetById.fold(
|
||||
() => Container(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Row(
|
||||
children: [
|
||||
// Left Panel - Success Message & Order Info
|
||||
Expanded(
|
||||
flex: 35,
|
||||
child: PaymentSuccessLeftPanel(order: state.order),
|
||||
),
|
||||
// Hasil cetak dilaporkan lewat dialog progres global di `AppWidget`.
|
||||
return Scaffold(
|
||||
backgroundColor: AppColor.background,
|
||||
body: SafeArea(
|
||||
child: BlocBuilder<OrderLoaderBloc, OrderLoaderState>(
|
||||
builder: (context, state) {
|
||||
if (state.isFetchingById) {
|
||||
return const Center(child: LoaderWithText());
|
||||
}
|
||||
return state.failureOptionGetById.fold(
|
||||
() => Container(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Row(
|
||||
children: [
|
||||
// Left Panel - Success Message & Order Info
|
||||
Expanded(
|
||||
flex: 35,
|
||||
child: PaymentSuccessLeftPanel(order: state.order),
|
||||
),
|
||||
|
||||
const SizedBox(width: 16),
|
||||
const SizedBox(width: 16),
|
||||
|
||||
// Right Panel - Order Details
|
||||
Expanded(
|
||||
flex: 65,
|
||||
child: PaymentSuccessRightPanel(order: state.order),
|
||||
),
|
||||
],
|
||||
),
|
||||
// Right Panel - Order Details
|
||||
Expanded(
|
||||
flex: 65,
|
||||
child: PaymentSuccessRightPanel(order: state.order),
|
||||
),
|
||||
],
|
||||
),
|
||||
(f) => OrderLoaderErrorStateWidget(
|
||||
failure: f,
|
||||
onRefresh: () {
|
||||
context.read<OrderLoaderBloc>().add(
|
||||
OrderLoaderEvent.getById(orderId),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
(f) => OrderLoaderErrorStateWidget(
|
||||
failure: f,
|
||||
onRefresh: () {
|
||||
context.read<OrderLoaderBloc>().add(
|
||||
OrderLoaderEvent.getById(orderId),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
+39
-55
@@ -3,13 +3,11 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../../../../../application/order/order_loader/order_loader_bloc.dart';
|
||||
import '../../../../../application/printer/print_struck/print_struck_bloc.dart';
|
||||
import '../../../../../common/theme/theme.dart';
|
||||
import '../../../../../domain/order/order.dart';
|
||||
import '../../../../../injection.dart';
|
||||
import '../../../../components/error/order_loader_error_state_widget.dart';
|
||||
import '../../../../components/loader/loader_with_text.dart';
|
||||
import '../../../../components/toast/flushbar.dart';
|
||||
import 'widgets/payment_success_split_bill_left_panel.dart';
|
||||
import 'widgets/payment_success_split_bill_right_panel.dart';
|
||||
|
||||
@@ -26,64 +24,50 @@ class PaymentSuccessSplitBillPage extends StatelessWidget
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocListener<PrintStruckBloc, PrintStruckState>(
|
||||
listenWhen: (previous, current) =>
|
||||
previous.failureOrPrintStruck != current.failureOrPrintStruck,
|
||||
listener: (context, state) {
|
||||
state.failureOrPrintStruck.fold(
|
||||
() {},
|
||||
(either) => either.fold(
|
||||
(f) => AppFlushbar.showPrinterFailureToast(context, f),
|
||||
(success) {
|
||||
AppFlushbar.showSuccess(context, "Struck berhasil dicetak");
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Scaffold(
|
||||
backgroundColor: AppColor.background,
|
||||
body: SafeArea(
|
||||
child: BlocBuilder<OrderLoaderBloc, OrderLoaderState>(
|
||||
builder: (context, state) {
|
||||
if (state.isFetchingById) {
|
||||
return const Center(child: LoaderWithText());
|
||||
}
|
||||
return state.failureOptionGetById.fold(
|
||||
() => Container(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Row(
|
||||
children: [
|
||||
// Left Panel - Success Message & Order Info
|
||||
Expanded(
|
||||
flex: 35,
|
||||
child: PaymentSuccessSplitBillLeftPanel(
|
||||
order: state.order.copyWith(orderItems: orderItems),
|
||||
),
|
||||
// Hasil cetak dilaporkan lewat dialog progres global di `AppWidget`.
|
||||
return Scaffold(
|
||||
backgroundColor: AppColor.background,
|
||||
body: SafeArea(
|
||||
child: BlocBuilder<OrderLoaderBloc, OrderLoaderState>(
|
||||
builder: (context, state) {
|
||||
if (state.isFetchingById) {
|
||||
return const Center(child: LoaderWithText());
|
||||
}
|
||||
return state.failureOptionGetById.fold(
|
||||
() => Container(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Row(
|
||||
children: [
|
||||
// Left Panel - Success Message & Order Info
|
||||
Expanded(
|
||||
flex: 35,
|
||||
child: PaymentSuccessSplitBillLeftPanel(
|
||||
order: state.order.copyWith(orderItems: orderItems),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(width: 16),
|
||||
const SizedBox(width: 16),
|
||||
|
||||
// Right Panel - Order Details
|
||||
Expanded(
|
||||
flex: 65,
|
||||
child: PaymentSuccessSplitBillRightPanel(
|
||||
order: state.order.copyWith(orderItems: orderItems),
|
||||
),
|
||||
// Right Panel - Order Details
|
||||
Expanded(
|
||||
flex: 65,
|
||||
child: PaymentSuccessSplitBillRightPanel(
|
||||
order: state.order.copyWith(orderItems: orderItems),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
(f) => OrderLoaderErrorStateWidget(
|
||||
failure: f,
|
||||
onRefresh: () {
|
||||
context.read<OrderLoaderBloc>().add(
|
||||
OrderLoaderEvent.getById(orderId),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
(f) => OrderLoaderErrorStateWidget(
|
||||
failure: f,
|
||||
onRefresh: () {
|
||||
context.read<OrderLoaderBloc>().add(
|
||||
OrderLoaderEvent.getById(orderId),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -2,11 +2,9 @@ import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../../../../../application/printer/print_struck/print_struck_bloc.dart';
|
||||
import '../../../../../application/void/void_form/void_form_bloc.dart';
|
||||
import '../../../../../common/theme/theme.dart';
|
||||
import '../../../../components/spaces/space.dart';
|
||||
import '../../../../components/toast/flushbar.dart';
|
||||
import 'widgets/void_success_left_panel.dart';
|
||||
import 'widgets/void_success_right_panel.dart';
|
||||
|
||||
@@ -16,44 +14,30 @@ class VoidSuccessPage extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocListener<PrintStruckBloc, PrintStruckState>(
|
||||
listenWhen: (previous, current) =>
|
||||
previous.failureOrPrintStruck != current.failureOrPrintStruck,
|
||||
listener: (context, state) {
|
||||
state.failureOrPrintStruck.fold(
|
||||
() {},
|
||||
(either) => either.fold(
|
||||
(f) => AppFlushbar.showPrinterFailureToast(context, f),
|
||||
(success) {
|
||||
AppFlushbar.showSuccess(context, "Struck berhasil dicetak");
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Scaffold(
|
||||
backgroundColor: AppColor.background,
|
||||
body: BlocBuilder<VoidFormBloc, VoidFormState>(
|
||||
builder: (context, state) {
|
||||
return SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 35,
|
||||
child: VoidSuccessLeftPanel(state: state),
|
||||
),
|
||||
const SpaceWidth(16),
|
||||
Expanded(
|
||||
flex: 65,
|
||||
child: VoidSuccessRightPanel(state: state),
|
||||
),
|
||||
],
|
||||
),
|
||||
// Hasil cetak dilaporkan lewat dialog progres global di `AppWidget`.
|
||||
return Scaffold(
|
||||
backgroundColor: AppColor.background,
|
||||
body: BlocBuilder<VoidFormBloc, VoidFormState>(
|
||||
builder: (context, state) {
|
||||
return SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 35,
|
||||
child: VoidSuccessLeftPanel(state: state),
|
||||
),
|
||||
const SpaceWidth(16),
|
||||
Expanded(
|
||||
flex: 65,
|
||||
child: VoidSuccessRightPanel(state: state),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
+5
-5
@@ -852,10 +852,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
|
||||
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.18.0"
|
||||
version: "1.17.0"
|
||||
mime:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1337,10 +1337,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
|
||||
sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.11"
|
||||
version: "0.7.10"
|
||||
time:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1502,5 +1502,5 @@ packages:
|
||||
source: hosted
|
||||
version: "3.1.3"
|
||||
sdks:
|
||||
dart: ">=3.10.0-0 <4.0.0"
|
||||
dart: ">=3.9.0-0 <4.0.0"
|
||||
flutter: ">=3.29.0"
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ description: "A new Flutter project."
|
||||
|
||||
publish_to: "none"
|
||||
|
||||
version: 1.0.11+17
|
||||
version: 1.0.12+19
|
||||
|
||||
environment:
|
||||
sdk: ^3.8.1
|
||||
|
||||
Reference in New Issue
Block a user