Compare commits
24
Commits
ea29c62af1
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
924d698640 | ||
|
|
4342af689f | ||
|
|
1505a83dd3 | ||
|
|
9777f63426 | ||
|
|
7986164a17 | ||
|
|
32f64a197a | ||
|
|
cb7139fc3e | ||
|
|
951e154623 | ||
|
|
ebcddca4bb | ||
|
|
1b4caf45d5 | ||
|
|
038f4d65fe | ||
|
|
865897fc04 | ||
|
|
f5b0e66706 | ||
|
|
1ff144a1a2 | ||
|
|
eb031d4c2a | ||
|
|
7161f523c2 | ||
|
|
d097df1592 | ||
|
|
e8bbef03b0 | ||
|
|
b228538725 | ||
|
|
2ab20a1150 | ||
|
|
b9fcd79962 | ||
|
|
9a5f0c7415 | ||
|
|
d345294a2f | ||
|
|
beb86f6259 |
@@ -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
+17
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Dev",
|
||||
"request": "launch",
|
||||
"type": "dart",
|
||||
"args": ["--dart-define=ENV=dev"]
|
||||
},
|
||||
{
|
||||
"name": "Prod",
|
||||
"request": "launch",
|
||||
"type": "dart",
|
||||
"args": ["--dart-define=ENV=prod"]
|
||||
}
|
||||
]
|
||||
}
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"dart.flutterSdkPath": ".fvm/versions/3.41.9"
|
||||
}
|
||||
@@ -24,7 +24,8 @@ Aplikasi Point of Sale (POS) berbasis Flutter untuk manajemen kasir restoran. Me
|
||||
## Tech Stack
|
||||
|
||||
| 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,120 @@ 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
|
||||
|
||||
fvm flutter build apk --release --split-per-abi --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
|
||||
@@ -137,7 +216,7 @@ DI menggunakan `get_it` + `injectable`. Semua service, repository, dan BLoC dida
|
||||
### Modul DI
|
||||
|
||||
| File | Isi |
|
||||
|---|---|
|
||||
| ---------------------------- | ----------------------------------------------------------------------------------------- |
|
||||
| `di_firebase.dart` | `FirebaseMessaging`, `FlutterLocalNotificationsPlugin`, `DeviceInfoPlugin`, `PackageInfo` |
|
||||
| `di_dio.dart` | `Dio` HTTP client |
|
||||
| `di_shared_preferences.dart` | `SharedPreferences` |
|
||||
@@ -210,6 +289,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,3 +1,7 @@
|
||||
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
# This builtInKotlin flag was added automatically by Flutter migrator
|
||||
android.builtInKotlin=false
|
||||
# This newDsl flag was added automatically by Flutter migrator
|
||||
android.newDsl=false
|
||||
|
||||
+1
-1
@@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-all.zip
|
||||
|
||||
@@ -18,8 +18,8 @@ pluginManagement {
|
||||
|
||||
plugins {
|
||||
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
|
||||
id("com.android.application") version "8.7.3" apply false
|
||||
id("org.jetbrains.kotlin.android") version "2.1.0" apply false
|
||||
id("com.android.application") version "8.11.1" apply false
|
||||
id("org.jetbrains.kotlin.android") version "2.2.20" apply false
|
||||
id("com.google.gms.google-services") version "4.4.4" apply false
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,279 @@
|
||||
# Integrasi Produk Timbangan — POS Mobile & Backoffice
|
||||
|
||||
**Migrasi:** `000089` · **Base URL:** `/api/v1` · **Kompatibilitas:** mundur penuh
|
||||
|
||||
Panduan untuk menjual produk per timbangan (ikan, daging, buah) dari sisi klien.
|
||||
Alasan di balik setiap keputusan desain ada di [`rfc-weight-based-products.md`](./rfc-weight-based-products.md).
|
||||
|
||||
---
|
||||
|
||||
## 1. Konsep inti
|
||||
|
||||
**Satu penimbangan = satu baris order.**
|
||||
|
||||
Pelanggan memesan Ikan Tude 4,2 ons, lalu memesan Ikan Tude lagi 5,6 ons. Itu **dua
|
||||
baris terpisah**, karena keduanya dua ikan berbeda yang ditimbang sendiri-sendiri.
|
||||
|
||||
| Bukan begini | Melainkan begini |
|
||||
|---|---|
|
||||
| `quantity: 9.8` | `quantity: 1, weight: 4.2`<br>`quantity: 1, weight: 5.6` |
|
||||
| Dua ikan hilang jejaknya, dan `quantity` bertipe integer sehingga pecahan ditolak | Tiap penimbangan berdiri sendiri, bisa di-void atau dibayar terpisah |
|
||||
|
||||
Empat aturan yang berlaku di seluruh dokumen ini:
|
||||
|
||||
1. `quantity` untuk produk timbangan **selalu 1**. Backend memaksanya, dan database
|
||||
menolak nilai lain lewat constraint `chk_order_items_weight_single_line`.
|
||||
2. `weight` menyimpan angka timbangan, dalam satuan produk itu sendiri (ons, kg, gram —
|
||||
apa pun yang dipilih saat setup).
|
||||
3. Harga baris dihitung `weight × unit_price`, bukan `quantity × unit_price`.
|
||||
`unit_price` tetap berarti harga per satu satuan (per ons).
|
||||
4. **Jangan pernah menggabungkan dua baris** produk timbangan menjadi satu, meski
|
||||
produknya sama.
|
||||
|
||||
Untuk produk biasa tidak ada yang berubah: `weight` tidak dikirim, `quantity` tetap
|
||||
cacah seperti sekarang.
|
||||
|
||||
---
|
||||
|
||||
## 2. Backoffice — setup produk
|
||||
|
||||
### 2.1 Pastikan satuannya ada
|
||||
|
||||
Satuan disimpan per organisasi. Buat sekali, pakai ulang untuk semua produk timbangan.
|
||||
|
||||
`POST /api/v1/units`
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Ons",
|
||||
"abbreviation": "ons",
|
||||
"is_active": true
|
||||
}
|
||||
```
|
||||
|
||||
`abbreviation` yang dipakai POS untuk mencetak `4,2 ons` di struk — isi dengan bentuk
|
||||
pendek yang benar-benar ingin ditampilkan. Daftar satuan dibaca lewat `GET /api/v1/units`.
|
||||
|
||||
### 2.2 Buat produk sebagai produk timbangan
|
||||
|
||||
`POST /api/v1/products`
|
||||
|
||||
```json
|
||||
{
|
||||
"category_id": "…",
|
||||
"name": "Ikan Tude",
|
||||
"price": 4500,
|
||||
"cost": 3000,
|
||||
"sell_by": "weight",
|
||||
"unit_id": "<id satuan Ons>"
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Tipe | Keterangan |
|
||||
|---|---|---|
|
||||
| `sell_by` | `"unit"` \| `"weight"` | Opsional, default `"unit"`. Nilai lain **ditolak** dengan pesan jelas. |
|
||||
| `unit_id` | UUID | **Wajib** saat `sell_by: "weight"`, ditolak backend bila kosong. Opsional untuk produk satuan. |
|
||||
| `price` | number | Harga per satu satuan. Rp 4.500 per ons, bukan harga per ikan. |
|
||||
|
||||
Pada `PUT /api/v1/products/:id`, `unit_id` **tidak perlu dikirim ulang** bila produknya
|
||||
sudah punya satuan — mengubah `sell_by` menjadi `"weight"` saja sudah cukup. Yang ditolak
|
||||
adalah kondisi akhirnya: produk yang dijual per timbangan tanpa satuan.
|
||||
|
||||
Keduanya juga bisa diubah lewat `PUT /api/v1/products/:id` dengan bentuk yang sama, dan
|
||||
ikut terbaca di setiap response produk (`GET /api/v1/products`, `/products/all`,
|
||||
`/products/:id`).
|
||||
|
||||
### 2.3 Catatan UI
|
||||
|
||||
- Kunci `sell_by` **setelah produk punya transaksi**. Mengubah produk lama dari `unit`
|
||||
ke `weight` tidak mengubah baris order yang sudah ada — baris lama tetap dihitung per
|
||||
cacah — tapi akan membingungkan pengguna yang melihat riwayatnya.
|
||||
- Saat `weight` dipilih, jadikan pemilih satuan sebagai field **wajib** di form. Backend
|
||||
juga menolaknya, tapi ditangkap di form lebih baik daripada baru gagal saat simpan.
|
||||
- Ubah label harga mengikuti satuan yang dipilih: *"Harga per ons"*.
|
||||
- Untuk produk satuan, pemilih satuan boleh disembunyikan — `unit_id` opsional dan belum
|
||||
dikonsumsi apa pun di POS.
|
||||
|
||||
---
|
||||
|
||||
## 3. POS Mobile — transaksi
|
||||
|
||||
### 3.1 Bentuk input mengikuti `sell_by`
|
||||
|
||||
| `sell_by` | Input di POS | Yang dikirim |
|
||||
|---|---|---|
|
||||
| `"unit"` | Stepper − / + seperti sekarang | `quantity: n`, tanpa `weight` |
|
||||
| `"weight"` | Papan angka desimal, satuan dari `unit` produk | `quantity: 1` + `weight: 4.2` |
|
||||
|
||||
### 3.2 Mengirim order
|
||||
|
||||
`POST /api/v1/orders`
|
||||
|
||||
```json
|
||||
{
|
||||
"outlet_id": "…",
|
||||
"user_id": "…",
|
||||
"order_type": "dine_in",
|
||||
"order_items": [
|
||||
{ "product_id": "<ikan-tude>", "quantity": 1, "weight": 4.2 },
|
||||
{ "product_id": "<ikan-tude>", "quantity": 1, "weight": 5.6 },
|
||||
{ "product_id": "<nasi-goreng>", "quantity": 2 }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Bentuk yang sama berlaku untuk `POST /api/v1/orders/:id/add-items` dan untuk pemesanan
|
||||
mandiri `POST /api/v1/self-order/orders`.
|
||||
|
||||
**Presisi.** Berat dibulatkan backend ke 3 desimal. Kirim `4.2` atau `4.237`; angka di
|
||||
bawah `0.001` membulat ke nol dan ditolak.
|
||||
|
||||
**Harga.** Field `unit_price` pada request **diabaikan** — harga selalu diambil backend
|
||||
dari master produk. Jangan mengirim harga hasil hitungan sendiri.
|
||||
|
||||
---
|
||||
|
||||
## 4. Menampilkan baris
|
||||
|
||||
Setiap `order_items[]` di response membawa empat field tambahan:
|
||||
|
||||
```json
|
||||
{
|
||||
"product_name": "Ikan Tude",
|
||||
"quantity": 1,
|
||||
"weight": 4.2,
|
||||
"unit_id": "…",
|
||||
"unit_name": "Ons",
|
||||
"unit_abbreviation": "ons",
|
||||
"unit_price": 4500,
|
||||
"total_price": 18900
|
||||
}
|
||||
```
|
||||
|
||||
Semuanya `null` atau absen untuk produk biasa, jadi cabangkan tampilan pada `weight`:
|
||||
|
||||
| Kondisi | Tampilkan |
|
||||
|---|---|
|
||||
| `weight == null` | `2 × Rp 25.000` |
|
||||
| `weight != null` | `4,2 ons × Rp 4.500` |
|
||||
|
||||
**Jangan menampilkan `quantity` untuk baris berbobot** — nilainya selalu 1 dan akan
|
||||
terbaca seperti "satu ons". Gunakan `weight` dengan `unit_abbreviation`, dan pakai koma
|
||||
desimal sesuai format Indonesia.
|
||||
|
||||
---
|
||||
|
||||
## 5. Void, refund, split bill
|
||||
|
||||
Baris berbobot bersifat **utuh**: dibatalkan seluruhnya atau tidak sama sekali. Karena
|
||||
`quantity`-nya 1, semua endpoint cukup dikirimi `1`, dan backend menghitung nilai
|
||||
rupiahnya dari `weight`.
|
||||
|
||||
| Aksi | Endpoint | Field untuk baris berbobot |
|
||||
|---|---|---|
|
||||
| Void per item | `POST /orders/void` | `items[].quantity: 1` |
|
||||
| Refund per item | `POST /orders/:id/refund` | `order_items[].refund_quantity: 1` (atau kosongkan) |
|
||||
| Split bill per item | `POST /orders/split-bill` | `items[].quantity: 1` = bayar baris itu penuh |
|
||||
|
||||
```json
|
||||
{
|
||||
"order_id": "…",
|
||||
"reason": "Salah timbang",
|
||||
"type": "ITEM",
|
||||
"items": [
|
||||
{ "order_item_id": "<baris 4,2 ons>", "quantity": 1 }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Untuk split bill, baris berbobot hanya bisa berstatus belum dibayar atau lunas — tidak
|
||||
ada nilai di antaranya. Sembunyikan stepper jumlah pada baris berbobot, ganti dengan
|
||||
tombol pilih baris.
|
||||
|
||||
**Batasan yang disengaja.** Mengembalikan *sebagian berat* — 1 ons dari baris 4,2 ons —
|
||||
tidak didukung. Koreksi salah timbang ditangani dengan void baris itu lalu input ulang,
|
||||
sehingga jejak auditnya tetap jujur.
|
||||
|
||||
---
|
||||
|
||||
## 6. Referensi error
|
||||
|
||||
Semua error mengikuti amplop standar. Pesan validasi baru muncul dengan kode `900`:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"data": null,
|
||||
"errors": [
|
||||
{ "code": "900", "entity": "ORDER",
|
||||
"cause": "product Ikan Tude is sold by weight and requires a weight" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
| Pesan (`cause`) | Penyebab | Perbaikan di klien |
|
||||
|---|---|---|
|
||||
| `… is sold by weight and requires a weight` | Produk `sell_by: "weight"` dikirim tanpa `weight` | Wajibkan input timbangan sebelum item masuk keranjang |
|
||||
| `… is not sold by weight and must not carry a weight` | `weight` dikirim untuk produk satuan | Kirim `weight` hanya bila `sell_by == "weight"` |
|
||||
| `weight for … must be greater than 0` | Berat nol, negatif, atau membulat ke nol | Validasi minimal `0.001` di keypad |
|
||||
| `quantity for … must be at least 1` | Produk satuan dengan `quantity` ≤ 0 | Perilaku lama, tidak berubah |
|
||||
|
||||
Pesan menyebut **nama produk**, sehingga bisa ditampilkan apa adanya ke kasir.
|
||||
|
||||
### Setup produk (Backoffice)
|
||||
|
||||
| Pesan (`cause`) | Penyebab | Perbaikan di klien |
|
||||
|---|---|---|
|
||||
| `unit_id is required when sell_by is 'weight'` | Produk timbangan dibuat tanpa satuan | Wajibkan pemilih satuan saat Timbangan dipilih |
|
||||
| `sell_by must be either 'unit' or 'weight'` | Nilai `sell_by` di luar dua itu | Kirim persis `"unit"` atau `"weight"` |
|
||||
| `product '…' is sold by weight and requires a unit_id` | Update membuat produk jadi timbangan tanpa satuan | Kirim `unit_id` bersama perubahan `sell_by` |
|
||||
|
||||
---
|
||||
|
||||
## 7. Kompatibilitas mundur
|
||||
|
||||
- Semua produk lama otomatis `sell_by: "unit"`. Perilakunya identik dengan sebelumnya.
|
||||
- `weight` opsional di request. Klien yang tidak mengenalnya tetap berfungsi penuh untuk
|
||||
produk satuan.
|
||||
- Field baru di response semuanya `omitempty` — tidak muncul untuk baris biasa, jadi
|
||||
parser lama tidak terganggu.
|
||||
- `quantity` tetap **integer** di seluruh API. Tidak ada field yang berubah tipe.
|
||||
|
||||
Yang tidak berfungsi di klien lama hanyalah menjual produk timbangan itu sendiri —
|
||||
permintaannya ditolak dengan pesan jelas, bukan gagal diam-diam.
|
||||
|
||||
---
|
||||
|
||||
## 8. Batasan yang diketahui
|
||||
|
||||
- **Pembulatan uang ke 2 desimal.** `4,237 ons × Rp 4.500` tersimpan `Rp 19.066,50`,
|
||||
bukan dibulatkan ke rupiah utuh. Bila kasir harus menerima rupiah penuh, ini perlu
|
||||
diputuskan dan diubah di backend lebih dulu (`RoundMoney`, satu tempat).
|
||||
- **Stok belum otomatis berkurang** saat penjualan — untuk produk timbangan maupun
|
||||
produk biasa. Pengurangan stok belum tersambung di backend, jadi jangan menampilkan
|
||||
sisa stok yang mengandaikan itu berjalan.
|
||||
|
||||
---
|
||||
|
||||
## 9. Checklist per tim
|
||||
|
||||
**Backoffice Website**
|
||||
|
||||
- [ ] CRUD satuan tersedia di menu master data
|
||||
- [ ] Form produk punya pilihan cara jual: Satuan / Timbangan
|
||||
- [ ] Pemilih satuan menjadi wajib saat Timbangan dipilih
|
||||
- [ ] Label harga ikut satuan — "Harga per ons"
|
||||
- [ ] `sell_by` dikunci untuk produk yang sudah bertransaksi
|
||||
- [ ] Daftar produk menandai mana yang dijual per timbangan
|
||||
- [ ] Laporan harian menampilkan kolom Berat
|
||||
|
||||
**POS Mobile**
|
||||
|
||||
- [ ] Menu membaca `sell_by` tiap produk
|
||||
- [ ] Papan angka desimal untuk produk timbangan
|
||||
- [ ] Kirim `quantity: 1` + `weight`
|
||||
- [ ] Dua penimbangan menjadi dua baris, tidak digabung
|
||||
- [ ] Keranjang & struk menampilkan `4,2 ons × Rp 4.500`
|
||||
- [ ] Void & refund baris berbobot bersifat utuh
|
||||
- [ ] Split bill: pilih baris, bukan stepper jumlah
|
||||
- [ ] Pesan error validasi ditampilkan ke kasir
|
||||
@@ -6,6 +6,7 @@ import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
|
||||
import '../../../common/service/device_info_service.dart';
|
||||
import '../../../common/service/unit_registry.dart';
|
||||
import '../../../common/service/fcm_service.dart';
|
||||
import '../../../domain/auth/auth.dart';
|
||||
|
||||
@@ -18,11 +19,13 @@ class LoginFormBloc extends Bloc<LoginFormEvent, LoginFormState> {
|
||||
final IAuthRepository _authRepository;
|
||||
final FcmService _fcmService;
|
||||
final DeviceInfoService _deviceInfoService;
|
||||
final UnitRegistry _unitRegistry;
|
||||
|
||||
LoginFormBloc(
|
||||
this._authRepository,
|
||||
this._fcmService,
|
||||
this._deviceInfoService,
|
||||
this._unitRegistry,
|
||||
) : super(LoginFormState.initial()) {
|
||||
on<LoginFormEvent>(_onLoginFormEvent);
|
||||
}
|
||||
@@ -62,6 +65,12 @@ class LoginFormBloc extends Bloc<LoginFormEvent, LoginFormState> {
|
||||
devicePayload: devicePayload,
|
||||
);
|
||||
|
||||
// Master satuan diambil sekali per sesi supaya keranjang bisa
|
||||
// menulis "4,2 ons" tanpa menunggu request saat kasir menimbang.
|
||||
if (failureOrLogin.isRight()) {
|
||||
await _unitRegistry.load(forceRefresh: true);
|
||||
}
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
isSubmitting: false,
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'dart:developer';
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:injectable/injectable.dart' hide Order;
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
import '../../../common/types/order_type.dart';
|
||||
import '../../../domain/delivery/delivery.dart';
|
||||
@@ -19,6 +20,33 @@ class CheckoutFormBloc extends Bloc<CheckoutFormEvent, CheckoutFormState> {
|
||||
CheckoutFormBloc() : super(CheckoutFormState.initial()) {
|
||||
on<CheckoutFormEvent>(_onCheckoutFormEvent);
|
||||
}
|
||||
|
||||
/// Baris berbobot dihitung `weight × harga`, baris cacah `quantity × harga`.
|
||||
/// Satu tempat saja supaya total keranjang, subtotal, dan ringkasan tidak
|
||||
/// pernah berbeda hitungan.
|
||||
CheckoutFormState _recalculate(
|
||||
CheckoutFormState state,
|
||||
List<ProductQuantity> items,
|
||||
) {
|
||||
final totalQuantity = items.fold<int>(
|
||||
0,
|
||||
// Baris berbobot selalu satu penimbangan, jadi dihitung satu.
|
||||
(sum, item) => sum + (item.isWeighted ? 1 : item.quantity),
|
||||
);
|
||||
|
||||
final totalPrice = items.fold<double>(
|
||||
0,
|
||||
(sum, item) => sum + item.lineTotal,
|
||||
);
|
||||
|
||||
return state.copyWith(
|
||||
items: items,
|
||||
totalQuantity: totalQuantity,
|
||||
totalPrice: totalPrice.toInt(),
|
||||
isLoading: false,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onCheckoutFormEvent(
|
||||
CheckoutFormEvent event,
|
||||
Emitter<CheckoutFormState> emit,
|
||||
@@ -26,12 +54,21 @@ class CheckoutFormBloc extends Bloc<CheckoutFormEvent, CheckoutFormState> {
|
||||
return event.map(
|
||||
addItem: (e) async {
|
||||
final currentState = state;
|
||||
|
||||
// Produk timbangan tidak punya stepper — masuk lewat addWeightedItem
|
||||
// setelah kasir memasukkan angka timbangan.
|
||||
if (e.product.isSoldByWeight) {
|
||||
log('⚖️ ${e.product.name} dijual per timbangan, butuh berat');
|
||||
return;
|
||||
}
|
||||
|
||||
emit(currentState.copyWith(isLoading: true));
|
||||
|
||||
List<ProductQuantity> items = [...currentState.items];
|
||||
|
||||
final index = items.indexWhere(
|
||||
(element) =>
|
||||
!element.isWeighted &&
|
||||
element.product.id == e.product.id &&
|
||||
element.variant?.id == e.variant?.id,
|
||||
);
|
||||
@@ -49,35 +86,47 @@ class CheckoutFormBloc extends Bloc<CheckoutFormEvent, CheckoutFormState> {
|
||||
quantity: 1,
|
||||
variant: e.variant,
|
||||
notes: '',
|
||||
lineId: const Uuid().v4(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
log('🛒 Items updated: ${items.length} items total ${items.toList()}');
|
||||
|
||||
final totalQuantity = items.fold<int>(
|
||||
0,
|
||||
(sum, item) => sum + item.quantity,
|
||||
);
|
||||
emit(_recalculate(currentState, items));
|
||||
},
|
||||
addWeightedItem: (e) async {
|
||||
final currentState = state;
|
||||
emit(currentState.copyWith(isLoading: true));
|
||||
|
||||
final totalPrice = state.items.isEmpty
|
||||
? 0.0
|
||||
: state.items
|
||||
.map(
|
||||
(e) =>
|
||||
(e.product.price * e.quantity) +
|
||||
(e.variant?.priceModifier ?? 0),
|
||||
)
|
||||
.reduce((value, element) => value + element);
|
||||
|
||||
emit(
|
||||
currentState.copyWith(
|
||||
items: items,
|
||||
totalQuantity: totalQuantity,
|
||||
totalPrice: totalPrice.toInt(),
|
||||
isLoading: false,
|
||||
// Sengaja tidak digabung dengan baris lain: dua penimbangan adalah dua
|
||||
// ikan berbeda, masing-masing bisa di-void atau dibayar terpisah.
|
||||
final items = [
|
||||
...currentState.items,
|
||||
ProductQuantity(
|
||||
product: e.product,
|
||||
variant: e.variant,
|
||||
quantity: 1,
|
||||
notes: '',
|
||||
lineId: const Uuid().v4(),
|
||||
weight: e.weight,
|
||||
),
|
||||
);
|
||||
];
|
||||
|
||||
log('⚖️ Penimbangan baru: ${e.product.name} ${e.weight}');
|
||||
|
||||
emit(_recalculate(currentState, items));
|
||||
},
|
||||
updateItemWeight: (e) async {
|
||||
final currentState = state;
|
||||
final items = [...currentState.items];
|
||||
|
||||
final index = items.indexWhere((item) => item.lineId == e.lineId);
|
||||
if (index == -1) return;
|
||||
|
||||
items[index] = items[index].copyWith(weight: e.weight);
|
||||
|
||||
emit(_recalculate(currentState, items));
|
||||
},
|
||||
removeItem: (e) async {
|
||||
final currentState = state;
|
||||
@@ -86,6 +135,7 @@ class CheckoutFormBloc extends Bloc<CheckoutFormEvent, CheckoutFormState> {
|
||||
|
||||
final index = items.indexWhere(
|
||||
(element) =>
|
||||
!element.isWeighted &&
|
||||
element.product.id == e.product.id &&
|
||||
element.variant?.id == e.variant?.id,
|
||||
);
|
||||
@@ -103,32 +153,18 @@ class CheckoutFormBloc extends Bloc<CheckoutFormEvent, CheckoutFormState> {
|
||||
}
|
||||
}
|
||||
|
||||
final totalQuantity = items.fold<int>(
|
||||
0,
|
||||
(sum, item) => sum + item.quantity,
|
||||
);
|
||||
final totalPrice = items.fold<int>(
|
||||
0,
|
||||
(sum, item) =>
|
||||
sum +
|
||||
(item.quantity *
|
||||
(item.variant?.priceModifier.toInt() ??
|
||||
item.product.price.toInt())),
|
||||
);
|
||||
log('🗑️ Item removed. Total items: ${items.length}');
|
||||
|
||||
log(
|
||||
'🗑️ Item removed. Total items: ${items.length}, totalQuantity: $totalQuantity, totalPrice: $totalPrice',
|
||||
);
|
||||
emit(_recalculate(currentState, items));
|
||||
},
|
||||
removeLine: (e) async {
|
||||
final currentState = state;
|
||||
final items = [...currentState.items]
|
||||
..removeWhere((item) => item.lineId == e.lineId);
|
||||
|
||||
// Emit state baru
|
||||
emit(
|
||||
currentState.copyWith(
|
||||
items: items,
|
||||
totalQuantity: totalQuantity,
|
||||
totalPrice: totalPrice,
|
||||
isLoading: false,
|
||||
),
|
||||
);
|
||||
log('🗑️ Baris dihapus. Sisa ${items.length} baris');
|
||||
|
||||
emit(_recalculate(currentState, items));
|
||||
},
|
||||
started: (e) async {
|
||||
emit(CheckoutFormState.initial().copyWith(isLoading: true));
|
||||
@@ -153,14 +189,13 @@ class CheckoutFormBloc extends Bloc<CheckoutFormEvent, CheckoutFormState> {
|
||||
);
|
||||
}
|
||||
},
|
||||
updateItemNotes: (e) async {
|
||||
updateLineNotes: (e) async {
|
||||
final currentState = state;
|
||||
|
||||
// Clone list items agar tidak mutasi langsung
|
||||
// Catatan diikat ke baris, bukan ke produk: dua penimbangan produk yang
|
||||
// sama bisa punya catatan berbeda.
|
||||
final items = [...currentState.items];
|
||||
final index = items.indexWhere(
|
||||
(element) => element.product.id == e.product.id,
|
||||
);
|
||||
final index = items.indexWhere((item) => item.lineId == e.lineId);
|
||||
|
||||
if (index != -1) {
|
||||
items[index] = items[index].copyWith(notes: e.notes);
|
||||
@@ -181,8 +216,8 @@ class CheckoutFormBloc extends Bloc<CheckoutFormEvent, CheckoutFormState> {
|
||||
emit(state.copyWith(orderAdded: e.order));
|
||||
},
|
||||
customerNameChanged: (e) async {
|
||||
emit(state.copyWith(customerName: e.name,));
|
||||
}
|
||||
emit(state.copyWith(customerName: e.name));
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -10,15 +10,32 @@ class CheckoutFormEvent with _$CheckoutFormEvent {
|
||||
ProductVariant? variant,
|
||||
) = _AddItem;
|
||||
|
||||
/// Satu penimbangan = satu baris baru. Dipanggil setiap kali kasir selesai
|
||||
/// memasukkan angka timbangan, meski produknya sama dengan baris lain.
|
||||
const factory CheckoutFormEvent.addWeightedItem(
|
||||
Product product,
|
||||
ProductVariant? variant,
|
||||
double weight,
|
||||
) = _AddWeightedItem;
|
||||
|
||||
/// Koreksi angka timbangan pada satu baris tertentu.
|
||||
const factory CheckoutFormEvent.updateItemWeight(
|
||||
String lineId,
|
||||
double weight,
|
||||
) = _UpdateItemWeight;
|
||||
|
||||
const factory CheckoutFormEvent.removeItem(
|
||||
Product product,
|
||||
ProductVariant? variant,
|
||||
) = _RemoveItem;
|
||||
|
||||
const factory CheckoutFormEvent.updateItemNotes(
|
||||
Product product,
|
||||
/// Hapus satu baris utuh — satu-satunya cara membatalkan baris berbobot.
|
||||
const factory CheckoutFormEvent.removeLine(String lineId) = _RemoveLine;
|
||||
|
||||
const factory CheckoutFormEvent.updateLineNotes(
|
||||
String lineId,
|
||||
String notes,
|
||||
) = _UpdateItemNotes;
|
||||
) = _UpdateLineNotes;
|
||||
|
||||
const factory CheckoutFormEvent.updateOrderType(OrderType orderType) =
|
||||
_UpdateOrderType;
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:dartz/dartz.dart' hide Order;
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:injectable/injectable.dart' hide Order;
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
import '../../../common/types/order_type.dart';
|
||||
import '../../../domain/customer/customer.dart';
|
||||
@@ -59,15 +60,7 @@ class OrderFormBloc extends Bloc<OrderFormEvent, OrderFormState> {
|
||||
notes: '',
|
||||
customerName: state.customerName ?? "",
|
||||
orderItems: e.items
|
||||
.map(
|
||||
(item) => OrderItemRequest(
|
||||
productId: item.product.id,
|
||||
productVariantId: item.variant?.id ?? "",
|
||||
quantity: item.quantity,
|
||||
unitPrice: item.product.price.toInt(),
|
||||
notes: item.notes,
|
||||
),
|
||||
)
|
||||
.map(OrderItemRequest.fromProductQuantity)
|
||||
.toList(),
|
||||
);
|
||||
|
||||
@@ -99,15 +92,7 @@ class OrderFormBloc extends Bloc<OrderFormEvent, OrderFormState> {
|
||||
notes: '',
|
||||
customerName: state.customerName ?? "",
|
||||
orderItems: e.items
|
||||
.map(
|
||||
(item) => OrderItemRequest(
|
||||
productId: item.product.id,
|
||||
productVariantId: item.variant?.id ?? "",
|
||||
quantity: item.quantity,
|
||||
unitPrice: item.product.price.toInt(),
|
||||
notes: item.notes,
|
||||
),
|
||||
)
|
||||
.map(OrderItemRequest.fromProductQuantity)
|
||||
.toList(),
|
||||
);
|
||||
|
||||
@@ -123,34 +108,35 @@ class OrderFormBloc extends Bloc<OrderFormEvent, OrderFormState> {
|
||||
addItemOrder: (e) async {
|
||||
Either<OrderFailure, Order> failureOrAddItemOrder;
|
||||
|
||||
// Generate new idempotency key saat user intent (tap Add Items)
|
||||
// Kalau sedang retry, pakai key yang sama
|
||||
final idempotencyKey = state.addItemIdempotencyKey ?? const Uuid().v4();
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
isAddingItemOrder: true,
|
||||
failureOrAddItemOrder: none(),
|
||||
addItemIdempotencyKey: idempotencyKey,
|
||||
),
|
||||
);
|
||||
|
||||
final request = e.items
|
||||
.map(
|
||||
(item) => AddItemOrderRequest(
|
||||
productId: item.product.id,
|
||||
productVariantId: item.variant?.id ?? '',
|
||||
quantity: item.quantity,
|
||||
unitPrice: item.product.price.toInt(),
|
||||
notes: item.notes,
|
||||
),
|
||||
)
|
||||
.map(AddItemOrderRequest.fromProductQuantity)
|
||||
.toList();
|
||||
|
||||
failureOrAddItemOrder = await _repository.addItemOrder(
|
||||
id: e.orderId,
|
||||
request: request,
|
||||
idempotencyKey: idempotencyKey,
|
||||
);
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
isAddingItemOrder: false,
|
||||
failureOrAddItemOrder: optionOf(failureOrAddItemOrder),
|
||||
// Clear key on success, keep on failure for retry
|
||||
addItemIdempotencyKey:
|
||||
failureOrAddItemOrder.isRight() ? null : idempotencyKey,
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
@@ -1584,6 +1584,11 @@ mixin _$OrderFormState {
|
||||
bool get isCreatingWithPayment => throw _privateConstructorUsedError;
|
||||
bool get isAddingItemOrder => throw _privateConstructorUsedError;
|
||||
|
||||
/// Idempotency key untuk add item order.
|
||||
/// Di-generate saat user tap "Add Items", dipakai ulang saat retry,
|
||||
/// di-clear setelah sukses.
|
||||
String? get addItemIdempotencyKey => throw _privateConstructorUsedError;
|
||||
|
||||
/// Create a copy of OrderFormState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@@ -1608,6 +1613,7 @@ abstract class $OrderFormStateCopyWith<$Res> {
|
||||
bool isCreating,
|
||||
bool isCreatingWithPayment,
|
||||
bool isAddingItemOrder,
|
||||
String? addItemIdempotencyKey,
|
||||
});
|
||||
|
||||
$PaymentMethodCopyWith<$Res>? get paymentMethod;
|
||||
@@ -1638,6 +1644,7 @@ class _$OrderFormStateCopyWithImpl<$Res, $Val extends OrderFormState>
|
||||
Object? isCreating = null,
|
||||
Object? isCreatingWithPayment = null,
|
||||
Object? isAddingItemOrder = null,
|
||||
Object? addItemIdempotencyKey = freezed,
|
||||
}) {
|
||||
return _then(
|
||||
_value.copyWith(
|
||||
@@ -1678,6 +1685,10 @@ class _$OrderFormStateCopyWithImpl<$Res, $Val extends OrderFormState>
|
||||
? _value.isAddingItemOrder
|
||||
: isAddingItemOrder // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
addItemIdempotencyKey: freezed == addItemIdempotencyKey
|
||||
? _value.addItemIdempotencyKey
|
||||
: addItemIdempotencyKey // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
)
|
||||
as $Val,
|
||||
);
|
||||
@@ -1731,6 +1742,7 @@ abstract class _$$OrderFormStateImplCopyWith<$Res>
|
||||
bool isCreating,
|
||||
bool isCreatingWithPayment,
|
||||
bool isAddingItemOrder,
|
||||
String? addItemIdempotencyKey,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -1762,6 +1774,7 @@ class __$$OrderFormStateImplCopyWithImpl<$Res>
|
||||
Object? isCreating = null,
|
||||
Object? isCreatingWithPayment = null,
|
||||
Object? isAddingItemOrder = null,
|
||||
Object? addItemIdempotencyKey = freezed,
|
||||
}) {
|
||||
return _then(
|
||||
_$OrderFormStateImpl(
|
||||
@@ -1801,6 +1814,10 @@ class __$$OrderFormStateImplCopyWithImpl<$Res>
|
||||
? _value.isAddingItemOrder
|
||||
: isAddingItemOrder // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
addItemIdempotencyKey: freezed == addItemIdempotencyKey
|
||||
? _value.addItemIdempotencyKey
|
||||
: addItemIdempotencyKey // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -1821,6 +1838,7 @@ class _$OrderFormStateImpl
|
||||
this.isCreating = false,
|
||||
this.isCreatingWithPayment = false,
|
||||
this.isAddingItemOrder = false,
|
||||
this.addItemIdempotencyKey,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -1845,9 +1863,15 @@ class _$OrderFormStateImpl
|
||||
@JsonKey()
|
||||
final bool isAddingItemOrder;
|
||||
|
||||
/// Idempotency key untuk add item order.
|
||||
/// Di-generate saat user tap "Add Items", dipakai ulang saat retry,
|
||||
/// di-clear setelah sukses.
|
||||
@override
|
||||
final String? addItemIdempotencyKey;
|
||||
|
||||
@override
|
||||
String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) {
|
||||
return 'OrderFormState(paymentMethod: $paymentMethod, customerName: $customerName, customer: $customer, failureOrCreateOrder: $failureOrCreateOrder, failureOrCreateOrderWithPayment: $failureOrCreateOrderWithPayment, failureOrAddItemOrder: $failureOrAddItemOrder, isCreating: $isCreating, isCreatingWithPayment: $isCreatingWithPayment, isAddingItemOrder: $isAddingItemOrder)';
|
||||
return 'OrderFormState(paymentMethod: $paymentMethod, customerName: $customerName, customer: $customer, failureOrCreateOrder: $failureOrCreateOrder, failureOrCreateOrderWithPayment: $failureOrCreateOrderWithPayment, failureOrAddItemOrder: $failureOrAddItemOrder, isCreating: $isCreating, isCreatingWithPayment: $isCreatingWithPayment, isAddingItemOrder: $isAddingItemOrder, addItemIdempotencyKey: $addItemIdempotencyKey)';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -1868,7 +1892,10 @@ class _$OrderFormStateImpl
|
||||
..add(DiagnosticsProperty('failureOrAddItemOrder', failureOrAddItemOrder))
|
||||
..add(DiagnosticsProperty('isCreating', isCreating))
|
||||
..add(DiagnosticsProperty('isCreatingWithPayment', isCreatingWithPayment))
|
||||
..add(DiagnosticsProperty('isAddingItemOrder', isAddingItemOrder));
|
||||
..add(DiagnosticsProperty('isAddingItemOrder', isAddingItemOrder))
|
||||
..add(
|
||||
DiagnosticsProperty('addItemIdempotencyKey', addItemIdempotencyKey),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -1897,7 +1924,9 @@ class _$OrderFormStateImpl
|
||||
(identical(other.isCreatingWithPayment, isCreatingWithPayment) ||
|
||||
other.isCreatingWithPayment == isCreatingWithPayment) &&
|
||||
(identical(other.isAddingItemOrder, isAddingItemOrder) ||
|
||||
other.isAddingItemOrder == isAddingItemOrder));
|
||||
other.isAddingItemOrder == isAddingItemOrder) &&
|
||||
(identical(other.addItemIdempotencyKey, addItemIdempotencyKey) ||
|
||||
other.addItemIdempotencyKey == addItemIdempotencyKey));
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -1912,6 +1941,7 @@ class _$OrderFormStateImpl
|
||||
isCreating,
|
||||
isCreatingWithPayment,
|
||||
isAddingItemOrder,
|
||||
addItemIdempotencyKey,
|
||||
);
|
||||
|
||||
/// Create a copy of OrderFormState
|
||||
@@ -1938,6 +1968,7 @@ abstract class _OrderFormState implements OrderFormState {
|
||||
final bool isCreating,
|
||||
final bool isCreatingWithPayment,
|
||||
final bool isAddingItemOrder,
|
||||
final String? addItemIdempotencyKey,
|
||||
}) = _$OrderFormStateImpl;
|
||||
|
||||
@override
|
||||
@@ -1959,6 +1990,12 @@ abstract class _OrderFormState implements OrderFormState {
|
||||
@override
|
||||
bool get isAddingItemOrder;
|
||||
|
||||
/// Idempotency key untuk add item order.
|
||||
/// Di-generate saat user tap "Add Items", dipakai ulang saat retry,
|
||||
/// di-clear setelah sukses.
|
||||
@override
|
||||
String? get addItemIdempotencyKey;
|
||||
|
||||
/// Create a copy of OrderFormState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
|
||||
@@ -13,6 +13,10 @@ class OrderFormState with _$OrderFormState {
|
||||
@Default(false) bool isCreating,
|
||||
@Default(false) bool isCreatingWithPayment,
|
||||
@Default(false) bool isAddingItemOrder,
|
||||
/// Idempotency key untuk add item order.
|
||||
/// Di-generate saat user tap "Add Items", dipakai ulang saat retry,
|
||||
/// di-clear setelah sukses.
|
||||
String? addItemIdempotencyKey,
|
||||
}) = _OrderFormState;
|
||||
|
||||
factory OrderFormState.initial() => OrderFormState(
|
||||
|
||||
@@ -4,6 +4,7 @@ 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;
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
import '../../../common/types/split_type.dart';
|
||||
import '../../../domain/order/order.dart';
|
||||
@@ -38,7 +39,15 @@ class PaymentFormBloc extends Bloc<PaymentFormEvent, PaymentFormState> {
|
||||
submitted: (e) async {
|
||||
Either<OrderFailure, Payment> failureOrPayment;
|
||||
|
||||
emit(state.copyWith(isSubmitting: true, failureOrPayment: none()));
|
||||
// Generate new idempotency key saat user intent (tap Bayar)
|
||||
// Kalau sedang retry (isSubmitting sebelumnya gagal), pakai key yang sama
|
||||
final idempotencyKey = state.idempotencyKey ?? const Uuid().v4();
|
||||
|
||||
emit(state.copyWith(
|
||||
isSubmitting: true,
|
||||
failureOrPayment: none(),
|
||||
idempotencyKey: idempotencyKey,
|
||||
));
|
||||
|
||||
final request = PaymentRequest(
|
||||
orderId: state.order.id,
|
||||
@@ -58,20 +67,32 @@ class PaymentFormBloc extends Bloc<PaymentFormEvent, PaymentFormState> {
|
||||
.toList(),
|
||||
);
|
||||
|
||||
failureOrPayment = await _repository.createPayment(request: request);
|
||||
failureOrPayment = await _repository.createPayment(
|
||||
request: request,
|
||||
idempotencyKey: idempotencyKey,
|
||||
);
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
isSubmitting: false,
|
||||
failureOrPayment: optionOf(failureOrPayment),
|
||||
// Clear key on success, keep on failure for retry
|
||||
idempotencyKey: failureOrPayment.isRight() ? null : idempotencyKey,
|
||||
),
|
||||
);
|
||||
},
|
||||
submittedSplitBill: (e) async {
|
||||
Either<OrderFailure, Payment> failureOrPayment;
|
||||
|
||||
// Generate new idempotency key untuk split bill
|
||||
final idempotencyKey = state.splitBillIdempotencyKey ?? const Uuid().v4();
|
||||
|
||||
emit(
|
||||
state.copyWith(isSubmitting: true, failureOrPaymentSplitBill: none()),
|
||||
state.copyWith(
|
||||
isSubmitting: true,
|
||||
failureOrPaymentSplitBill: none(),
|
||||
splitBillIdempotencyKey: idempotencyKey,
|
||||
),
|
||||
);
|
||||
|
||||
final request = PaymentSplitBillRequest(
|
||||
@@ -93,12 +114,18 @@ class PaymentFormBloc extends Bloc<PaymentFormEvent, PaymentFormState> {
|
||||
|
||||
log(request.toString());
|
||||
|
||||
failureOrPayment = await _repository.createSplitBill(request);
|
||||
failureOrPayment = await _repository.createSplitBill(
|
||||
request,
|
||||
idempotencyKey: idempotencyKey,
|
||||
);
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
isSubmitting: false,
|
||||
failureOrPaymentSplitBill: optionOf(failureOrPayment),
|
||||
// Clear key on success, keep on failure for retry
|
||||
splitBillIdempotencyKey:
|
||||
failureOrPayment.isRight() ? null : idempotencyKey,
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
@@ -813,6 +813,14 @@ mixin _$PaymentFormState {
|
||||
PaymentMethod? get paymentMethod => throw _privateConstructorUsedError;
|
||||
bool get isSubmitting => throw _privateConstructorUsedError;
|
||||
|
||||
/// Idempotency key untuk payment submission.
|
||||
/// Di-generate saat user tap "Bayar", dipakai ulang saat retry,
|
||||
/// di-clear setelah sukses.
|
||||
String? get idempotencyKey => throw _privateConstructorUsedError;
|
||||
|
||||
/// Idempotency key untuk split bill submission.
|
||||
String? get splitBillIdempotencyKey => throw _privateConstructorUsedError;
|
||||
|
||||
/// Create a copy of PaymentFormState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@@ -834,6 +842,8 @@ abstract class $PaymentFormStateCopyWith<$Res> {
|
||||
Option<Either<OrderFailure, Payment>> failureOrPaymentSplitBill,
|
||||
PaymentMethod? paymentMethod,
|
||||
bool isSubmitting,
|
||||
String? idempotencyKey,
|
||||
String? splitBillIdempotencyKey,
|
||||
});
|
||||
|
||||
$OrderCopyWith<$Res> get order;
|
||||
@@ -861,6 +871,8 @@ class _$PaymentFormStateCopyWithImpl<$Res, $Val extends PaymentFormState>
|
||||
Object? failureOrPaymentSplitBill = null,
|
||||
Object? paymentMethod = freezed,
|
||||
Object? isSubmitting = null,
|
||||
Object? idempotencyKey = freezed,
|
||||
Object? splitBillIdempotencyKey = freezed,
|
||||
}) {
|
||||
return _then(
|
||||
_value.copyWith(
|
||||
@@ -888,6 +900,14 @@ class _$PaymentFormStateCopyWithImpl<$Res, $Val extends PaymentFormState>
|
||||
? _value.isSubmitting
|
||||
: isSubmitting // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
idempotencyKey: freezed == idempotencyKey
|
||||
? _value.idempotencyKey
|
||||
: idempotencyKey // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
splitBillIdempotencyKey: freezed == splitBillIdempotencyKey
|
||||
? _value.splitBillIdempotencyKey
|
||||
: splitBillIdempotencyKey // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
)
|
||||
as $Val,
|
||||
);
|
||||
@@ -934,6 +954,8 @@ abstract class _$$PaymentFormStateImplCopyWith<$Res>
|
||||
Option<Either<OrderFailure, Payment>> failureOrPaymentSplitBill,
|
||||
PaymentMethod? paymentMethod,
|
||||
bool isSubmitting,
|
||||
String? idempotencyKey,
|
||||
String? splitBillIdempotencyKey,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -962,6 +984,8 @@ class __$$PaymentFormStateImplCopyWithImpl<$Res>
|
||||
Object? failureOrPaymentSplitBill = null,
|
||||
Object? paymentMethod = freezed,
|
||||
Object? isSubmitting = null,
|
||||
Object? idempotencyKey = freezed,
|
||||
Object? splitBillIdempotencyKey = freezed,
|
||||
}) {
|
||||
return _then(
|
||||
_$PaymentFormStateImpl(
|
||||
@@ -989,6 +1013,14 @@ class __$$PaymentFormStateImplCopyWithImpl<$Res>
|
||||
? _value.isSubmitting
|
||||
: isSubmitting // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
idempotencyKey: freezed == idempotencyKey
|
||||
? _value.idempotencyKey
|
||||
: idempotencyKey // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
splitBillIdempotencyKey: freezed == splitBillIdempotencyKey
|
||||
? _value.splitBillIdempotencyKey
|
||||
: splitBillIdempotencyKey // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -1004,6 +1036,8 @@ class _$PaymentFormStateImpl implements _PaymentFormState {
|
||||
required this.failureOrPaymentSplitBill,
|
||||
this.paymentMethod,
|
||||
this.isSubmitting = false,
|
||||
this.idempotencyKey,
|
||||
this.splitBillIdempotencyKey,
|
||||
}) : _pendingItems = pendingItems;
|
||||
|
||||
@override
|
||||
@@ -1026,9 +1060,19 @@ class _$PaymentFormStateImpl implements _PaymentFormState {
|
||||
@JsonKey()
|
||||
final bool isSubmitting;
|
||||
|
||||
/// Idempotency key untuk payment submission.
|
||||
/// Di-generate saat user tap "Bayar", dipakai ulang saat retry,
|
||||
/// di-clear setelah sukses.
|
||||
@override
|
||||
final String? idempotencyKey;
|
||||
|
||||
/// Idempotency key untuk split bill submission.
|
||||
@override
|
||||
final String? splitBillIdempotencyKey;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'PaymentFormState(order: $order, pendingItems: $pendingItems, failureOrPayment: $failureOrPayment, failureOrPaymentSplitBill: $failureOrPaymentSplitBill, paymentMethod: $paymentMethod, isSubmitting: $isSubmitting)';
|
||||
return 'PaymentFormState(order: $order, pendingItems: $pendingItems, failureOrPayment: $failureOrPayment, failureOrPaymentSplitBill: $failureOrPaymentSplitBill, paymentMethod: $paymentMethod, isSubmitting: $isSubmitting, idempotencyKey: $idempotencyKey, splitBillIdempotencyKey: $splitBillIdempotencyKey)';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -1051,7 +1095,14 @@ class _$PaymentFormStateImpl implements _PaymentFormState {
|
||||
(identical(other.paymentMethod, paymentMethod) ||
|
||||
other.paymentMethod == paymentMethod) &&
|
||||
(identical(other.isSubmitting, isSubmitting) ||
|
||||
other.isSubmitting == isSubmitting));
|
||||
other.isSubmitting == isSubmitting) &&
|
||||
(identical(other.idempotencyKey, idempotencyKey) ||
|
||||
other.idempotencyKey == idempotencyKey) &&
|
||||
(identical(
|
||||
other.splitBillIdempotencyKey,
|
||||
splitBillIdempotencyKey,
|
||||
) ||
|
||||
other.splitBillIdempotencyKey == splitBillIdempotencyKey));
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -1063,6 +1114,8 @@ class _$PaymentFormStateImpl implements _PaymentFormState {
|
||||
failureOrPaymentSplitBill,
|
||||
paymentMethod,
|
||||
isSubmitting,
|
||||
idempotencyKey,
|
||||
splitBillIdempotencyKey,
|
||||
);
|
||||
|
||||
/// Create a copy of PaymentFormState
|
||||
@@ -1086,6 +1139,8 @@ abstract class _PaymentFormState implements PaymentFormState {
|
||||
failureOrPaymentSplitBill,
|
||||
final PaymentMethod? paymentMethod,
|
||||
final bool isSubmitting,
|
||||
final String? idempotencyKey,
|
||||
final String? splitBillIdempotencyKey,
|
||||
}) = _$PaymentFormStateImpl;
|
||||
|
||||
@override
|
||||
@@ -1101,6 +1156,16 @@ abstract class _PaymentFormState implements PaymentFormState {
|
||||
@override
|
||||
bool get isSubmitting;
|
||||
|
||||
/// Idempotency key untuk payment submission.
|
||||
/// Di-generate saat user tap "Bayar", dipakai ulang saat retry,
|
||||
/// di-clear setelah sukses.
|
||||
@override
|
||||
String? get idempotencyKey;
|
||||
|
||||
/// Idempotency key untuk split bill submission.
|
||||
@override
|
||||
String? get splitBillIdempotencyKey;
|
||||
|
||||
/// Create a copy of PaymentFormState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
|
||||
@@ -9,6 +9,12 @@ class PaymentFormState with _$PaymentFormState {
|
||||
required Option<Either<OrderFailure, Payment>> failureOrPaymentSplitBill,
|
||||
PaymentMethod? paymentMethod,
|
||||
@Default(false) bool isSubmitting,
|
||||
/// Idempotency key untuk payment submission.
|
||||
/// Di-generate saat user tap "Bayar", dipakai ulang saat retry,
|
||||
/// di-clear setelah sukses.
|
||||
String? idempotencyKey,
|
||||
/// Idempotency key untuk split bill submission.
|
||||
String? splitBillIdempotencyKey,
|
||||
}) = _PaymentFormState;
|
||||
|
||||
factory PaymentFormState.initial() => PaymentFormState(
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
);
|
||||
|
||||
/// 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,
|
||||
];
|
||||
|
||||
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;
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
isPrinting: false,
|
||||
failureOrPrintStruck: optionOf(failureOrSuccess),
|
||||
isPrinting: true,
|
||||
order: order,
|
||||
jobId: state.jobId + 1,
|
||||
stations: [
|
||||
for (final station in stations) PrintStationResult.pending(station),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
cashier: (e) async {
|
||||
Either<PrinterFailure, Unit> failureOrSuccess;
|
||||
|
||||
emit(state.copyWith(isPrinting: true, failureOrPrintStruck: none()));
|
||||
|
||||
failureOrSuccess = await _printerRepository.printStruckCashier(
|
||||
order: e.order,
|
||||
await emit.forEach<List<PrintStationResult>>(
|
||||
_printerRepository.printStations(order: order, stations: stations),
|
||||
onData: (results) => state.copyWith(stations: results),
|
||||
);
|
||||
|
||||
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),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
// 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;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ 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;
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
import '../../../common/data/refund_data.dart';
|
||||
import '../../../domain/order/order.dart';
|
||||
@@ -34,7 +35,15 @@ class RefundFormBloc extends Bloc<RefundFormEvent, RefundFormState> {
|
||||
submitted: (e) async {
|
||||
Either<OrderFailure, Unit>? failureOrRefund;
|
||||
|
||||
emit(state.copyWith(isSubmitting: true, failureOrRefund: none()));
|
||||
// Generate new idempotency key saat user intent (tap Refund)
|
||||
// Kalau sedang retry, pakai key yang sama
|
||||
final idempotencyKey = state.idempotencyKey ?? const Uuid().v4();
|
||||
|
||||
emit(state.copyWith(
|
||||
isSubmitting: true,
|
||||
failureOrRefund: none(),
|
||||
idempotencyKey: idempotencyKey,
|
||||
));
|
||||
|
||||
failureOrRefund = await _repository.refundOrder(
|
||||
id: state.order.id,
|
||||
@@ -42,12 +51,15 @@ class RefundFormBloc extends Bloc<RefundFormEvent, RefundFormState> {
|
||||
? state.reason
|
||||
: state.refundReason?.value ?? '',
|
||||
refundAmount: state.order.totalAmount,
|
||||
idempotencyKey: idempotencyKey,
|
||||
);
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
isSubmitting: false,
|
||||
failureOrRefund: optionOf(failureOrRefund),
|
||||
// Clear key on success, keep on failure for retry
|
||||
idempotencyKey: failureOrRefund.isRight() ? null : idempotencyKey,
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
@@ -692,6 +692,11 @@ mixin _$RefundFormState {
|
||||
throw _privateConstructorUsedError;
|
||||
bool get isSubmitting => throw _privateConstructorUsedError;
|
||||
|
||||
/// Idempotency key untuk refund submission.
|
||||
/// Di-generate saat user tap "Refund", dipakai ulang saat retry,
|
||||
/// di-clear setelah sukses.
|
||||
String? get idempotencyKey => throw _privateConstructorUsedError;
|
||||
|
||||
/// Create a copy of RefundFormState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@@ -712,6 +717,7 @@ abstract class $RefundFormStateCopyWith<$Res> {
|
||||
RefundReason? refundReason,
|
||||
Option<Either<OrderFailure, Unit>> failureOrRefund,
|
||||
bool isSubmitting,
|
||||
String? idempotencyKey,
|
||||
});
|
||||
|
||||
$OrderCopyWith<$Res> get order;
|
||||
@@ -737,6 +743,7 @@ class _$RefundFormStateCopyWithImpl<$Res, $Val extends RefundFormState>
|
||||
Object? refundReason = freezed,
|
||||
Object? failureOrRefund = null,
|
||||
Object? isSubmitting = null,
|
||||
Object? idempotencyKey = freezed,
|
||||
}) {
|
||||
return _then(
|
||||
_value.copyWith(
|
||||
@@ -760,6 +767,10 @@ class _$RefundFormStateCopyWithImpl<$Res, $Val extends RefundFormState>
|
||||
? _value.isSubmitting
|
||||
: isSubmitting // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
idempotencyKey: freezed == idempotencyKey
|
||||
? _value.idempotencyKey
|
||||
: idempotencyKey // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
)
|
||||
as $Val,
|
||||
);
|
||||
@@ -791,6 +802,7 @@ abstract class _$$RefundFormStateImplCopyWith<$Res>
|
||||
RefundReason? refundReason,
|
||||
Option<Either<OrderFailure, Unit>> failureOrRefund,
|
||||
bool isSubmitting,
|
||||
String? idempotencyKey,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -816,6 +828,7 @@ class __$$RefundFormStateImplCopyWithImpl<$Res>
|
||||
Object? refundReason = freezed,
|
||||
Object? failureOrRefund = null,
|
||||
Object? isSubmitting = null,
|
||||
Object? idempotencyKey = freezed,
|
||||
}) {
|
||||
return _then(
|
||||
_$RefundFormStateImpl(
|
||||
@@ -839,6 +852,10 @@ class __$$RefundFormStateImplCopyWithImpl<$Res>
|
||||
? _value.isSubmitting
|
||||
: isSubmitting // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
idempotencyKey: freezed == idempotencyKey
|
||||
? _value.idempotencyKey
|
||||
: idempotencyKey // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -853,6 +870,7 @@ class _$RefundFormStateImpl implements _RefundFormState {
|
||||
this.refundReason,
|
||||
required this.failureOrRefund,
|
||||
this.isSubmitting = false,
|
||||
this.idempotencyKey,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -867,9 +885,15 @@ class _$RefundFormStateImpl implements _RefundFormState {
|
||||
@JsonKey()
|
||||
final bool isSubmitting;
|
||||
|
||||
/// Idempotency key untuk refund submission.
|
||||
/// Di-generate saat user tap "Refund", dipakai ulang saat retry,
|
||||
/// di-clear setelah sukses.
|
||||
@override
|
||||
final String? idempotencyKey;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'RefundFormState(order: $order, reason: $reason, refundReason: $refundReason, failureOrRefund: $failureOrRefund, isSubmitting: $isSubmitting)';
|
||||
return 'RefundFormState(order: $order, reason: $reason, refundReason: $refundReason, failureOrRefund: $failureOrRefund, isSubmitting: $isSubmitting, idempotencyKey: $idempotencyKey)';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -884,7 +908,9 @@ class _$RefundFormStateImpl implements _RefundFormState {
|
||||
(identical(other.failureOrRefund, failureOrRefund) ||
|
||||
other.failureOrRefund == failureOrRefund) &&
|
||||
(identical(other.isSubmitting, isSubmitting) ||
|
||||
other.isSubmitting == isSubmitting));
|
||||
other.isSubmitting == isSubmitting) &&
|
||||
(identical(other.idempotencyKey, idempotencyKey) ||
|
||||
other.idempotencyKey == idempotencyKey));
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -895,6 +921,7 @@ class _$RefundFormStateImpl implements _RefundFormState {
|
||||
refundReason,
|
||||
failureOrRefund,
|
||||
isSubmitting,
|
||||
idempotencyKey,
|
||||
);
|
||||
|
||||
/// Create a copy of RefundFormState
|
||||
@@ -916,6 +943,7 @@ abstract class _RefundFormState implements RefundFormState {
|
||||
final RefundReason? refundReason,
|
||||
required final Option<Either<OrderFailure, Unit>> failureOrRefund,
|
||||
final bool isSubmitting,
|
||||
final String? idempotencyKey,
|
||||
}) = _$RefundFormStateImpl;
|
||||
|
||||
@override
|
||||
@@ -929,6 +957,12 @@ abstract class _RefundFormState implements RefundFormState {
|
||||
@override
|
||||
bool get isSubmitting;
|
||||
|
||||
/// Idempotency key untuk refund submission.
|
||||
/// Di-generate saat user tap "Refund", dipakai ulang saat retry,
|
||||
/// di-clear setelah sukses.
|
||||
@override
|
||||
String? get idempotencyKey;
|
||||
|
||||
/// Create a copy of RefundFormState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
|
||||
@@ -8,6 +8,10 @@ class RefundFormState with _$RefundFormState {
|
||||
RefundReason? refundReason,
|
||||
required Option<Either<OrderFailure, Unit>> failureOrRefund,
|
||||
@Default(false) bool isSubmitting,
|
||||
/// Idempotency key untuk refund submission.
|
||||
/// Di-generate saat user tap "Refund", dipakai ulang saat retry,
|
||||
/// di-clear setelah sukses.
|
||||
String? idempotencyKey,
|
||||
}) = _RefundFormState;
|
||||
|
||||
factory RefundFormState.initial() => RefundFormState(
|
||||
|
||||
@@ -67,7 +67,11 @@ class SplitBillFormBloc extends Bloc<SplitBillFormEvent, SplitBillFormState> {
|
||||
final item = state.pendingItems.firstWhere(
|
||||
(item) => item.id == entry.key,
|
||||
);
|
||||
newTotal += item.unitPrice.toInt() * entry.value;
|
||||
// Baris berbobot dibayar utuh: nilainya weight x harga, bukan
|
||||
// quantity x harga.
|
||||
newTotal += item.isWeighted
|
||||
? item.totalPrice
|
||||
: item.unitPrice.toInt() * entry.value;
|
||||
}
|
||||
|
||||
emit(
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:dartz/dartz.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
|
||||
import '../../../common/service/unit_registry.dart';
|
||||
import '../../../domain/category/category.dart';
|
||||
import '../../../domain/product/product.dart';
|
||||
|
||||
@@ -14,9 +15,13 @@ part 'sync_setting_bloc.freezed.dart';
|
||||
class SyncSettingBloc extends Bloc<SyncSettingEvent, SyncSettingState> {
|
||||
final IProductRepository _productRepository;
|
||||
final ICategoryRepository _categoryRepository;
|
||||
final UnitRegistry _unitRegistry;
|
||||
|
||||
SyncSettingBloc(this._productRepository, this._categoryRepository)
|
||||
: super(SyncSettingState.initial()) {
|
||||
SyncSettingBloc(
|
||||
this._productRepository,
|
||||
this._categoryRepository,
|
||||
this._unitRegistry,
|
||||
) : super(SyncSettingState.initial()) {
|
||||
on<SyncSettingEvent>(_onSyncSettingEvent);
|
||||
}
|
||||
|
||||
@@ -97,6 +102,10 @@ class SyncSettingBloc extends Bloc<SyncSettingEvent, SyncSettingState> {
|
||||
),
|
||||
);
|
||||
|
||||
// Satuan lebih dulu: produk timbangan butuh singkatannya untuk bisa
|
||||
// ditulis "4,2 ons" begitu selesai disinkronkan.
|
||||
await _unitRegistry.load(forceRefresh: true);
|
||||
|
||||
// Sync categories first
|
||||
final categoryResult = await _categoryRepository.syncAllCategories();
|
||||
|
||||
@@ -169,6 +178,8 @@ class SyncSettingBloc extends Bloc<SyncSettingEvent, SyncSettingState> {
|
||||
Future<void> _onSyncProducts(Emitter<SyncSettingState> emit) async {
|
||||
emit(state.copyWith(isLoading: true, failureOptionSyncProduct: none()));
|
||||
|
||||
await _unitRegistry.load(forceRefresh: true);
|
||||
|
||||
final result = await _productRepository.syncAllProducts();
|
||||
|
||||
result.fold(
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:collection/collection.dart';
|
||||
import 'package:dartz/dartz.dart' hide Order;
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:injectable/injectable.dart' hide Order;
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
import '../../../common/types/void_type.dart';
|
||||
import '../../../domain/order/order.dart';
|
||||
@@ -54,7 +55,10 @@ class VoidFormBloc extends Bloc<VoidFormEvent, VoidFormState> {
|
||||
(item) => item.id == entry.key,
|
||||
);
|
||||
if (item == null) continue;
|
||||
newTotal += item.unitPrice.toInt() * entry.value;
|
||||
// Baris berbobot di-void utuh; rupiahnya sudah weight x harga.
|
||||
newTotal += item.isWeighted
|
||||
? item.totalPrice
|
||||
: item.unitPrice.toInt() * entry.value;
|
||||
}
|
||||
|
||||
emit(
|
||||
@@ -91,7 +95,13 @@ class VoidFormBloc extends Bloc<VoidFormEvent, VoidFormState> {
|
||||
productVariantName: originalItem.productVariantName,
|
||||
quantity: voidQty,
|
||||
unitPrice: originalItem.unitPrice,
|
||||
totalPrice: (originalItem.unitPrice) * voidQty,
|
||||
totalPrice: originalItem.isWeighted
|
||||
? originalItem.totalPrice
|
||||
: (originalItem.unitPrice) * voidQty,
|
||||
weight: originalItem.weight,
|
||||
unitId: originalItem.unitId,
|
||||
unitName: originalItem.unitName,
|
||||
unitAbbreviation: originalItem.unitAbbreviation,
|
||||
modifiers: originalItem.modifiers,
|
||||
notes: originalItem.notes,
|
||||
status: originalItem.status,
|
||||
@@ -99,18 +109,28 @@ class VoidFormBloc extends Bloc<VoidFormEvent, VoidFormState> {
|
||||
updatedAt: originalItem.updatedAt,
|
||||
printerType: originalItem.printerType,
|
||||
paidQuantity: originalItem.paidQuantity,
|
||||
printToChecker: originalItem.printToChecker,
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
emit(state.copyWith(isSubmitting: true, failureOrVoid: none()));
|
||||
// Generate new idempotency key saat user intent (tap Void)
|
||||
// Kalau sedang retry, pakai key yang sama
|
||||
final idempotencyKey = state.idempotencyKey ?? const Uuid().v4();
|
||||
|
||||
emit(state.copyWith(
|
||||
isSubmitting: true,
|
||||
failureOrVoid: none(),
|
||||
idempotencyKey: idempotencyKey,
|
||||
));
|
||||
|
||||
failureOrVoid = await _repository.voidOrder(
|
||||
orderId: state.order.id,
|
||||
reason: state.voidReason ?? '',
|
||||
orderItems: voidItems,
|
||||
type: state.voidType.toStringType(),
|
||||
idempotencyKey: idempotencyKey,
|
||||
);
|
||||
|
||||
emit(
|
||||
@@ -118,6 +138,8 @@ class VoidFormBloc extends Bloc<VoidFormEvent, VoidFormState> {
|
||||
isSubmitting: false,
|
||||
failureOrVoid: optionOf(failureOrVoid),
|
||||
voidItems: state.voidType.isItem ? voidItems : state.pendingItems,
|
||||
// Clear key on success, keep on failure for retry
|
||||
idempotencyKey: failureOrVoid.isRight() ? null : idempotencyKey,
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
@@ -1256,6 +1256,11 @@ mixin _$VoidFormState {
|
||||
throw _privateConstructorUsedError;
|
||||
bool get isSubmitting => throw _privateConstructorUsedError;
|
||||
|
||||
/// Idempotency key untuk void submission.
|
||||
/// Di-generate saat user tap "Void", dipakai ulang saat retry,
|
||||
/// di-clear setelah sukses atau clearState.
|
||||
String? get idempotencyKey => throw _privateConstructorUsedError;
|
||||
|
||||
/// Create a copy of VoidFormState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@@ -1280,6 +1285,7 @@ abstract class $VoidFormStateCopyWith<$Res> {
|
||||
int totalPriceVoid,
|
||||
Option<Either<OrderFailure, Unit>> failureOrVoid,
|
||||
bool isSubmitting,
|
||||
String? idempotencyKey,
|
||||
});
|
||||
|
||||
$OrderCopyWith<$Res> get order;
|
||||
@@ -1309,6 +1315,7 @@ class _$VoidFormStateCopyWithImpl<$Res, $Val extends VoidFormState>
|
||||
Object? totalPriceVoid = null,
|
||||
Object? failureOrVoid = null,
|
||||
Object? isSubmitting = null,
|
||||
Object? idempotencyKey = freezed,
|
||||
}) {
|
||||
return _then(
|
||||
_value.copyWith(
|
||||
@@ -1348,6 +1355,10 @@ class _$VoidFormStateCopyWithImpl<$Res, $Val extends VoidFormState>
|
||||
? _value.isSubmitting
|
||||
: isSubmitting // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
idempotencyKey: freezed == idempotencyKey
|
||||
? _value.idempotencyKey
|
||||
: idempotencyKey // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
)
|
||||
as $Val,
|
||||
);
|
||||
@@ -1383,6 +1394,7 @@ abstract class _$$VoidFormStateImplCopyWith<$Res>
|
||||
int totalPriceVoid,
|
||||
Option<Either<OrderFailure, Unit>> failureOrVoid,
|
||||
bool isSubmitting,
|
||||
String? idempotencyKey,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -1412,6 +1424,7 @@ class __$$VoidFormStateImplCopyWithImpl<$Res>
|
||||
Object? totalPriceVoid = null,
|
||||
Object? failureOrVoid = null,
|
||||
Object? isSubmitting = null,
|
||||
Object? idempotencyKey = freezed,
|
||||
}) {
|
||||
return _then(
|
||||
_$VoidFormStateImpl(
|
||||
@@ -1451,6 +1464,10 @@ class __$$VoidFormStateImplCopyWithImpl<$Res>
|
||||
? _value.isSubmitting
|
||||
: isSubmitting // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
idempotencyKey: freezed == idempotencyKey
|
||||
? _value.idempotencyKey
|
||||
: idempotencyKey // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -1469,6 +1486,7 @@ class _$VoidFormStateImpl implements _VoidFormState {
|
||||
required this.totalPriceVoid,
|
||||
required this.failureOrVoid,
|
||||
this.isSubmitting = false,
|
||||
this.idempotencyKey,
|
||||
}) : _pendingItems = pendingItems,
|
||||
_voidItems = voidItems,
|
||||
_selectedItemQuantities = selectedItemQuantities;
|
||||
@@ -1512,9 +1530,15 @@ class _$VoidFormStateImpl implements _VoidFormState {
|
||||
@JsonKey()
|
||||
final bool isSubmitting;
|
||||
|
||||
/// Idempotency key untuk void submission.
|
||||
/// Di-generate saat user tap "Void", dipakai ulang saat retry,
|
||||
/// di-clear setelah sukses atau clearState.
|
||||
@override
|
||||
final String? idempotencyKey;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'VoidFormState(order: $order, pendingItems: $pendingItems, voidItems: $voidItems, voidType: $voidType, selectedItemQuantities: $selectedItemQuantities, voidReason: $voidReason, totalPriceVoid: $totalPriceVoid, failureOrVoid: $failureOrVoid, isSubmitting: $isSubmitting)';
|
||||
return 'VoidFormState(order: $order, pendingItems: $pendingItems, voidItems: $voidItems, voidType: $voidType, selectedItemQuantities: $selectedItemQuantities, voidReason: $voidReason, totalPriceVoid: $totalPriceVoid, failureOrVoid: $failureOrVoid, isSubmitting: $isSubmitting, idempotencyKey: $idempotencyKey)';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -1544,7 +1568,9 @@ class _$VoidFormStateImpl implements _VoidFormState {
|
||||
(identical(other.failureOrVoid, failureOrVoid) ||
|
||||
other.failureOrVoid == failureOrVoid) &&
|
||||
(identical(other.isSubmitting, isSubmitting) ||
|
||||
other.isSubmitting == isSubmitting));
|
||||
other.isSubmitting == isSubmitting) &&
|
||||
(identical(other.idempotencyKey, idempotencyKey) ||
|
||||
other.idempotencyKey == idempotencyKey));
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -1559,6 +1585,7 @@ class _$VoidFormStateImpl implements _VoidFormState {
|
||||
totalPriceVoid,
|
||||
failureOrVoid,
|
||||
isSubmitting,
|
||||
idempotencyKey,
|
||||
);
|
||||
|
||||
/// Create a copy of VoidFormState
|
||||
@@ -1581,6 +1608,7 @@ abstract class _VoidFormState implements VoidFormState {
|
||||
required final int totalPriceVoid,
|
||||
required final Option<Either<OrderFailure, Unit>> failureOrVoid,
|
||||
final bool isSubmitting,
|
||||
final String? idempotencyKey,
|
||||
}) = _$VoidFormStateImpl;
|
||||
|
||||
@override
|
||||
@@ -1602,6 +1630,12 @@ abstract class _VoidFormState implements VoidFormState {
|
||||
@override
|
||||
bool get isSubmitting;
|
||||
|
||||
/// Idempotency key untuk void submission.
|
||||
/// Di-generate saat user tap "Void", dipakai ulang saat retry,
|
||||
/// di-clear setelah sukses atau clearState.
|
||||
@override
|
||||
String? get idempotencyKey;
|
||||
|
||||
/// Create a copy of VoidFormState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
|
||||
@@ -12,6 +12,10 @@ class VoidFormState with _$VoidFormState {
|
||||
required int totalPriceVoid,
|
||||
required Option<Either<OrderFailure, Unit>> failureOrVoid,
|
||||
@Default(false) bool isSubmitting,
|
||||
/// Idempotency key untuk void submission.
|
||||
/// Di-generate saat user tap "Void", dipakai ulang saat retry,
|
||||
/// di-clear setelah sukses atau clearState.
|
||||
String? idempotencyKey,
|
||||
}) = _VoidFormState;
|
||||
|
||||
factory VoidFormState.initial() => VoidFormState(
|
||||
|
||||
@@ -16,6 +16,7 @@ import 'interceptors/bad_network_interceptor.dart';
|
||||
import 'interceptors/bad_request_interceptor.dart';
|
||||
import 'interceptors/connection_timeout_interceptor.dart';
|
||||
import 'interceptors/crashlytic_interceptor.dart';
|
||||
import 'interceptors/idempotency_interceptor.dart';
|
||||
import 'interceptors/internal_server_interceptor.dart';
|
||||
import 'interceptors/not_found_interceptor.dart';
|
||||
import 'interceptors/unauthorized_interceptor.dart';
|
||||
@@ -30,6 +31,7 @@ class ApiClient {
|
||||
_dio.options.connectTimeout = const Duration(seconds: 20);
|
||||
_dio.options.validateStatus = (status) =>
|
||||
status != null && status >= 200 && status < 500;
|
||||
_dio.interceptors.add(IdempotencyInterceptor(_dio));
|
||||
_dio.interceptors.add(BadNetworkErrorInterceptor());
|
||||
_dio.interceptors.add(BadRequestErrorInterceptor());
|
||||
_dio.interceptors.add(InternalServerErrorInterceptor());
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
/// Interceptor yang secara otomatis menambahkan header X-Idempotency-Key
|
||||
/// untuk endpoint-endpoint kritis yang memerlukan idempotency.
|
||||
///
|
||||
/// Key sebaiknya di-generate di level BLoC/Cubit saat user trigger action,
|
||||
/// lalu dikirim via headers. Interceptor ini hanya berfungsi sebagai fallback
|
||||
/// jika key belum di-set.
|
||||
///
|
||||
/// Jika response 409 dengan error code `request_in_progress`, interceptor
|
||||
/// akan retry setelah delay 1-2 detik.
|
||||
class IdempotencyInterceptor extends Interceptor {
|
||||
static const _headerKey = 'X-Idempotency-Key';
|
||||
static const _replayHeader = 'X-Idempotent-Replay';
|
||||
|
||||
// Endpoints that require idempotency keys (exact match)
|
||||
static const _idempotentPaths = [
|
||||
'/api/v1/payments',
|
||||
'/api/v1/orders/void',
|
||||
];
|
||||
|
||||
// Endpoints that require idempotency keys (pattern match)
|
||||
static final _idempotentPathPatterns = [
|
||||
RegExp(r'/api/v1/orders/.+/add-items$'),
|
||||
RegExp(r'/api/v1/orders/.+/refund$'),
|
||||
RegExp(r'/api/v1/payments/.+/refund$'),
|
||||
];
|
||||
|
||||
static const _maxRetries = 3;
|
||||
static const _retryDelay = Duration(seconds: 2);
|
||||
|
||||
final Dio _dio;
|
||||
|
||||
IdempotencyInterceptor(this._dio);
|
||||
|
||||
@override
|
||||
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
|
||||
if (options.method == 'POST' && _requiresIdempotencyKey(options.path)) {
|
||||
// Gunakan key yang sudah di-set dari BLoC, atau generate baru sebagai fallback
|
||||
options.headers[_headerKey] ??= const Uuid().v4();
|
||||
}
|
||||
handler.next(options);
|
||||
}
|
||||
|
||||
@override
|
||||
void onResponse(Response response, ResponseInterceptorHandler handler) {
|
||||
// Log jika response adalah replay dari request sebelumnya
|
||||
final isReplay = response.headers.value(_replayHeader);
|
||||
if (isReplay == 'true') {
|
||||
// Response ini adalah hasil dari request pertama yang sudah diproses.
|
||||
// Treat as success — tidak perlu perlakuan khusus.
|
||||
}
|
||||
handler.next(response);
|
||||
}
|
||||
|
||||
@override
|
||||
void onError(DioException err, ErrorInterceptorHandler handler) async {
|
||||
final response = err.response;
|
||||
final options = err.requestOptions;
|
||||
|
||||
if (response == null) {
|
||||
handler.next(err);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle 409 Conflict with request_in_progress
|
||||
if (response.statusCode == 409 &&
|
||||
_isRequestInProgress(response.data) &&
|
||||
_requiresIdempotencyKey(options.path)) {
|
||||
final retryCount = options.extra['_idempotency_retry_count'] ?? 0;
|
||||
|
||||
if (retryCount < _maxRetries) {
|
||||
await Future.delayed(_retryDelay);
|
||||
|
||||
options.extra['_idempotency_retry_count'] = retryCount + 1;
|
||||
|
||||
try {
|
||||
final retryResponse = await _dio.fetch(options);
|
||||
handler.resolve(retryResponse);
|
||||
} on DioException catch (e) {
|
||||
handler.next(e);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
handler.next(err);
|
||||
}
|
||||
|
||||
bool _requiresIdempotencyKey(String path) {
|
||||
if (_idempotentPaths.any((p) => path.endsWith(p))) return true;
|
||||
if (_idempotentPathPatterns.any((r) => r.hasMatch(path))) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool _isRequestInProgress(dynamic data) {
|
||||
if (data is Map<String, dynamic>) {
|
||||
final errorCode = data['error_code'] ?? data['code'] ?? '';
|
||||
return errorCode == 'request_in_progress';
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -2,4 +2,5 @@ class LocalStorageKey {
|
||||
static const String token = 'token';
|
||||
static const String user = 'user';
|
||||
static const String outlet = 'outlet';
|
||||
static const String units = 'units';
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ class DatabaseHelper {
|
||||
|
||||
return await openDatabase(
|
||||
path,
|
||||
version: 1, // Updated version for categories table
|
||||
version: 3,
|
||||
onCreate: _onCreate,
|
||||
onUpgrade: _onUpgrade,
|
||||
);
|
||||
@@ -38,10 +38,15 @@ class DatabaseHelper {
|
||||
business_type TEXT,
|
||||
image_url TEXT,
|
||||
printer_type TEXT,
|
||||
print_to_checker INTEGER DEFAULT 0,
|
||||
metadata TEXT,
|
||||
is_active INTEGER,
|
||||
created_at TEXT,
|
||||
updated_at TEXT
|
||||
updated_at TEXT,
|
||||
sell_by TEXT DEFAULT 'unit',
|
||||
unit_id TEXT,
|
||||
unit_name TEXT,
|
||||
unit_abbreviation TEXT
|
||||
)
|
||||
''');
|
||||
|
||||
@@ -107,7 +112,24 @@ class DatabaseHelper {
|
||||
await db.execute('CREATE INDEX idx_printers_type ON printers(type)');
|
||||
}
|
||||
|
||||
Future<void> _onUpgrade(Database db, int oldVersion, int newVersion) async {}
|
||||
Future<void> _onUpgrade(Database db, int oldVersion, int newVersion) async {
|
||||
if (oldVersion < 2) {
|
||||
await db.execute(
|
||||
'ALTER TABLE products ADD COLUMN print_to_checker INTEGER DEFAULT 0',
|
||||
);
|
||||
}
|
||||
|
||||
// Produk timbangan: cara jual dan satuan ikut disimpan lokal supaya POS
|
||||
// tetap bisa menampilkan "4,2 ons" saat offline.
|
||||
if (oldVersion < 3) {
|
||||
await db.execute(
|
||||
"ALTER TABLE products ADD COLUMN sell_by TEXT DEFAULT 'unit'",
|
||||
);
|
||||
await db.execute('ALTER TABLE products ADD COLUMN unit_id TEXT');
|
||||
await db.execute('ALTER TABLE products ADD COLUMN unit_name TEXT');
|
||||
await db.execute('ALTER TABLE products ADD COLUMN unit_abbreviation TEXT');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> close() async {
|
||||
final db = await database;
|
||||
|
||||
@@ -13,3 +13,10 @@ extension DoubleExt on double {
|
||||
decimalDigits: 0,
|
||||
).format(this);
|
||||
}
|
||||
|
||||
extension WeightExt on double {
|
||||
/// Format berat gaya Indonesia: koma desimal, tanpa nol di belakang.
|
||||
/// `4.2` → `4,2`, `4.237` → `4,237`, `5.0` → `5`.
|
||||
String get weightFormat =>
|
||||
NumberFormat('#,##0.###', 'id').format(this);
|
||||
}
|
||||
|
||||
@@ -75,3 +75,25 @@ int generateRandomNumber() {
|
||||
final random = Random();
|
||||
return 1 + random.nextInt(10000);
|
||||
}
|
||||
|
||||
/// Ambil pesan validasi dari amplop error standar backend.
|
||||
///
|
||||
/// Untuk produk timbangan pesannya menyebut nama produk — misalnya
|
||||
/// "product Ikan Tude is sold by weight and requires a weight" — jadi layak
|
||||
/// ditampilkan apa adanya ke kasir alih-alih diganti pesan generik.
|
||||
String? extractApiErrorCause(dynamic responseData) {
|
||||
if (responseData is! Map) return null;
|
||||
|
||||
final errors = responseData['errors'];
|
||||
if (errors is! List || errors.isEmpty) return null;
|
||||
|
||||
final causes = errors
|
||||
.whereType<Map>()
|
||||
.map((e) => (e['cause'] ?? e['message'])?.toString())
|
||||
.where((cause) => cause != null && cause.isNotEmpty)
|
||||
.cast<String>()
|
||||
.toList();
|
||||
|
||||
if (causes.isEmpty) return null;
|
||||
return causes.join('\n');
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:injectable/injectable.dart';
|
||||
|
||||
import '../../domain/unit/unit.dart';
|
||||
|
||||
/// Kamus satuan yang dipakai untuk menerjemahkan `unit_id` sebuah produk
|
||||
/// menjadi singkatan yang dicetak di keranjang dan struk (`4,2 ons`).
|
||||
///
|
||||
/// Response produk hanya dijamin membawa `unit_id`; nama dan singkatannya
|
||||
/// datang dari `GET /api/v1/units`. Registry ini menjembatani keduanya tanpa
|
||||
/// memaksa setiap pemanggil menunggu request.
|
||||
@lazySingleton
|
||||
class UnitRegistry {
|
||||
final IUnitRepository _repository;
|
||||
final _logName = 'UnitRegistry';
|
||||
|
||||
Map<String, ProductUnit> _unitsById = {};
|
||||
|
||||
UnitRegistry(this._repository);
|
||||
|
||||
bool get isEmpty => _unitsById.isEmpty;
|
||||
|
||||
ProductUnit? byId(String? id) {
|
||||
if (id == null || id.isEmpty) return null;
|
||||
return _unitsById[id];
|
||||
}
|
||||
|
||||
String abbreviationOf(String? id) => byId(id)?.abbreviation ?? '';
|
||||
|
||||
String nameOf(String? id) => byId(id)?.name ?? '';
|
||||
|
||||
/// Muat satuan ke memori. Aman dipanggil berulang; [forceRefresh] menembak
|
||||
/// server ulang saat pengguna melakukan sinkronisasi manual.
|
||||
Future<void> load({bool forceRefresh = false}) async {
|
||||
final result = await _repository.getUnits(forceRefresh: forceRefresh);
|
||||
|
||||
result.fold(
|
||||
(failure) => log('⚠️ Gagal memuat satuan: $failure', name: _logName),
|
||||
(units) {
|
||||
_unitsById = {for (final unit in units) unit.id: unit};
|
||||
log('📏 ${units.length} satuan siap dipakai', name: _logName);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void clear() => _unitsById = {};
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
/// Cara sebuah produk dijual.
|
||||
///
|
||||
/// Lihat `docs/integration-weight-based-products.md` — backend hanya menerima
|
||||
/// dua nilai ini, dan produk lama otomatis dianggap [SellBy.unit].
|
||||
class SellBy {
|
||||
const SellBy._();
|
||||
|
||||
/// Dijual per cacah. `quantity` integer, `weight` tidak dikirim.
|
||||
static const String unit = 'unit';
|
||||
|
||||
/// Dijual per timbangan. `quantity` selalu 1, `weight` wajib.
|
||||
static const String weight = 'weight';
|
||||
|
||||
static bool isWeight(String? value) => value == weight;
|
||||
|
||||
/// Nilai apa pun di luar dua yang dikenal diperlakukan sebagai [unit],
|
||||
/// sama seperti perilaku backend untuk produk lama.
|
||||
static String normalize(String? value) => value == weight ? weight : unit;
|
||||
}
|
||||
@@ -4,6 +4,7 @@ class ApiPath {
|
||||
static const String outlets = '/api/v1/outlets';
|
||||
static const String categories = '/api/v1/categories';
|
||||
static const String products = '/api/v1/products';
|
||||
static const String units = '/api/v1/units';
|
||||
static const String tables = '/api/v1/tables';
|
||||
static const String customers = '/api/v1/customers';
|
||||
static const String paymentMethods = '/api/v1/payment-methods';
|
||||
|
||||
@@ -8,6 +8,9 @@ class AddItemOrderRequest with _$AddItemOrderRequest {
|
||||
required int quantity,
|
||||
required int unitPrice,
|
||||
required String notes,
|
||||
|
||||
/// Hanya diisi untuk produk `sell_by: "weight"`.
|
||||
double? weight,
|
||||
}) = _AddItemOrderRequest;
|
||||
|
||||
factory AddItemOrderRequest.empty() => const AddItemOrderRequest(
|
||||
@@ -17,4 +20,14 @@ class AddItemOrderRequest with _$AddItemOrderRequest {
|
||||
unitPrice: 0,
|
||||
notes: '',
|
||||
);
|
||||
|
||||
factory AddItemOrderRequest.fromProductQuantity(ProductQuantity item) =>
|
||||
AddItemOrderRequest(
|
||||
productId: item.product.id,
|
||||
productVariantId: item.variant?.id ?? '',
|
||||
quantity: item.isWeighted ? 1 : item.quantity,
|
||||
unitPrice: item.product.price.toInt(),
|
||||
notes: item.notes,
|
||||
weight: item.weight,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -125,6 +125,7 @@ class Order with _$Order {
|
||||
createdAt: DateTime.now().subtract(const Duration(hours: 1)),
|
||||
updatedAt: DateTime.now(),
|
||||
printerType: 'Barista',
|
||||
printToChecker: false,
|
||||
paidQuantity: 2,
|
||||
categoryId: 'CAT-001',
|
||||
categoryName: 'Minuman',
|
||||
@@ -145,6 +146,28 @@ class Order with _$Order {
|
||||
createdAt: DateTime.now().subtract(const Duration(hours: 1)),
|
||||
updatedAt: DateTime.now(),
|
||||
printerType: 'Kitchen',
|
||||
printToChecker: false,
|
||||
paidQuantity: 1,
|
||||
categoryId: 'CAT-002',
|
||||
categoryName: 'Makanan',
|
||||
),
|
||||
OrderItem(
|
||||
id: 'ITEM-003',
|
||||
orderId: 'ORD-001',
|
||||
productId: 'PROD-003',
|
||||
productName: 'Pasta',
|
||||
productVariantId: 'VAR-002',
|
||||
productVariantName: '',
|
||||
quantity: 1,
|
||||
unitPrice: 50000,
|
||||
totalPrice: 50000,
|
||||
modifiers: [],
|
||||
notes: '',
|
||||
status: 'Served',
|
||||
createdAt: DateTime.now().subtract(const Duration(hours: 1)),
|
||||
updatedAt: DateTime.now(),
|
||||
printerType: 'Kitchen',
|
||||
printToChecker: false,
|
||||
paidQuantity: 1,
|
||||
categoryId: 'CAT-002',
|
||||
categoryName: 'Makanan',
|
||||
@@ -174,19 +197,19 @@ class Order with _$Order {
|
||||
);
|
||||
|
||||
// For Add Item Order
|
||||
factory Order.fromLastOrder(Order order, List<ProductQuantity> products) =>
|
||||
order.copyWith(
|
||||
orderItems: products
|
||||
.map((e) => OrderItem.fromProductQuantity(e))
|
||||
.toList(),
|
||||
totalAmount: products
|
||||
.map((e) => e.product.price.toInt() * e.quantity)
|
||||
.reduce((value, element) => value + element),
|
||||
subtotal: products
|
||||
.map((e) => e.product.price.toInt() * e.quantity)
|
||||
.reduce((value, element) => value + element),
|
||||
factory Order.fromLastOrder(Order order, List<ProductQuantity> products) {
|
||||
final subtotal = products.fold<int>(
|
||||
0,
|
||||
(sum, item) => sum + item.lineTotal.toInt(),
|
||||
);
|
||||
|
||||
return order.copyWith(
|
||||
orderItems: products.map(OrderItem.fromProductQuantity).toList(),
|
||||
totalAmount: subtotal,
|
||||
subtotal: subtotal,
|
||||
metadata: order.metadata,
|
||||
);
|
||||
}
|
||||
|
||||
List<OrderItem> get orderItemNotCancelled =>
|
||||
orderItems.where((item) => item.status != 'cancelled').toList();
|
||||
@@ -194,6 +217,8 @@ class Order with _$Order {
|
||||
|
||||
@freezed
|
||||
class OrderItem with _$OrderItem {
|
||||
const OrderItem._();
|
||||
|
||||
const factory OrderItem({
|
||||
required String id,
|
||||
required String orderId,
|
||||
@@ -210,9 +235,16 @@ class OrderItem with _$OrderItem {
|
||||
required DateTime createdAt,
|
||||
required DateTime updatedAt,
|
||||
required String printerType,
|
||||
required bool printToChecker,
|
||||
required int paidQuantity,
|
||||
required String categoryId,
|
||||
required String categoryName,
|
||||
|
||||
/// Angka timbangan baris ini. `null` untuk produk cacah.
|
||||
double? weight,
|
||||
@Default('') String unitId,
|
||||
@Default('') String unitName,
|
||||
@Default('') String unitAbbreviation,
|
||||
}) = _OrderItem;
|
||||
|
||||
factory OrderItem.empty() => OrderItem(
|
||||
@@ -231,6 +263,7 @@ class OrderItem with _$OrderItem {
|
||||
createdAt: DateTime(1970),
|
||||
updatedAt: DateTime(1970),
|
||||
printerType: '',
|
||||
printToChecker: false,
|
||||
paidQuantity: 0,
|
||||
categoryId: '',
|
||||
categoryName: '',
|
||||
@@ -244,18 +277,43 @@ class OrderItem with _$OrderItem {
|
||||
productName: productQuantity.product.name,
|
||||
productVariantId: productQuantity.variant?.id ?? '',
|
||||
productVariantName: productQuantity.variant?.name ?? '',
|
||||
quantity: productQuantity.quantity,
|
||||
quantity: productQuantity.isWeighted ? 1 : productQuantity.quantity,
|
||||
unitPrice: productQuantity.product.price.toInt(),
|
||||
totalPrice: (productQuantity.product.price * productQuantity.quantity)
|
||||
.toInt(),
|
||||
totalPrice: productQuantity.lineTotal.toInt(),
|
||||
modifiers: [],
|
||||
notes: productQuantity.notes,
|
||||
status: 'pending',
|
||||
createdAt: DateTime.now(),
|
||||
updatedAt: DateTime.now(),
|
||||
printerType: productQuantity.product.printerType,
|
||||
printToChecker: productQuantity.product.printToChecker,
|
||||
paidQuantity: 0,
|
||||
categoryId: '',
|
||||
categoryName: '',
|
||||
weight: productQuantity.weight,
|
||||
unitId: productQuantity.product.unitId,
|
||||
unitName: productQuantity.product.unitName,
|
||||
unitAbbreviation: productQuantity.product.unitAbbreviation,
|
||||
);
|
||||
|
||||
/// Baris hasil penimbangan — utuh, tidak bisa dipecah sebagian.
|
||||
bool get isWeighted => weight != null && weight! > 0;
|
||||
|
||||
/// Singkatan satuan untuk ditampilkan; jatuh ke nama panjang bila kosong.
|
||||
String get unitLabel =>
|
||||
unitAbbreviation.isNotEmpty ? unitAbbreviation : unitName;
|
||||
|
||||
/// `4,2 ons` untuk baris berbobot, `2` untuk baris biasa.
|
||||
///
|
||||
/// Jangan menampilkan [quantity] mentah pada baris berbobot — nilainya selalu
|
||||
/// 1 dan terbaca seperti "satu ons".
|
||||
String get quantityLabel {
|
||||
if (!isWeighted) return quantity.toString();
|
||||
final value = weight!.weightFormat;
|
||||
return unitLabel.isEmpty ? value : '$value $unitLabel';
|
||||
}
|
||||
|
||||
/// `4,2 ons x Rp 4.500` atau `2 x Rp 25.000`.
|
||||
String get quantityPriceLabel =>
|
||||
'$quantityLabel x ${unitPrice.currencyFormatRpV2}';
|
||||
}
|
||||
|
||||
@@ -33,6 +33,10 @@ class OrderItemRequest with _$OrderItemRequest {
|
||||
required int quantity,
|
||||
required int unitPrice,
|
||||
required String notes,
|
||||
|
||||
/// Hanya diisi untuk produk `sell_by: "weight"`; produk cacah tidak boleh
|
||||
/// mengirim berat sama sekali.
|
||||
double? weight,
|
||||
}) = _OrderItemRequest;
|
||||
|
||||
factory OrderItemRequest.empty() => const OrderItemRequest(
|
||||
@@ -42,4 +46,16 @@ class OrderItemRequest with _$OrderItemRequest {
|
||||
unitPrice: 0,
|
||||
notes: '',
|
||||
);
|
||||
|
||||
/// Bentuk baris dari isi keranjang: produk timbangan selalu `quantity: 1`
|
||||
/// ditambah `weight`, produk cacah tetap seperti sebelumnya.
|
||||
factory OrderItemRequest.fromProductQuantity(ProductQuantity item) =>
|
||||
OrderItemRequest(
|
||||
productId: item.product.id,
|
||||
productVariantId: item.variant?.id ?? '',
|
||||
quantity: item.isWeighted ? 1 : item.quantity,
|
||||
unitPrice: item.product.price.toInt(),
|
||||
notes: item.notes,
|
||||
weight: item.weight,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
import '../../common/api/api_failure.dart';
|
||||
import '../../common/types/split_type.dart';
|
||||
import '../../common/extension/extension.dart';
|
||||
import '../product/product.dart';
|
||||
|
||||
part 'order.freezed.dart';
|
||||
|
||||
@@ -1002,10 +1002,17 @@ mixin _$OrderItem {
|
||||
DateTime get createdAt => throw _privateConstructorUsedError;
|
||||
DateTime get updatedAt => throw _privateConstructorUsedError;
|
||||
String get printerType => throw _privateConstructorUsedError;
|
||||
bool get printToChecker => throw _privateConstructorUsedError;
|
||||
int get paidQuantity => throw _privateConstructorUsedError;
|
||||
String get categoryId => throw _privateConstructorUsedError;
|
||||
String get categoryName => throw _privateConstructorUsedError;
|
||||
|
||||
/// Angka timbangan baris ini. `null` untuk produk cacah.
|
||||
double? get weight => throw _privateConstructorUsedError;
|
||||
String get unitId => throw _privateConstructorUsedError;
|
||||
String get unitName => throw _privateConstructorUsedError;
|
||||
String get unitAbbreviation => throw _privateConstructorUsedError;
|
||||
|
||||
/// Create a copy of OrderItem
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@@ -1034,9 +1041,14 @@ abstract class $OrderItemCopyWith<$Res> {
|
||||
DateTime createdAt,
|
||||
DateTime updatedAt,
|
||||
String printerType,
|
||||
bool printToChecker,
|
||||
int paidQuantity,
|
||||
String categoryId,
|
||||
String categoryName,
|
||||
double? weight,
|
||||
String unitId,
|
||||
String unitName,
|
||||
String unitAbbreviation,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1070,9 +1082,14 @@ class _$OrderItemCopyWithImpl<$Res, $Val extends OrderItem>
|
||||
Object? createdAt = null,
|
||||
Object? updatedAt = null,
|
||||
Object? printerType = null,
|
||||
Object? printToChecker = null,
|
||||
Object? paidQuantity = null,
|
||||
Object? categoryId = null,
|
||||
Object? categoryName = null,
|
||||
Object? weight = freezed,
|
||||
Object? unitId = null,
|
||||
Object? unitName = null,
|
||||
Object? unitAbbreviation = null,
|
||||
}) {
|
||||
return _then(
|
||||
_value.copyWith(
|
||||
@@ -1136,6 +1153,10 @@ class _$OrderItemCopyWithImpl<$Res, $Val extends OrderItem>
|
||||
? _value.printerType
|
||||
: printerType // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
printToChecker: null == printToChecker
|
||||
? _value.printToChecker
|
||||
: printToChecker // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
paidQuantity: null == paidQuantity
|
||||
? _value.paidQuantity
|
||||
: paidQuantity // ignore: cast_nullable_to_non_nullable
|
||||
@@ -1148,6 +1169,22 @@ class _$OrderItemCopyWithImpl<$Res, $Val extends OrderItem>
|
||||
? _value.categoryName
|
||||
: categoryName // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
weight: freezed == weight
|
||||
? _value.weight
|
||||
: weight // ignore: cast_nullable_to_non_nullable
|
||||
as double?,
|
||||
unitId: null == unitId
|
||||
? _value.unitId
|
||||
: unitId // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
unitName: null == unitName
|
||||
? _value.unitName
|
||||
: unitName // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
unitAbbreviation: null == unitAbbreviation
|
||||
? _value.unitAbbreviation
|
||||
: unitAbbreviation // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
)
|
||||
as $Val,
|
||||
);
|
||||
@@ -1179,9 +1216,14 @@ abstract class _$$OrderItemImplCopyWith<$Res>
|
||||
DateTime createdAt,
|
||||
DateTime updatedAt,
|
||||
String printerType,
|
||||
bool printToChecker,
|
||||
int paidQuantity,
|
||||
String categoryId,
|
||||
String categoryName,
|
||||
double? weight,
|
||||
String unitId,
|
||||
String unitName,
|
||||
String unitAbbreviation,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1214,9 +1256,14 @@ class __$$OrderItemImplCopyWithImpl<$Res>
|
||||
Object? createdAt = null,
|
||||
Object? updatedAt = null,
|
||||
Object? printerType = null,
|
||||
Object? printToChecker = null,
|
||||
Object? paidQuantity = null,
|
||||
Object? categoryId = null,
|
||||
Object? categoryName = null,
|
||||
Object? weight = freezed,
|
||||
Object? unitId = null,
|
||||
Object? unitName = null,
|
||||
Object? unitAbbreviation = null,
|
||||
}) {
|
||||
return _then(
|
||||
_$OrderItemImpl(
|
||||
@@ -1280,6 +1327,10 @@ class __$$OrderItemImplCopyWithImpl<$Res>
|
||||
? _value.printerType
|
||||
: printerType // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
printToChecker: null == printToChecker
|
||||
? _value.printToChecker
|
||||
: printToChecker // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
paidQuantity: null == paidQuantity
|
||||
? _value.paidQuantity
|
||||
: paidQuantity // ignore: cast_nullable_to_non_nullable
|
||||
@@ -1292,6 +1343,22 @@ class __$$OrderItemImplCopyWithImpl<$Res>
|
||||
? _value.categoryName
|
||||
: categoryName // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
weight: freezed == weight
|
||||
? _value.weight
|
||||
: weight // ignore: cast_nullable_to_non_nullable
|
||||
as double?,
|
||||
unitId: null == unitId
|
||||
? _value.unitId
|
||||
: unitId // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
unitName: null == unitName
|
||||
? _value.unitName
|
||||
: unitName // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
unitAbbreviation: null == unitAbbreviation
|
||||
? _value.unitAbbreviation
|
||||
: unitAbbreviation // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -1299,7 +1366,7 @@ class __$$OrderItemImplCopyWithImpl<$Res>
|
||||
|
||||
/// @nodoc
|
||||
|
||||
class _$OrderItemImpl implements _OrderItem {
|
||||
class _$OrderItemImpl extends _OrderItem {
|
||||
const _$OrderItemImpl({
|
||||
required this.id,
|
||||
required this.orderId,
|
||||
@@ -1316,10 +1383,16 @@ class _$OrderItemImpl implements _OrderItem {
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
required this.printerType,
|
||||
required this.printToChecker,
|
||||
required this.paidQuantity,
|
||||
required this.categoryId,
|
||||
required this.categoryName,
|
||||
}) : _modifiers = modifiers;
|
||||
this.weight,
|
||||
this.unitId = '',
|
||||
this.unitName = '',
|
||||
this.unitAbbreviation = '',
|
||||
}) : _modifiers = modifiers,
|
||||
super._();
|
||||
|
||||
@override
|
||||
final String id;
|
||||
@@ -1358,15 +1431,30 @@ class _$OrderItemImpl implements _OrderItem {
|
||||
@override
|
||||
final String printerType;
|
||||
@override
|
||||
final bool printToChecker;
|
||||
@override
|
||||
final int paidQuantity;
|
||||
@override
|
||||
final String categoryId;
|
||||
@override
|
||||
final String categoryName;
|
||||
|
||||
/// Angka timbangan baris ini. `null` untuk produk cacah.
|
||||
@override
|
||||
final double? weight;
|
||||
@override
|
||||
@JsonKey()
|
||||
final String unitId;
|
||||
@override
|
||||
@JsonKey()
|
||||
final String unitName;
|
||||
@override
|
||||
@JsonKey()
|
||||
final String unitAbbreviation;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'OrderItem(id: $id, orderId: $orderId, productId: $productId, productName: $productName, productVariantId: $productVariantId, productVariantName: $productVariantName, quantity: $quantity, unitPrice: $unitPrice, totalPrice: $totalPrice, modifiers: $modifiers, notes: $notes, status: $status, createdAt: $createdAt, updatedAt: $updatedAt, printerType: $printerType, paidQuantity: $paidQuantity, categoryId: $categoryId, categoryName: $categoryName)';
|
||||
return 'OrderItem(id: $id, orderId: $orderId, productId: $productId, productName: $productName, productVariantId: $productVariantId, productVariantName: $productVariantName, quantity: $quantity, unitPrice: $unitPrice, totalPrice: $totalPrice, modifiers: $modifiers, notes: $notes, status: $status, createdAt: $createdAt, updatedAt: $updatedAt, printerType: $printerType, printToChecker: $printToChecker, paidQuantity: $paidQuantity, categoryId: $categoryId, categoryName: $categoryName, weight: $weight, unitId: $unitId, unitName: $unitName, unitAbbreviation: $unitAbbreviation)';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -1402,16 +1490,24 @@ class _$OrderItemImpl implements _OrderItem {
|
||||
other.updatedAt == updatedAt) &&
|
||||
(identical(other.printerType, printerType) ||
|
||||
other.printerType == printerType) &&
|
||||
(identical(other.printToChecker, printToChecker) ||
|
||||
other.printToChecker == printToChecker) &&
|
||||
(identical(other.paidQuantity, paidQuantity) ||
|
||||
other.paidQuantity == paidQuantity) &&
|
||||
(identical(other.categoryId, categoryId) ||
|
||||
other.categoryId == categoryId) &&
|
||||
(identical(other.categoryName, categoryName) ||
|
||||
other.categoryName == categoryName));
|
||||
other.categoryName == categoryName) &&
|
||||
(identical(other.weight, weight) || other.weight == weight) &&
|
||||
(identical(other.unitId, unitId) || other.unitId == unitId) &&
|
||||
(identical(other.unitName, unitName) ||
|
||||
other.unitName == unitName) &&
|
||||
(identical(other.unitAbbreviation, unitAbbreviation) ||
|
||||
other.unitAbbreviation == unitAbbreviation));
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
int get hashCode => Object.hashAll([
|
||||
runtimeType,
|
||||
id,
|
||||
orderId,
|
||||
@@ -1428,10 +1524,15 @@ class _$OrderItemImpl implements _OrderItem {
|
||||
createdAt,
|
||||
updatedAt,
|
||||
printerType,
|
||||
printToChecker,
|
||||
paidQuantity,
|
||||
categoryId,
|
||||
categoryName,
|
||||
);
|
||||
weight,
|
||||
unitId,
|
||||
unitName,
|
||||
unitAbbreviation,
|
||||
]);
|
||||
|
||||
/// Create a copy of OrderItem
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@@ -1442,7 +1543,7 @@ class _$OrderItemImpl implements _OrderItem {
|
||||
__$$OrderItemImplCopyWithImpl<_$OrderItemImpl>(this, _$identity);
|
||||
}
|
||||
|
||||
abstract class _OrderItem implements OrderItem {
|
||||
abstract class _OrderItem extends OrderItem {
|
||||
const factory _OrderItem({
|
||||
required final String id,
|
||||
required final String orderId,
|
||||
@@ -1459,10 +1560,16 @@ abstract class _OrderItem implements OrderItem {
|
||||
required final DateTime createdAt,
|
||||
required final DateTime updatedAt,
|
||||
required final String printerType,
|
||||
required final bool printToChecker,
|
||||
required final int paidQuantity,
|
||||
required final String categoryId,
|
||||
required final String categoryName,
|
||||
final double? weight,
|
||||
final String unitId,
|
||||
final String unitName,
|
||||
final String unitAbbreviation,
|
||||
}) = _$OrderItemImpl;
|
||||
const _OrderItem._() : super._();
|
||||
|
||||
@override
|
||||
String get id;
|
||||
@@ -1495,12 +1602,24 @@ abstract class _OrderItem implements OrderItem {
|
||||
@override
|
||||
String get printerType;
|
||||
@override
|
||||
bool get printToChecker;
|
||||
@override
|
||||
int get paidQuantity;
|
||||
@override
|
||||
String get categoryId;
|
||||
@override
|
||||
String get categoryName;
|
||||
|
||||
/// Angka timbangan baris ini. `null` untuk produk cacah.
|
||||
@override
|
||||
double? get weight;
|
||||
@override
|
||||
String get unitId;
|
||||
@override
|
||||
String get unitName;
|
||||
@override
|
||||
String get unitAbbreviation;
|
||||
|
||||
/// Create a copy of OrderItem
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@@ -1825,6 +1944,10 @@ mixin _$OrderItemRequest {
|
||||
int get unitPrice => throw _privateConstructorUsedError;
|
||||
String get notes => throw _privateConstructorUsedError;
|
||||
|
||||
/// Hanya diisi untuk produk `sell_by: "weight"`; produk cacah tidak boleh
|
||||
/// mengirim berat sama sekali.
|
||||
double? get weight => throw _privateConstructorUsedError;
|
||||
|
||||
/// Create a copy of OrderItemRequest
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@@ -1845,6 +1968,7 @@ abstract class $OrderItemRequestCopyWith<$Res> {
|
||||
int quantity,
|
||||
int unitPrice,
|
||||
String notes,
|
||||
double? weight,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1868,6 +1992,7 @@ class _$OrderItemRequestCopyWithImpl<$Res, $Val extends OrderItemRequest>
|
||||
Object? quantity = null,
|
||||
Object? unitPrice = null,
|
||||
Object? notes = null,
|
||||
Object? weight = freezed,
|
||||
}) {
|
||||
return _then(
|
||||
_value.copyWith(
|
||||
@@ -1891,6 +2016,10 @@ class _$OrderItemRequestCopyWithImpl<$Res, $Val extends OrderItemRequest>
|
||||
? _value.notes
|
||||
: notes // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
weight: freezed == weight
|
||||
? _value.weight
|
||||
: weight // ignore: cast_nullable_to_non_nullable
|
||||
as double?,
|
||||
)
|
||||
as $Val,
|
||||
);
|
||||
@@ -1912,6 +2041,7 @@ abstract class _$$OrderItemRequestImplCopyWith<$Res>
|
||||
int quantity,
|
||||
int unitPrice,
|
||||
String notes,
|
||||
double? weight,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1934,6 +2064,7 @@ class __$$OrderItemRequestImplCopyWithImpl<$Res>
|
||||
Object? quantity = null,
|
||||
Object? unitPrice = null,
|
||||
Object? notes = null,
|
||||
Object? weight = freezed,
|
||||
}) {
|
||||
return _then(
|
||||
_$OrderItemRequestImpl(
|
||||
@@ -1957,6 +2088,10 @@ class __$$OrderItemRequestImplCopyWithImpl<$Res>
|
||||
? _value.notes
|
||||
: notes // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
weight: freezed == weight
|
||||
? _value.weight
|
||||
: weight // ignore: cast_nullable_to_non_nullable
|
||||
as double?,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -1971,6 +2106,7 @@ class _$OrderItemRequestImpl implements _OrderItemRequest {
|
||||
required this.quantity,
|
||||
required this.unitPrice,
|
||||
required this.notes,
|
||||
this.weight,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -1984,9 +2120,14 @@ class _$OrderItemRequestImpl implements _OrderItemRequest {
|
||||
@override
|
||||
final String notes;
|
||||
|
||||
/// Hanya diisi untuk produk `sell_by: "weight"`; produk cacah tidak boleh
|
||||
/// mengirim berat sama sekali.
|
||||
@override
|
||||
final double? weight;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'OrderItemRequest(productId: $productId, productVariantId: $productVariantId, quantity: $quantity, unitPrice: $unitPrice, notes: $notes)';
|
||||
return 'OrderItemRequest(productId: $productId, productVariantId: $productVariantId, quantity: $quantity, unitPrice: $unitPrice, notes: $notes, weight: $weight)';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -2002,7 +2143,8 @@ class _$OrderItemRequestImpl implements _OrderItemRequest {
|
||||
other.quantity == quantity) &&
|
||||
(identical(other.unitPrice, unitPrice) ||
|
||||
other.unitPrice == unitPrice) &&
|
||||
(identical(other.notes, notes) || other.notes == notes));
|
||||
(identical(other.notes, notes) || other.notes == notes) &&
|
||||
(identical(other.weight, weight) || other.weight == weight));
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -2013,6 +2155,7 @@ class _$OrderItemRequestImpl implements _OrderItemRequest {
|
||||
quantity,
|
||||
unitPrice,
|
||||
notes,
|
||||
weight,
|
||||
);
|
||||
|
||||
/// Create a copy of OrderItemRequest
|
||||
@@ -2034,6 +2177,7 @@ abstract class _OrderItemRequest implements OrderItemRequest {
|
||||
required final int quantity,
|
||||
required final int unitPrice,
|
||||
required final String notes,
|
||||
final double? weight,
|
||||
}) = _$OrderItemRequestImpl;
|
||||
|
||||
@override
|
||||
@@ -2047,6 +2191,11 @@ abstract class _OrderItemRequest implements OrderItemRequest {
|
||||
@override
|
||||
String get notes;
|
||||
|
||||
/// Hanya diisi untuk produk `sell_by: "weight"`; produk cacah tidak boleh
|
||||
/// mengirim berat sama sekali.
|
||||
@override
|
||||
double? get weight;
|
||||
|
||||
/// Create a copy of OrderItemRequest
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@@ -3426,6 +3575,9 @@ mixin _$AddItemOrderRequest {
|
||||
int get unitPrice => throw _privateConstructorUsedError;
|
||||
String get notes => throw _privateConstructorUsedError;
|
||||
|
||||
/// Hanya diisi untuk produk `sell_by: "weight"`.
|
||||
double? get weight => throw _privateConstructorUsedError;
|
||||
|
||||
/// Create a copy of AddItemOrderRequest
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@@ -3446,6 +3598,7 @@ abstract class $AddItemOrderRequestCopyWith<$Res> {
|
||||
int quantity,
|
||||
int unitPrice,
|
||||
String notes,
|
||||
double? weight,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3469,6 +3622,7 @@ class _$AddItemOrderRequestCopyWithImpl<$Res, $Val extends AddItemOrderRequest>
|
||||
Object? quantity = null,
|
||||
Object? unitPrice = null,
|
||||
Object? notes = null,
|
||||
Object? weight = freezed,
|
||||
}) {
|
||||
return _then(
|
||||
_value.copyWith(
|
||||
@@ -3492,6 +3646,10 @@ class _$AddItemOrderRequestCopyWithImpl<$Res, $Val extends AddItemOrderRequest>
|
||||
? _value.notes
|
||||
: notes // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
weight: freezed == weight
|
||||
? _value.weight
|
||||
: weight // ignore: cast_nullable_to_non_nullable
|
||||
as double?,
|
||||
)
|
||||
as $Val,
|
||||
);
|
||||
@@ -3513,6 +3671,7 @@ abstract class _$$AddItemOrderRequestImplCopyWith<$Res>
|
||||
int quantity,
|
||||
int unitPrice,
|
||||
String notes,
|
||||
double? weight,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3535,6 +3694,7 @@ class __$$AddItemOrderRequestImplCopyWithImpl<$Res>
|
||||
Object? quantity = null,
|
||||
Object? unitPrice = null,
|
||||
Object? notes = null,
|
||||
Object? weight = freezed,
|
||||
}) {
|
||||
return _then(
|
||||
_$AddItemOrderRequestImpl(
|
||||
@@ -3558,6 +3718,10 @@ class __$$AddItemOrderRequestImplCopyWithImpl<$Res>
|
||||
? _value.notes
|
||||
: notes // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
weight: freezed == weight
|
||||
? _value.weight
|
||||
: weight // ignore: cast_nullable_to_non_nullable
|
||||
as double?,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -3572,6 +3736,7 @@ class _$AddItemOrderRequestImpl implements _AddItemOrderRequest {
|
||||
required this.quantity,
|
||||
required this.unitPrice,
|
||||
required this.notes,
|
||||
this.weight,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -3585,9 +3750,13 @@ class _$AddItemOrderRequestImpl implements _AddItemOrderRequest {
|
||||
@override
|
||||
final String notes;
|
||||
|
||||
/// Hanya diisi untuk produk `sell_by: "weight"`.
|
||||
@override
|
||||
final double? weight;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'AddItemOrderRequest(productId: $productId, productVariantId: $productVariantId, quantity: $quantity, unitPrice: $unitPrice, notes: $notes)';
|
||||
return 'AddItemOrderRequest(productId: $productId, productVariantId: $productVariantId, quantity: $quantity, unitPrice: $unitPrice, notes: $notes, weight: $weight)';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -3603,7 +3772,8 @@ class _$AddItemOrderRequestImpl implements _AddItemOrderRequest {
|
||||
other.quantity == quantity) &&
|
||||
(identical(other.unitPrice, unitPrice) ||
|
||||
other.unitPrice == unitPrice) &&
|
||||
(identical(other.notes, notes) || other.notes == notes));
|
||||
(identical(other.notes, notes) || other.notes == notes) &&
|
||||
(identical(other.weight, weight) || other.weight == weight));
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -3614,6 +3784,7 @@ class _$AddItemOrderRequestImpl implements _AddItemOrderRequest {
|
||||
quantity,
|
||||
unitPrice,
|
||||
notes,
|
||||
weight,
|
||||
);
|
||||
|
||||
/// Create a copy of AddItemOrderRequest
|
||||
@@ -3635,6 +3806,7 @@ abstract class _AddItemOrderRequest implements AddItemOrderRequest {
|
||||
required final int quantity,
|
||||
required final int unitPrice,
|
||||
required final String notes,
|
||||
final double? weight,
|
||||
}) = _$AddItemOrderRequestImpl;
|
||||
|
||||
@override
|
||||
@@ -3648,6 +3820,10 @@ abstract class _AddItemOrderRequest implements AddItemOrderRequest {
|
||||
@override
|
||||
String get notes;
|
||||
|
||||
/// Hanya diisi untuk produk `sell_by: "weight"`.
|
||||
@override
|
||||
double? get weight;
|
||||
|
||||
/// Create a copy of AddItemOrderRequest
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
|
||||
@@ -24,10 +24,12 @@ abstract class IOrderRepository {
|
||||
Future<Either<OrderFailure, Order>> addItemOrder({
|
||||
required String id,
|
||||
required List<AddItemOrderRequest> request,
|
||||
String? idempotencyKey,
|
||||
});
|
||||
|
||||
Future<Either<OrderFailure, Payment>> createPayment({
|
||||
required PaymentRequest request,
|
||||
String? idempotencyKey,
|
||||
});
|
||||
|
||||
Future<Either<OrderFailure, Unit>> voidOrder({
|
||||
@@ -35,15 +37,18 @@ abstract class IOrderRepository {
|
||||
required String reason,
|
||||
String type = "ITEM", // TYPE: ALL, ITEM
|
||||
required List<OrderItem> orderItems,
|
||||
String? idempotencyKey,
|
||||
});
|
||||
|
||||
Future<Either<OrderFailure, Payment>> createSplitBill(
|
||||
PaymentSplitBillRequest request,
|
||||
);
|
||||
PaymentSplitBillRequest request, {
|
||||
String? idempotencyKey,
|
||||
});
|
||||
|
||||
Future<Either<OrderFailure, Unit>> refundOrder({
|
||||
required String id,
|
||||
required String reason,
|
||||
required int refundAmount,
|
||||
String? idempotencyKey,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -21,6 +21,8 @@ class ListProduct with _$ListProduct {
|
||||
|
||||
@freezed
|
||||
class Product with _$Product {
|
||||
const Product._();
|
||||
|
||||
const factory Product({
|
||||
required String id,
|
||||
required String organizationId,
|
||||
@@ -33,11 +35,17 @@ class Product with _$Product {
|
||||
required String businessType,
|
||||
required String imageUrl,
|
||||
required String printerType,
|
||||
required bool printToChecker,
|
||||
required Map<String, dynamic> metadata,
|
||||
required bool isActive,
|
||||
required String createdAt,
|
||||
required String updatedAt,
|
||||
required List<ProductVariant> variants,
|
||||
/// `unit` (default) atau `weight`. Lihat [SellBy].
|
||||
@Default(SellBy.unit) String sellBy,
|
||||
@Default('') String unitId,
|
||||
@Default('') String unitName,
|
||||
@Default('') String unitAbbreviation,
|
||||
}) = _Product;
|
||||
|
||||
factory Product.empty() => const Product(
|
||||
@@ -52,12 +60,25 @@ class Product with _$Product {
|
||||
businessType: '',
|
||||
imageUrl: '',
|
||||
printerType: '',
|
||||
printToChecker: false,
|
||||
metadata: {},
|
||||
isActive: false,
|
||||
createdAt: '',
|
||||
updatedAt: '',
|
||||
variants: [],
|
||||
);
|
||||
|
||||
/// Produk yang dijual per timbangan: `quantity` selalu 1, `weight` wajib.
|
||||
bool get isSoldByWeight => SellBy.isWeight(sellBy);
|
||||
|
||||
/// Singkatan satuan untuk ditampilkan — `ons`. Jatuh ke nama panjang bila
|
||||
/// backend hanya mengirim itu.
|
||||
String get unitLabel =>
|
||||
unitAbbreviation.isNotEmpty ? unitAbbreviation : unitName;
|
||||
|
||||
/// Label harga mengikuti satuan: "Harga per ons".
|
||||
String get priceUnitLabel =>
|
||||
isSoldByWeight && unitLabel.isNotEmpty ? 'per $unitLabel' : '';
|
||||
}
|
||||
|
||||
@freezed
|
||||
|
||||
@@ -1,14 +1,53 @@
|
||||
part of '../product.dart';
|
||||
|
||||
/// Satu baris keranjang.
|
||||
///
|
||||
/// Untuk produk timbangan, **satu penimbangan = satu baris**: dua kali menimbang
|
||||
/// Ikan Tude menghasilkan dua baris terpisah dengan [quantity] 1 dan [weight]
|
||||
/// masing-masing. Karena itu baris dibedakan lewat [lineId], bukan lewat
|
||||
/// kombinasi produk + varian.
|
||||
@freezed
|
||||
class ProductQuantity with _$ProductQuantity {
|
||||
const ProductQuantity._();
|
||||
|
||||
const factory ProductQuantity({
|
||||
required Product product,
|
||||
ProductVariant? variant,
|
||||
required int quantity,
|
||||
required String notes,
|
||||
@Default('') String lineId,
|
||||
|
||||
/// Angka timbangan dalam satuan produk. `null` untuk produk cacah.
|
||||
double? weight,
|
||||
}) = _ProductQuantity;
|
||||
|
||||
factory ProductQuantity.empty() =>
|
||||
ProductQuantity(product: Product.empty(), quantity: 0, notes: '');
|
||||
|
||||
bool get isWeighted => weight != null;
|
||||
|
||||
double get unitPrice => product.price + (variant?.priceModifier ?? 0);
|
||||
|
||||
/// Harga baris: `weight × unit_price` untuk timbangan, `quantity × unit_price`
|
||||
/// untuk produk cacah.
|
||||
double get lineTotal =>
|
||||
isWeighted ? unitPrice * weight! : unitPrice * quantity;
|
||||
|
||||
/// `4,2 ons` atau `2` — dipakai di kolom Qty keranjang.
|
||||
String get quantityLabel {
|
||||
if (!isWeighted) return quantity.toString();
|
||||
final label = product.unitLabel;
|
||||
final value = weight!.weightFormat;
|
||||
return label.isEmpty ? value : '$value $label';
|
||||
}
|
||||
}
|
||||
|
||||
extension ProductQuantityListExt on List<ProductQuantity> {
|
||||
/// Total keranjang: baris berbobot pakai `weight × harga`, baris cacah pakai
|
||||
/// `quantity × harga`.
|
||||
double get cartTotal => fold<double>(0, (sum, item) => sum + item.lineTotal);
|
||||
|
||||
/// Baris berbobot dihitung satu — satu penimbangan, satu barang.
|
||||
int get cartItemCount =>
|
||||
fold<int>(0, (sum, item) => sum + (item.isWeighted ? 1 : item.quantity));
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
import '../../common/api/api_failure.dart';
|
||||
import '../../common/extension/extension.dart';
|
||||
import '../../common/types/sell_by.dart';
|
||||
|
||||
part 'product.freezed.dart';
|
||||
|
||||
|
||||
@@ -279,12 +279,19 @@ mixin _$Product {
|
||||
String get businessType => throw _privateConstructorUsedError;
|
||||
String get imageUrl => throw _privateConstructorUsedError;
|
||||
String get printerType => throw _privateConstructorUsedError;
|
||||
bool get printToChecker => throw _privateConstructorUsedError;
|
||||
Map<String, dynamic> get metadata => throw _privateConstructorUsedError;
|
||||
bool get isActive => throw _privateConstructorUsedError;
|
||||
String get createdAt => throw _privateConstructorUsedError;
|
||||
String get updatedAt => throw _privateConstructorUsedError;
|
||||
List<ProductVariant> get variants => throw _privateConstructorUsedError;
|
||||
|
||||
/// `unit` (default) atau `weight`. Lihat [SellBy].
|
||||
String get sellBy => throw _privateConstructorUsedError;
|
||||
String get unitId => throw _privateConstructorUsedError;
|
||||
String get unitName => throw _privateConstructorUsedError;
|
||||
String get unitAbbreviation => throw _privateConstructorUsedError;
|
||||
|
||||
/// Create a copy of Product
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@@ -308,11 +315,16 @@ abstract class $ProductCopyWith<$Res> {
|
||||
String businessType,
|
||||
String imageUrl,
|
||||
String printerType,
|
||||
bool printToChecker,
|
||||
Map<String, dynamic> metadata,
|
||||
bool isActive,
|
||||
String createdAt,
|
||||
String updatedAt,
|
||||
List<ProductVariant> variants,
|
||||
String sellBy,
|
||||
String unitId,
|
||||
String unitName,
|
||||
String unitAbbreviation,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -342,11 +354,16 @@ class _$ProductCopyWithImpl<$Res, $Val extends Product>
|
||||
Object? businessType = null,
|
||||
Object? imageUrl = null,
|
||||
Object? printerType = null,
|
||||
Object? printToChecker = null,
|
||||
Object? metadata = null,
|
||||
Object? isActive = null,
|
||||
Object? createdAt = null,
|
||||
Object? updatedAt = null,
|
||||
Object? variants = null,
|
||||
Object? sellBy = null,
|
||||
Object? unitId = null,
|
||||
Object? unitName = null,
|
||||
Object? unitAbbreviation = null,
|
||||
}) {
|
||||
return _then(
|
||||
_value.copyWith(
|
||||
@@ -394,6 +411,10 @@ class _$ProductCopyWithImpl<$Res, $Val extends Product>
|
||||
? _value.printerType
|
||||
: printerType // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
printToChecker: null == printToChecker
|
||||
? _value.printToChecker
|
||||
: printToChecker // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
metadata: null == metadata
|
||||
? _value.metadata
|
||||
: metadata // ignore: cast_nullable_to_non_nullable
|
||||
@@ -414,6 +435,22 @@ class _$ProductCopyWithImpl<$Res, $Val extends Product>
|
||||
? _value.variants
|
||||
: variants // ignore: cast_nullable_to_non_nullable
|
||||
as List<ProductVariant>,
|
||||
sellBy: null == sellBy
|
||||
? _value.sellBy
|
||||
: sellBy // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
unitId: null == unitId
|
||||
? _value.unitId
|
||||
: unitId // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
unitName: null == unitName
|
||||
? _value.unitName
|
||||
: unitName // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
unitAbbreviation: null == unitAbbreviation
|
||||
? _value.unitAbbreviation
|
||||
: unitAbbreviation // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
)
|
||||
as $Val,
|
||||
);
|
||||
@@ -440,11 +477,16 @@ abstract class _$$ProductImplCopyWith<$Res> implements $ProductCopyWith<$Res> {
|
||||
String businessType,
|
||||
String imageUrl,
|
||||
String printerType,
|
||||
bool printToChecker,
|
||||
Map<String, dynamic> metadata,
|
||||
bool isActive,
|
||||
String createdAt,
|
||||
String updatedAt,
|
||||
List<ProductVariant> variants,
|
||||
String sellBy,
|
||||
String unitId,
|
||||
String unitName,
|
||||
String unitAbbreviation,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -473,11 +515,16 @@ class __$$ProductImplCopyWithImpl<$Res>
|
||||
Object? businessType = null,
|
||||
Object? imageUrl = null,
|
||||
Object? printerType = null,
|
||||
Object? printToChecker = null,
|
||||
Object? metadata = null,
|
||||
Object? isActive = null,
|
||||
Object? createdAt = null,
|
||||
Object? updatedAt = null,
|
||||
Object? variants = null,
|
||||
Object? sellBy = null,
|
||||
Object? unitId = null,
|
||||
Object? unitName = null,
|
||||
Object? unitAbbreviation = null,
|
||||
}) {
|
||||
return _then(
|
||||
_$ProductImpl(
|
||||
@@ -525,6 +572,10 @@ class __$$ProductImplCopyWithImpl<$Res>
|
||||
? _value.printerType
|
||||
: printerType // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
printToChecker: null == printToChecker
|
||||
? _value.printToChecker
|
||||
: printToChecker // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
metadata: null == metadata
|
||||
? _value._metadata
|
||||
: metadata // ignore: cast_nullable_to_non_nullable
|
||||
@@ -545,6 +596,22 @@ class __$$ProductImplCopyWithImpl<$Res>
|
||||
? _value._variants
|
||||
: variants // ignore: cast_nullable_to_non_nullable
|
||||
as List<ProductVariant>,
|
||||
sellBy: null == sellBy
|
||||
? _value.sellBy
|
||||
: sellBy // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
unitId: null == unitId
|
||||
? _value.unitId
|
||||
: unitId // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
unitName: null == unitName
|
||||
? _value.unitName
|
||||
: unitName // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
unitAbbreviation: null == unitAbbreviation
|
||||
? _value.unitAbbreviation
|
||||
: unitAbbreviation // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -552,7 +619,7 @@ class __$$ProductImplCopyWithImpl<$Res>
|
||||
|
||||
/// @nodoc
|
||||
|
||||
class _$ProductImpl with DiagnosticableTreeMixin implements _Product {
|
||||
class _$ProductImpl extends _Product with DiagnosticableTreeMixin {
|
||||
const _$ProductImpl({
|
||||
required this.id,
|
||||
required this.organizationId,
|
||||
@@ -565,13 +632,19 @@ class _$ProductImpl with DiagnosticableTreeMixin implements _Product {
|
||||
required this.businessType,
|
||||
required this.imageUrl,
|
||||
required this.printerType,
|
||||
required this.printToChecker,
|
||||
required final Map<String, dynamic> metadata,
|
||||
required this.isActive,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
required final List<ProductVariant> variants,
|
||||
this.sellBy = SellBy.unit,
|
||||
this.unitId = '',
|
||||
this.unitName = '',
|
||||
this.unitAbbreviation = '',
|
||||
}) : _metadata = metadata,
|
||||
_variants = variants;
|
||||
_variants = variants,
|
||||
super._();
|
||||
|
||||
@override
|
||||
final String id;
|
||||
@@ -595,6 +668,8 @@ class _$ProductImpl with DiagnosticableTreeMixin implements _Product {
|
||||
final String imageUrl;
|
||||
@override
|
||||
final String printerType;
|
||||
@override
|
||||
final bool printToChecker;
|
||||
final Map<String, dynamic> _metadata;
|
||||
@override
|
||||
Map<String, dynamic> get metadata {
|
||||
@@ -617,9 +692,23 @@ class _$ProductImpl with DiagnosticableTreeMixin implements _Product {
|
||||
return EqualUnmodifiableListView(_variants);
|
||||
}
|
||||
|
||||
/// `unit` (default) atau `weight`. Lihat [SellBy].
|
||||
@override
|
||||
@JsonKey()
|
||||
final String sellBy;
|
||||
@override
|
||||
@JsonKey()
|
||||
final String unitId;
|
||||
@override
|
||||
@JsonKey()
|
||||
final String unitName;
|
||||
@override
|
||||
@JsonKey()
|
||||
final String unitAbbreviation;
|
||||
|
||||
@override
|
||||
String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) {
|
||||
return 'Product(id: $id, organizationId: $organizationId, categoryId: $categoryId, sku: $sku, name: $name, description: $description, price: $price, cost: $cost, businessType: $businessType, imageUrl: $imageUrl, printerType: $printerType, metadata: $metadata, isActive: $isActive, createdAt: $createdAt, updatedAt: $updatedAt, variants: $variants)';
|
||||
return 'Product(id: $id, organizationId: $organizationId, categoryId: $categoryId, sku: $sku, name: $name, description: $description, price: $price, cost: $cost, businessType: $businessType, imageUrl: $imageUrl, printerType: $printerType, printToChecker: $printToChecker, metadata: $metadata, isActive: $isActive, createdAt: $createdAt, updatedAt: $updatedAt, variants: $variants, sellBy: $sellBy, unitId: $unitId, unitName: $unitName, unitAbbreviation: $unitAbbreviation)';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -638,11 +727,16 @@ class _$ProductImpl with DiagnosticableTreeMixin implements _Product {
|
||||
..add(DiagnosticsProperty('businessType', businessType))
|
||||
..add(DiagnosticsProperty('imageUrl', imageUrl))
|
||||
..add(DiagnosticsProperty('printerType', printerType))
|
||||
..add(DiagnosticsProperty('printToChecker', printToChecker))
|
||||
..add(DiagnosticsProperty('metadata', metadata))
|
||||
..add(DiagnosticsProperty('isActive', isActive))
|
||||
..add(DiagnosticsProperty('createdAt', createdAt))
|
||||
..add(DiagnosticsProperty('updatedAt', updatedAt))
|
||||
..add(DiagnosticsProperty('variants', variants));
|
||||
..add(DiagnosticsProperty('variants', variants))
|
||||
..add(DiagnosticsProperty('sellBy', sellBy))
|
||||
..add(DiagnosticsProperty('unitId', unitId))
|
||||
..add(DiagnosticsProperty('unitName', unitName))
|
||||
..add(DiagnosticsProperty('unitAbbreviation', unitAbbreviation));
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -667,6 +761,8 @@ class _$ProductImpl with DiagnosticableTreeMixin implements _Product {
|
||||
other.imageUrl == imageUrl) &&
|
||||
(identical(other.printerType, printerType) ||
|
||||
other.printerType == printerType) &&
|
||||
(identical(other.printToChecker, printToChecker) ||
|
||||
other.printToChecker == printToChecker) &&
|
||||
const DeepCollectionEquality().equals(other._metadata, _metadata) &&
|
||||
(identical(other.isActive, isActive) ||
|
||||
other.isActive == isActive) &&
|
||||
@@ -674,11 +770,17 @@ class _$ProductImpl with DiagnosticableTreeMixin implements _Product {
|
||||
other.createdAt == createdAt) &&
|
||||
(identical(other.updatedAt, updatedAt) ||
|
||||
other.updatedAt == updatedAt) &&
|
||||
const DeepCollectionEquality().equals(other._variants, _variants));
|
||||
const DeepCollectionEquality().equals(other._variants, _variants) &&
|
||||
(identical(other.sellBy, sellBy) || other.sellBy == sellBy) &&
|
||||
(identical(other.unitId, unitId) || other.unitId == unitId) &&
|
||||
(identical(other.unitName, unitName) ||
|
||||
other.unitName == unitName) &&
|
||||
(identical(other.unitAbbreviation, unitAbbreviation) ||
|
||||
other.unitAbbreviation == unitAbbreviation));
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
int get hashCode => Object.hashAll([
|
||||
runtimeType,
|
||||
id,
|
||||
organizationId,
|
||||
@@ -691,12 +793,17 @@ class _$ProductImpl with DiagnosticableTreeMixin implements _Product {
|
||||
businessType,
|
||||
imageUrl,
|
||||
printerType,
|
||||
printToChecker,
|
||||
const DeepCollectionEquality().hash(_metadata),
|
||||
isActive,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
const DeepCollectionEquality().hash(_variants),
|
||||
);
|
||||
sellBy,
|
||||
unitId,
|
||||
unitName,
|
||||
unitAbbreviation,
|
||||
]);
|
||||
|
||||
/// Create a copy of Product
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@@ -707,7 +814,7 @@ class _$ProductImpl with DiagnosticableTreeMixin implements _Product {
|
||||
__$$ProductImplCopyWithImpl<_$ProductImpl>(this, _$identity);
|
||||
}
|
||||
|
||||
abstract class _Product implements Product {
|
||||
abstract class _Product extends Product {
|
||||
const factory _Product({
|
||||
required final String id,
|
||||
required final String organizationId,
|
||||
@@ -720,12 +827,18 @@ abstract class _Product implements Product {
|
||||
required final String businessType,
|
||||
required final String imageUrl,
|
||||
required final String printerType,
|
||||
required final bool printToChecker,
|
||||
required final Map<String, dynamic> metadata,
|
||||
required final bool isActive,
|
||||
required final String createdAt,
|
||||
required final String updatedAt,
|
||||
required final List<ProductVariant> variants,
|
||||
final String sellBy,
|
||||
final String unitId,
|
||||
final String unitName,
|
||||
final String unitAbbreviation,
|
||||
}) = _$ProductImpl;
|
||||
const _Product._() : super._();
|
||||
|
||||
@override
|
||||
String get id;
|
||||
@@ -750,6 +863,8 @@ abstract class _Product implements Product {
|
||||
@override
|
||||
String get printerType;
|
||||
@override
|
||||
bool get printToChecker;
|
||||
@override
|
||||
Map<String, dynamic> get metadata;
|
||||
@override
|
||||
bool get isActive;
|
||||
@@ -760,6 +875,16 @@ abstract class _Product implements Product {
|
||||
@override
|
||||
List<ProductVariant> get variants;
|
||||
|
||||
/// `unit` (default) atau `weight`. Lihat [SellBy].
|
||||
@override
|
||||
String get sellBy;
|
||||
@override
|
||||
String get unitId;
|
||||
@override
|
||||
String get unitName;
|
||||
@override
|
||||
String get unitAbbreviation;
|
||||
|
||||
/// Create a copy of Product
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@@ -1098,6 +1223,10 @@ mixin _$ProductQuantity {
|
||||
ProductVariant? get variant => throw _privateConstructorUsedError;
|
||||
int get quantity => throw _privateConstructorUsedError;
|
||||
String get notes => throw _privateConstructorUsedError;
|
||||
String get lineId => throw _privateConstructorUsedError;
|
||||
|
||||
/// Angka timbangan dalam satuan produk. `null` untuk produk cacah.
|
||||
double? get weight => throw _privateConstructorUsedError;
|
||||
|
||||
/// Create a copy of ProductQuantity
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@@ -1118,6 +1247,8 @@ abstract class $ProductQuantityCopyWith<$Res> {
|
||||
ProductVariant? variant,
|
||||
int quantity,
|
||||
String notes,
|
||||
String lineId,
|
||||
double? weight,
|
||||
});
|
||||
|
||||
$ProductCopyWith<$Res> get product;
|
||||
@@ -1143,6 +1274,8 @@ class _$ProductQuantityCopyWithImpl<$Res, $Val extends ProductQuantity>
|
||||
Object? variant = freezed,
|
||||
Object? quantity = null,
|
||||
Object? notes = null,
|
||||
Object? lineId = null,
|
||||
Object? weight = freezed,
|
||||
}) {
|
||||
return _then(
|
||||
_value.copyWith(
|
||||
@@ -1162,6 +1295,14 @@ class _$ProductQuantityCopyWithImpl<$Res, $Val extends ProductQuantity>
|
||||
? _value.notes
|
||||
: notes // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
lineId: null == lineId
|
||||
? _value.lineId
|
||||
: lineId // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
weight: freezed == weight
|
||||
? _value.weight
|
||||
: weight // ignore: cast_nullable_to_non_nullable
|
||||
as double?,
|
||||
)
|
||||
as $Val,
|
||||
);
|
||||
@@ -1206,6 +1347,8 @@ abstract class _$$ProductQuantityImplCopyWith<$Res>
|
||||
ProductVariant? variant,
|
||||
int quantity,
|
||||
String notes,
|
||||
String lineId,
|
||||
double? weight,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -1232,6 +1375,8 @@ class __$$ProductQuantityImplCopyWithImpl<$Res>
|
||||
Object? variant = freezed,
|
||||
Object? quantity = null,
|
||||
Object? notes = null,
|
||||
Object? lineId = null,
|
||||
Object? weight = freezed,
|
||||
}) {
|
||||
return _then(
|
||||
_$ProductQuantityImpl(
|
||||
@@ -1251,6 +1396,14 @@ class __$$ProductQuantityImplCopyWithImpl<$Res>
|
||||
? _value.notes
|
||||
: notes // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
lineId: null == lineId
|
||||
? _value.lineId
|
||||
: lineId // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
weight: freezed == weight
|
||||
? _value.weight
|
||||
: weight // ignore: cast_nullable_to_non_nullable
|
||||
as double?,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -1258,15 +1411,16 @@ class __$$ProductQuantityImplCopyWithImpl<$Res>
|
||||
|
||||
/// @nodoc
|
||||
|
||||
class _$ProductQuantityImpl
|
||||
with DiagnosticableTreeMixin
|
||||
implements _ProductQuantity {
|
||||
class _$ProductQuantityImpl extends _ProductQuantity
|
||||
with DiagnosticableTreeMixin {
|
||||
const _$ProductQuantityImpl({
|
||||
required this.product,
|
||||
this.variant,
|
||||
required this.quantity,
|
||||
required this.notes,
|
||||
});
|
||||
this.lineId = '',
|
||||
this.weight,
|
||||
}) : super._();
|
||||
|
||||
@override
|
||||
final Product product;
|
||||
@@ -1276,10 +1430,17 @@ class _$ProductQuantityImpl
|
||||
final int quantity;
|
||||
@override
|
||||
final String notes;
|
||||
@override
|
||||
@JsonKey()
|
||||
final String lineId;
|
||||
|
||||
/// Angka timbangan dalam satuan produk. `null` untuk produk cacah.
|
||||
@override
|
||||
final double? weight;
|
||||
|
||||
@override
|
||||
String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) {
|
||||
return 'ProductQuantity(product: $product, variant: $variant, quantity: $quantity, notes: $notes)';
|
||||
return 'ProductQuantity(product: $product, variant: $variant, quantity: $quantity, notes: $notes, lineId: $lineId, weight: $weight)';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -1290,7 +1451,9 @@ class _$ProductQuantityImpl
|
||||
..add(DiagnosticsProperty('product', product))
|
||||
..add(DiagnosticsProperty('variant', variant))
|
||||
..add(DiagnosticsProperty('quantity', quantity))
|
||||
..add(DiagnosticsProperty('notes', notes));
|
||||
..add(DiagnosticsProperty('notes', notes))
|
||||
..add(DiagnosticsProperty('lineId', lineId))
|
||||
..add(DiagnosticsProperty('weight', weight));
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -1302,12 +1465,21 @@ class _$ProductQuantityImpl
|
||||
(identical(other.variant, variant) || other.variant == variant) &&
|
||||
(identical(other.quantity, quantity) ||
|
||||
other.quantity == quantity) &&
|
||||
(identical(other.notes, notes) || other.notes == notes));
|
||||
(identical(other.notes, notes) || other.notes == notes) &&
|
||||
(identical(other.lineId, lineId) || other.lineId == lineId) &&
|
||||
(identical(other.weight, weight) || other.weight == weight));
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
Object.hash(runtimeType, product, variant, quantity, notes);
|
||||
int get hashCode => Object.hash(
|
||||
runtimeType,
|
||||
product,
|
||||
variant,
|
||||
quantity,
|
||||
notes,
|
||||
lineId,
|
||||
weight,
|
||||
);
|
||||
|
||||
/// Create a copy of ProductQuantity
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@@ -1321,13 +1493,16 @@ class _$ProductQuantityImpl
|
||||
);
|
||||
}
|
||||
|
||||
abstract class _ProductQuantity implements ProductQuantity {
|
||||
abstract class _ProductQuantity extends ProductQuantity {
|
||||
const factory _ProductQuantity({
|
||||
required final Product product,
|
||||
final ProductVariant? variant,
|
||||
required final int quantity,
|
||||
required final String notes,
|
||||
final String lineId,
|
||||
final double? weight,
|
||||
}) = _$ProductQuantityImpl;
|
||||
const _ProductQuantity._() : super._();
|
||||
|
||||
@override
|
||||
Product get product;
|
||||
@@ -1337,6 +1512,12 @@ abstract class _ProductQuantity implements ProductQuantity {
|
||||
int get quantity;
|
||||
@override
|
||||
String get notes;
|
||||
@override
|
||||
String get lineId;
|
||||
|
||||
/// Angka timbangan dalam satuan produk. `null` untuk produk cacah.
|
||||
@override
|
||||
double? get weight;
|
||||
|
||||
/// Create a copy of ProductQuantity
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
part of '../unit.dart';
|
||||
|
||||
/// Satuan timbangan milik organisasi — ons, kg, gram.
|
||||
///
|
||||
/// [abbreviation] yang dipakai POS untuk mencetak `4,2 ons` di struk.
|
||||
@freezed
|
||||
class ProductUnit with _$ProductUnit {
|
||||
const factory ProductUnit({
|
||||
required String id,
|
||||
required String organizationId,
|
||||
required String name,
|
||||
required String abbreviation,
|
||||
required bool isActive,
|
||||
}) = _ProductUnit;
|
||||
|
||||
factory ProductUnit.empty() => const ProductUnit(
|
||||
id: '',
|
||||
organizationId: '',
|
||||
name: '',
|
||||
abbreviation: '',
|
||||
isActive: false,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
part of '../unit.dart';
|
||||
|
||||
@freezed
|
||||
sealed class UnitFailure with _$UnitFailure {
|
||||
const factory UnitFailure.serverError(ApiFailure failure) = _ServerError;
|
||||
const factory UnitFailure.unexpectedError() = _UnexpectedError;
|
||||
const factory UnitFailure.empty() = _Empty;
|
||||
const factory UnitFailure.localStorageError(String erroMessage) =
|
||||
_LocalStorageError;
|
||||
const factory UnitFailure.dynamicErrorMessage(String erroMessage) =
|
||||
_DynamicErrorMessage;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
part of '../unit.dart';
|
||||
|
||||
abstract class IUnitRepository {
|
||||
/// Ambil satuan dari server lalu simpan ke cache lokal.
|
||||
/// Bila server tidak terjangkau, kembalikan isi cache supaya POS tetap bisa
|
||||
/// mencetak `4,2 ons` saat offline.
|
||||
Future<Either<UnitFailure, List<ProductUnit>>> getUnits({
|
||||
bool forceRefresh = false,
|
||||
});
|
||||
|
||||
Future<List<ProductUnit>> cachedUnits();
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
import '../../common/api/api_failure.dart';
|
||||
|
||||
part 'unit.freezed.dart';
|
||||
|
||||
part 'entities/unit_entity.dart';
|
||||
part 'failures/unit_failure.dart';
|
||||
part 'repositories/i_unit_repository.dart';
|
||||
File diff suppressed because it is too large
Load Diff
+4
-4
@@ -10,18 +10,18 @@ abstract class Env {
|
||||
@dev
|
||||
class DevEnv implements Env {
|
||||
@override
|
||||
String get baseUrl => 'https://api-pos.apskel.id';
|
||||
String get baseUrl => 'https://enaklo-pos-api.altru.id';
|
||||
|
||||
@override
|
||||
String get dbName => "apskel_pos_dev.db"; // example value
|
||||
String get dbName => "apskel_pos_staging.db";
|
||||
}
|
||||
|
||||
@Injectable(as: Env)
|
||||
@prod
|
||||
class ProdEnv implements Env {
|
||||
@override
|
||||
String get baseUrl => 'https://api-pos.apskel.id';
|
||||
String get baseUrl => 'https://enaklo-pos-api.altru.id';
|
||||
|
||||
@override
|
||||
String get dbName => "apskel_pos_dev.db";
|
||||
String get dbName => "apskel_pos_prod.db";
|
||||
}
|
||||
|
||||
@@ -18,6 +18,15 @@ class OrderRemoteDataProvider {
|
||||
final _logName = 'OrderRemoteDataProvider';
|
||||
OrderRemoteDataProvider(this._apiClient);
|
||||
|
||||
/// Helper untuk merge auth header dengan idempotency key
|
||||
Map<String, dynamic> _buildHeaders({String? idempotencyKey}) {
|
||||
final headers = getAuthorizationHeader();
|
||||
if (idempotencyKey != null) {
|
||||
headers['X-Idempotency-Key'] = idempotencyKey;
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
|
||||
Future<DC<OrderFailure, ListOrderDto>> fetchOrders({
|
||||
int page = 1,
|
||||
int limit = 10,
|
||||
@@ -46,6 +55,12 @@ class OrderRemoteDataProvider {
|
||||
);
|
||||
|
||||
if (response.data['success'] == false) {
|
||||
// Pesan validasi backend menyebut nama produk, jadi diteruskan apa
|
||||
// adanya ke kasir alih-alih diganti pesan generik.
|
||||
final cause = extractApiErrorCause(response.data);
|
||||
if (cause != null) {
|
||||
return DC.error(OrderFailure.dynamicErrorMessage(cause));
|
||||
}
|
||||
return DC.error(const OrderFailure.unexpectedError());
|
||||
}
|
||||
|
||||
@@ -68,6 +83,12 @@ class OrderRemoteDataProvider {
|
||||
);
|
||||
|
||||
if (response.data['success'] == false) {
|
||||
// Pesan validasi backend menyebut nama produk, jadi diteruskan apa
|
||||
// adanya ke kasir alih-alih diganti pesan generik.
|
||||
final cause = extractApiErrorCause(response.data);
|
||||
if (cause != null) {
|
||||
return DC.error(OrderFailure.dynamicErrorMessage(cause));
|
||||
}
|
||||
return DC.error(const OrderFailure.unexpectedError());
|
||||
}
|
||||
|
||||
@@ -93,6 +114,12 @@ class OrderRemoteDataProvider {
|
||||
);
|
||||
|
||||
if (response.data['success'] == false) {
|
||||
// Pesan validasi backend menyebut nama produk, jadi diteruskan apa
|
||||
// adanya ke kasir alih-alih diganti pesan generik.
|
||||
final cause = extractApiErrorCause(response.data);
|
||||
if (cause != null) {
|
||||
return DC.error(OrderFailure.dynamicErrorMessage(cause));
|
||||
}
|
||||
return DC.error(const OrderFailure.unexpectedError());
|
||||
}
|
||||
|
||||
@@ -119,6 +146,12 @@ class OrderRemoteDataProvider {
|
||||
);
|
||||
|
||||
if (response.data['success'] == false) {
|
||||
// Pesan validasi backend menyebut nama produk, jadi diteruskan apa
|
||||
// adanya ke kasir alih-alih diganti pesan generik.
|
||||
final cause = extractApiErrorCause(response.data);
|
||||
if (cause != null) {
|
||||
return DC.error(OrderFailure.dynamicErrorMessage(cause));
|
||||
}
|
||||
return DC.error(const OrderFailure.unexpectedError());
|
||||
} else {
|
||||
final order = OrderDto.fromJson(
|
||||
@@ -151,16 +184,23 @@ class OrderRemoteDataProvider {
|
||||
}
|
||||
|
||||
Future<DC<OrderFailure, PaymentDto>> storePayment(
|
||||
PaymentRequestDto request,
|
||||
) async {
|
||||
PaymentRequestDto request, {
|
||||
String? idempotencyKey,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _apiClient.post(
|
||||
ApiPath.payments,
|
||||
data: request.toJson(),
|
||||
headers: getAuthorizationHeader(),
|
||||
headers: _buildHeaders(idempotencyKey: idempotencyKey),
|
||||
);
|
||||
|
||||
if (response.data['success'] == false) {
|
||||
// Pesan validasi backend menyebut nama produk, jadi diteruskan apa
|
||||
// adanya ke kasir alih-alih diganti pesan generik.
|
||||
final cause = extractApiErrorCause(response.data);
|
||||
if (cause != null) {
|
||||
return DC.error(OrderFailure.dynamicErrorMessage(cause));
|
||||
}
|
||||
return DC.error(const OrderFailure.unexpectedError());
|
||||
}
|
||||
|
||||
@@ -178,6 +218,7 @@ class OrderRemoteDataProvider {
|
||||
Future<DC<OrderFailure, OrderDto>> addItemOrder({
|
||||
required String id,
|
||||
required List<AddItemOrderRequestDto> request,
|
||||
String? idempotencyKey,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _apiClient.post(
|
||||
@@ -186,10 +227,16 @@ class OrderRemoteDataProvider {
|
||||
'notes': '',
|
||||
'order_items': request.map((e) => e.toRequest()).toList(),
|
||||
},
|
||||
headers: getAuthorizationHeader(),
|
||||
headers: _buildHeaders(idempotencyKey: idempotencyKey),
|
||||
);
|
||||
|
||||
if (response.data['success'] == false) {
|
||||
// Pesan validasi backend menyebut nama produk, jadi diteruskan apa
|
||||
// adanya ke kasir alih-alih diganti pesan generik.
|
||||
final cause = extractApiErrorCause(response.data);
|
||||
if (cause != null) {
|
||||
return DC.error(OrderFailure.dynamicErrorMessage(cause));
|
||||
}
|
||||
return DC.error(const OrderFailure.unexpectedError());
|
||||
}
|
||||
|
||||
@@ -209,6 +256,7 @@ class OrderRemoteDataProvider {
|
||||
required String reason,
|
||||
String type = "ITEM", // TYPE: ALL, ITEM
|
||||
required List<OrderItemDto> orderItems,
|
||||
String? idempotencyKey,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _apiClient.post(
|
||||
@@ -223,10 +271,16 @@ class OrderRemoteDataProvider {
|
||||
)
|
||||
.toList(),
|
||||
},
|
||||
headers: getAuthorizationHeader(),
|
||||
headers: _buildHeaders(idempotencyKey: idempotencyKey),
|
||||
);
|
||||
|
||||
if (response.data['success'] == false) {
|
||||
// Pesan validasi backend menyebut nama produk, jadi diteruskan apa
|
||||
// adanya ke kasir alih-alih diganti pesan generik.
|
||||
final cause = extractApiErrorCause(response.data);
|
||||
if (cause != null) {
|
||||
return DC.error(OrderFailure.dynamicErrorMessage(cause));
|
||||
}
|
||||
return DC.error(const OrderFailure.unexpectedError());
|
||||
}
|
||||
|
||||
@@ -238,17 +292,24 @@ class OrderRemoteDataProvider {
|
||||
}
|
||||
|
||||
Future<DC<OrderFailure, PaymentDto>> createSplitBill(
|
||||
PaymentSplitBillRequestDto request,
|
||||
) async {
|
||||
PaymentSplitBillRequestDto request, {
|
||||
String? idempotencyKey,
|
||||
}) async {
|
||||
log(request.toRequest().toString());
|
||||
try {
|
||||
final response = await _apiClient.post(
|
||||
"${ApiPath.orders}/split-bill",
|
||||
data: request.toRequest(),
|
||||
headers: getAuthorizationHeader(),
|
||||
headers: _buildHeaders(idempotencyKey: idempotencyKey),
|
||||
);
|
||||
|
||||
if (response.data['success'] == false) {
|
||||
// Pesan validasi backend menyebut nama produk, jadi diteruskan apa
|
||||
// adanya ke kasir alih-alih diganti pesan generik.
|
||||
final cause = extractApiErrorCause(response.data);
|
||||
if (cause != null) {
|
||||
return DC.error(OrderFailure.dynamicErrorMessage(cause));
|
||||
}
|
||||
return DC.error(const OrderFailure.unexpectedError());
|
||||
}
|
||||
|
||||
@@ -267,15 +328,22 @@ class OrderRemoteDataProvider {
|
||||
required String id,
|
||||
required String reason,
|
||||
required int refundAmount,
|
||||
String? idempotencyKey,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _apiClient.post(
|
||||
'${ApiPath.orders}/$id/refund',
|
||||
data: {'refund_amount': refundAmount, 'reason': reason},
|
||||
headers: getAuthorizationHeader(),
|
||||
headers: _buildHeaders(idempotencyKey: idempotencyKey),
|
||||
);
|
||||
|
||||
if (response.data['success'] == false) {
|
||||
// Pesan validasi backend menyebut nama produk, jadi diteruskan apa
|
||||
// adanya ke kasir alih-alih diganti pesan generik.
|
||||
final cause = extractApiErrorCause(response.data);
|
||||
if (cause != null) {
|
||||
return DC.error(OrderFailure.dynamicErrorMessage(cause));
|
||||
}
|
||||
return DC.error(const OrderFailure.unexpectedError());
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ class AddItemOrderRequestDto with _$AddItemOrderRequestDto {
|
||||
@JsonKey(name: 'quantity') int? quantity,
|
||||
@JsonKey(name: 'unit_price') int? unitPrice,
|
||||
@JsonKey(name: 'notes') String? notes,
|
||||
@JsonKey(name: 'weight') double? weight,
|
||||
}) = _AddItemOrderRequestDto;
|
||||
|
||||
factory AddItemOrderRequestDto.fromJson(Map<String, dynamic> json) =>
|
||||
@@ -21,6 +22,7 @@ class AddItemOrderRequestDto with _$AddItemOrderRequestDto {
|
||||
quantity: quantity ?? 0,
|
||||
unitPrice: unitPrice ?? 0,
|
||||
notes: notes ?? '',
|
||||
weight: weight,
|
||||
);
|
||||
|
||||
factory AddItemOrderRequestDto.fromDomain(
|
||||
@@ -31,6 +33,7 @@ class AddItemOrderRequestDto with _$AddItemOrderRequestDto {
|
||||
quantity: addItemOrderRequest.quantity,
|
||||
unitPrice: addItemOrderRequest.unitPrice,
|
||||
notes: addItemOrderRequest.notes,
|
||||
weight: addItemOrderRequest.weight,
|
||||
);
|
||||
|
||||
Map<String, dynamic> toRequest() {
|
||||
@@ -45,6 +48,11 @@ class AddItemOrderRequestDto with _$AddItemOrderRequestDto {
|
||||
data['product_variant_id'] = productvariantId;
|
||||
}
|
||||
|
||||
// Produk satuan ditolak backend bila membawa berat.
|
||||
if (weight != null) {
|
||||
data['weight'] = weight;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,9 +114,14 @@ class OrderItemDto with _$OrderItemDto {
|
||||
@JsonKey(name: "created_at") String? createdAt,
|
||||
@JsonKey(name: "updated_at") String? updatedAt,
|
||||
@JsonKey(name: "printer_type") String? printerType,
|
||||
@JsonKey(name: "print_to_checker") bool? printToChecker,
|
||||
@JsonKey(name: "paid_quantity") int? paidQuantity,
|
||||
@JsonKey(name: "category_id") String? categoryId,
|
||||
@JsonKey(name: "category_name") String? categoryName,
|
||||
@JsonKey(name: "weight") double? weight,
|
||||
@JsonKey(name: "unit_id") String? unitId,
|
||||
@JsonKey(name: "unit_name") String? unitName,
|
||||
@JsonKey(name: "unit_abbreviation") String? unitAbbreviation,
|
||||
}) = _OrderItemDto;
|
||||
|
||||
factory OrderItemDto.fromJson(Map<String, dynamic> json) =>
|
||||
@@ -139,9 +144,14 @@ class OrderItemDto with _$OrderItemDto {
|
||||
createdAt: createdAt != null ? DateTime.parse(createdAt!) : DateTime(1970),
|
||||
updatedAt: updatedAt != null ? DateTime.parse(updatedAt!) : DateTime(1970),
|
||||
printerType: printerType ?? '',
|
||||
printToChecker: printToChecker ?? false,
|
||||
paidQuantity: paidQuantity ?? 0,
|
||||
categoryId: categoryId ?? '',
|
||||
categoryName: categoryName ?? '',
|
||||
weight: weight,
|
||||
unitId: unitId ?? '',
|
||||
unitName: unitName ?? '',
|
||||
unitAbbreviation: unitAbbreviation ?? '',
|
||||
);
|
||||
|
||||
factory OrderItemDto.fromDomain(OrderItem orderItem) => OrderItemDto(
|
||||
@@ -160,8 +170,13 @@ class OrderItemDto with _$OrderItemDto {
|
||||
createdAt: orderItem.createdAt.toIso8601String(),
|
||||
updatedAt: orderItem.updatedAt.toIso8601String(),
|
||||
printerType: orderItem.printerType,
|
||||
printToChecker: orderItem.printToChecker,
|
||||
paidQuantity: orderItem.paidQuantity,
|
||||
categoryId: orderItem.categoryId,
|
||||
categoryName: orderItem.categoryName,
|
||||
weight: orderItem.weight,
|
||||
unitId: orderItem.unitId,
|
||||
unitName: orderItem.unitName,
|
||||
unitAbbreviation: orderItem.unitAbbreviation,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -75,6 +75,7 @@ class OrderItemRequestDto with _$OrderItemRequestDto {
|
||||
@JsonKey(name: "quantity") int? quantity,
|
||||
@JsonKey(name: "unit_price") int? unitPrice,
|
||||
@JsonKey(name: "notes") String? notes,
|
||||
@JsonKey(name: "weight") double? weight,
|
||||
}) = _OrderItemRequestDto;
|
||||
|
||||
factory OrderItemRequestDto.fromJson(Map<String, dynamic> json) =>
|
||||
@@ -87,6 +88,7 @@ class OrderItemRequestDto with _$OrderItemRequestDto {
|
||||
quantity: quantity ?? 0,
|
||||
unitPrice: unitPrice ?? 0,
|
||||
notes: notes ?? '',
|
||||
weight: weight,
|
||||
);
|
||||
|
||||
factory OrderItemRequestDto.fromDomain(OrderItemRequest request) =>
|
||||
@@ -96,6 +98,7 @@ class OrderItemRequestDto with _$OrderItemRequestDto {
|
||||
quantity: request.quantity,
|
||||
unitPrice: request.unitPrice,
|
||||
notes: request.notes,
|
||||
weight: request.weight,
|
||||
);
|
||||
|
||||
Map<String, dynamic> toRequest() {
|
||||
@@ -110,6 +113,12 @@ class OrderItemRequestDto with _$OrderItemRequestDto {
|
||||
data["product_variant_id"] = productVariantId;
|
||||
}
|
||||
|
||||
// Produk satuan ditolak backend bila membawa berat, jadi field ini hanya
|
||||
// ikut saat memang ada hasil timbangan.
|
||||
if (weight != null) {
|
||||
data["weight"] = weight;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1169,12 +1169,22 @@ mixin _$OrderItemDto {
|
||||
String? get updatedAt => throw _privateConstructorUsedError;
|
||||
@JsonKey(name: "printer_type")
|
||||
String? get printerType => throw _privateConstructorUsedError;
|
||||
@JsonKey(name: "print_to_checker")
|
||||
bool? get printToChecker => throw _privateConstructorUsedError;
|
||||
@JsonKey(name: "paid_quantity")
|
||||
int? get paidQuantity => throw _privateConstructorUsedError;
|
||||
@JsonKey(name: "category_id")
|
||||
String? get categoryId => throw _privateConstructorUsedError;
|
||||
@JsonKey(name: "category_name")
|
||||
String? get categoryName => throw _privateConstructorUsedError;
|
||||
@JsonKey(name: "weight")
|
||||
double? get weight => throw _privateConstructorUsedError;
|
||||
@JsonKey(name: "unit_id")
|
||||
String? get unitId => throw _privateConstructorUsedError;
|
||||
@JsonKey(name: "unit_name")
|
||||
String? get unitName => throw _privateConstructorUsedError;
|
||||
@JsonKey(name: "unit_abbreviation")
|
||||
String? get unitAbbreviation => throw _privateConstructorUsedError;
|
||||
|
||||
/// Serializes this OrderItemDto to a JSON map.
|
||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
@@ -1209,9 +1219,14 @@ abstract class $OrderItemDtoCopyWith<$Res> {
|
||||
@JsonKey(name: "created_at") String? createdAt,
|
||||
@JsonKey(name: "updated_at") String? updatedAt,
|
||||
@JsonKey(name: "printer_type") String? printerType,
|
||||
@JsonKey(name: "print_to_checker") bool? printToChecker,
|
||||
@JsonKey(name: "paid_quantity") int? paidQuantity,
|
||||
@JsonKey(name: "category_id") String? categoryId,
|
||||
@JsonKey(name: "category_name") String? categoryName,
|
||||
@JsonKey(name: "weight") double? weight,
|
||||
@JsonKey(name: "unit_id") String? unitId,
|
||||
@JsonKey(name: "unit_name") String? unitName,
|
||||
@JsonKey(name: "unit_abbreviation") String? unitAbbreviation,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1245,9 +1260,14 @@ class _$OrderItemDtoCopyWithImpl<$Res, $Val extends OrderItemDto>
|
||||
Object? createdAt = freezed,
|
||||
Object? updatedAt = freezed,
|
||||
Object? printerType = freezed,
|
||||
Object? printToChecker = freezed,
|
||||
Object? paidQuantity = freezed,
|
||||
Object? categoryId = freezed,
|
||||
Object? categoryName = freezed,
|
||||
Object? weight = freezed,
|
||||
Object? unitId = freezed,
|
||||
Object? unitName = freezed,
|
||||
Object? unitAbbreviation = freezed,
|
||||
}) {
|
||||
return _then(
|
||||
_value.copyWith(
|
||||
@@ -1311,6 +1331,10 @@ class _$OrderItemDtoCopyWithImpl<$Res, $Val extends OrderItemDto>
|
||||
? _value.printerType
|
||||
: printerType // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
printToChecker: freezed == printToChecker
|
||||
? _value.printToChecker
|
||||
: printToChecker // ignore: cast_nullable_to_non_nullable
|
||||
as bool?,
|
||||
paidQuantity: freezed == paidQuantity
|
||||
? _value.paidQuantity
|
||||
: paidQuantity // ignore: cast_nullable_to_non_nullable
|
||||
@@ -1323,6 +1347,22 @@ class _$OrderItemDtoCopyWithImpl<$Res, $Val extends OrderItemDto>
|
||||
? _value.categoryName
|
||||
: categoryName // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
weight: freezed == weight
|
||||
? _value.weight
|
||||
: weight // ignore: cast_nullable_to_non_nullable
|
||||
as double?,
|
||||
unitId: freezed == unitId
|
||||
? _value.unitId
|
||||
: unitId // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
unitName: freezed == unitName
|
||||
? _value.unitName
|
||||
: unitName // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
unitAbbreviation: freezed == unitAbbreviation
|
||||
? _value.unitAbbreviation
|
||||
: unitAbbreviation // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
)
|
||||
as $Val,
|
||||
);
|
||||
@@ -1354,9 +1394,14 @@ abstract class _$$OrderItemDtoImplCopyWith<$Res>
|
||||
@JsonKey(name: "created_at") String? createdAt,
|
||||
@JsonKey(name: "updated_at") String? updatedAt,
|
||||
@JsonKey(name: "printer_type") String? printerType,
|
||||
@JsonKey(name: "print_to_checker") bool? printToChecker,
|
||||
@JsonKey(name: "paid_quantity") int? paidQuantity,
|
||||
@JsonKey(name: "category_id") String? categoryId,
|
||||
@JsonKey(name: "category_name") String? categoryName,
|
||||
@JsonKey(name: "weight") double? weight,
|
||||
@JsonKey(name: "unit_id") String? unitId,
|
||||
@JsonKey(name: "unit_name") String? unitName,
|
||||
@JsonKey(name: "unit_abbreviation") String? unitAbbreviation,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1389,9 +1434,14 @@ class __$$OrderItemDtoImplCopyWithImpl<$Res>
|
||||
Object? createdAt = freezed,
|
||||
Object? updatedAt = freezed,
|
||||
Object? printerType = freezed,
|
||||
Object? printToChecker = freezed,
|
||||
Object? paidQuantity = freezed,
|
||||
Object? categoryId = freezed,
|
||||
Object? categoryName = freezed,
|
||||
Object? weight = freezed,
|
||||
Object? unitId = freezed,
|
||||
Object? unitName = freezed,
|
||||
Object? unitAbbreviation = freezed,
|
||||
}) {
|
||||
return _then(
|
||||
_$OrderItemDtoImpl(
|
||||
@@ -1455,6 +1505,10 @@ class __$$OrderItemDtoImplCopyWithImpl<$Res>
|
||||
? _value.printerType
|
||||
: printerType // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
printToChecker: freezed == printToChecker
|
||||
? _value.printToChecker
|
||||
: printToChecker // ignore: cast_nullable_to_non_nullable
|
||||
as bool?,
|
||||
paidQuantity: freezed == paidQuantity
|
||||
? _value.paidQuantity
|
||||
: paidQuantity // ignore: cast_nullable_to_non_nullable
|
||||
@@ -1467,6 +1521,22 @@ class __$$OrderItemDtoImplCopyWithImpl<$Res>
|
||||
? _value.categoryName
|
||||
: categoryName // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
weight: freezed == weight
|
||||
? _value.weight
|
||||
: weight // ignore: cast_nullable_to_non_nullable
|
||||
as double?,
|
||||
unitId: freezed == unitId
|
||||
? _value.unitId
|
||||
: unitId // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
unitName: freezed == unitName
|
||||
? _value.unitName
|
||||
: unitName // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
unitAbbreviation: freezed == unitAbbreviation
|
||||
? _value.unitAbbreviation
|
||||
: unitAbbreviation // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -1491,9 +1561,14 @@ class _$OrderItemDtoImpl extends _OrderItemDto {
|
||||
@JsonKey(name: "created_at") this.createdAt,
|
||||
@JsonKey(name: "updated_at") this.updatedAt,
|
||||
@JsonKey(name: "printer_type") this.printerType,
|
||||
@JsonKey(name: "print_to_checker") this.printToChecker,
|
||||
@JsonKey(name: "paid_quantity") this.paidQuantity,
|
||||
@JsonKey(name: "category_id") this.categoryId,
|
||||
@JsonKey(name: "category_name") this.categoryName,
|
||||
@JsonKey(name: "weight") this.weight,
|
||||
@JsonKey(name: "unit_id") this.unitId,
|
||||
@JsonKey(name: "unit_name") this.unitName,
|
||||
@JsonKey(name: "unit_abbreviation") this.unitAbbreviation,
|
||||
}) : _modifiers = modifiers,
|
||||
super._();
|
||||
|
||||
@@ -1554,6 +1629,9 @@ class _$OrderItemDtoImpl extends _OrderItemDto {
|
||||
@JsonKey(name: "printer_type")
|
||||
final String? printerType;
|
||||
@override
|
||||
@JsonKey(name: "print_to_checker")
|
||||
final bool? printToChecker;
|
||||
@override
|
||||
@JsonKey(name: "paid_quantity")
|
||||
final int? paidQuantity;
|
||||
@override
|
||||
@@ -1562,10 +1640,22 @@ class _$OrderItemDtoImpl extends _OrderItemDto {
|
||||
@override
|
||||
@JsonKey(name: "category_name")
|
||||
final String? categoryName;
|
||||
@override
|
||||
@JsonKey(name: "weight")
|
||||
final double? weight;
|
||||
@override
|
||||
@JsonKey(name: "unit_id")
|
||||
final String? unitId;
|
||||
@override
|
||||
@JsonKey(name: "unit_name")
|
||||
final String? unitName;
|
||||
@override
|
||||
@JsonKey(name: "unit_abbreviation")
|
||||
final String? unitAbbreviation;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'OrderItemDto(id: $id, orderId: $orderId, productId: $productId, productName: $productName, productVariantId: $productVariantId, productVariantName: $productVariantName, quantity: $quantity, unitPrice: $unitPrice, totalPrice: $totalPrice, modifiers: $modifiers, notes: $notes, status: $status, createdAt: $createdAt, updatedAt: $updatedAt, printerType: $printerType, paidQuantity: $paidQuantity, categoryId: $categoryId, categoryName: $categoryName)';
|
||||
return 'OrderItemDto(id: $id, orderId: $orderId, productId: $productId, productName: $productName, productVariantId: $productVariantId, productVariantName: $productVariantName, quantity: $quantity, unitPrice: $unitPrice, totalPrice: $totalPrice, modifiers: $modifiers, notes: $notes, status: $status, createdAt: $createdAt, updatedAt: $updatedAt, printerType: $printerType, printToChecker: $printToChecker, paidQuantity: $paidQuantity, categoryId: $categoryId, categoryName: $categoryName, weight: $weight, unitId: $unitId, unitName: $unitName, unitAbbreviation: $unitAbbreviation)';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -1601,17 +1691,25 @@ class _$OrderItemDtoImpl extends _OrderItemDto {
|
||||
other.updatedAt == updatedAt) &&
|
||||
(identical(other.printerType, printerType) ||
|
||||
other.printerType == printerType) &&
|
||||
(identical(other.printToChecker, printToChecker) ||
|
||||
other.printToChecker == printToChecker) &&
|
||||
(identical(other.paidQuantity, paidQuantity) ||
|
||||
other.paidQuantity == paidQuantity) &&
|
||||
(identical(other.categoryId, categoryId) ||
|
||||
other.categoryId == categoryId) &&
|
||||
(identical(other.categoryName, categoryName) ||
|
||||
other.categoryName == categoryName));
|
||||
other.categoryName == categoryName) &&
|
||||
(identical(other.weight, weight) || other.weight == weight) &&
|
||||
(identical(other.unitId, unitId) || other.unitId == unitId) &&
|
||||
(identical(other.unitName, unitName) ||
|
||||
other.unitName == unitName) &&
|
||||
(identical(other.unitAbbreviation, unitAbbreviation) ||
|
||||
other.unitAbbreviation == unitAbbreviation));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
int get hashCode => Object.hashAll([
|
||||
runtimeType,
|
||||
id,
|
||||
orderId,
|
||||
@@ -1628,10 +1726,15 @@ class _$OrderItemDtoImpl extends _OrderItemDto {
|
||||
createdAt,
|
||||
updatedAt,
|
||||
printerType,
|
||||
printToChecker,
|
||||
paidQuantity,
|
||||
categoryId,
|
||||
categoryName,
|
||||
);
|
||||
weight,
|
||||
unitId,
|
||||
unitName,
|
||||
unitAbbreviation,
|
||||
]);
|
||||
|
||||
/// Create a copy of OrderItemDto
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@@ -1664,9 +1767,14 @@ abstract class _OrderItemDto extends OrderItemDto {
|
||||
@JsonKey(name: "created_at") final String? createdAt,
|
||||
@JsonKey(name: "updated_at") final String? updatedAt,
|
||||
@JsonKey(name: "printer_type") final String? printerType,
|
||||
@JsonKey(name: "print_to_checker") final bool? printToChecker,
|
||||
@JsonKey(name: "paid_quantity") final int? paidQuantity,
|
||||
@JsonKey(name: "category_id") final String? categoryId,
|
||||
@JsonKey(name: "category_name") final String? categoryName,
|
||||
@JsonKey(name: "weight") final double? weight,
|
||||
@JsonKey(name: "unit_id") final String? unitId,
|
||||
@JsonKey(name: "unit_name") final String? unitName,
|
||||
@JsonKey(name: "unit_abbreviation") final String? unitAbbreviation,
|
||||
}) = _$OrderItemDtoImpl;
|
||||
const _OrderItemDto._() : super._();
|
||||
|
||||
@@ -1719,6 +1827,9 @@ abstract class _OrderItemDto extends OrderItemDto {
|
||||
@JsonKey(name: "printer_type")
|
||||
String? get printerType;
|
||||
@override
|
||||
@JsonKey(name: "print_to_checker")
|
||||
bool? get printToChecker;
|
||||
@override
|
||||
@JsonKey(name: "paid_quantity")
|
||||
int? get paidQuantity;
|
||||
@override
|
||||
@@ -1727,6 +1838,18 @@ abstract class _OrderItemDto extends OrderItemDto {
|
||||
@override
|
||||
@JsonKey(name: "category_name")
|
||||
String? get categoryName;
|
||||
@override
|
||||
@JsonKey(name: "weight")
|
||||
double? get weight;
|
||||
@override
|
||||
@JsonKey(name: "unit_id")
|
||||
String? get unitId;
|
||||
@override
|
||||
@JsonKey(name: "unit_name")
|
||||
String? get unitName;
|
||||
@override
|
||||
@JsonKey(name: "unit_abbreviation")
|
||||
String? get unitAbbreviation;
|
||||
|
||||
/// Create a copy of OrderItemDto
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@@ -2111,6 +2234,8 @@ mixin _$OrderItemRequestDto {
|
||||
int? get unitPrice => throw _privateConstructorUsedError;
|
||||
@JsonKey(name: "notes")
|
||||
String? get notes => throw _privateConstructorUsedError;
|
||||
@JsonKey(name: "weight")
|
||||
double? get weight => throw _privateConstructorUsedError;
|
||||
|
||||
/// Serializes this OrderItemRequestDto to a JSON map.
|
||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
@@ -2135,6 +2260,7 @@ abstract class $OrderItemRequestDtoCopyWith<$Res> {
|
||||
@JsonKey(name: "quantity") int? quantity,
|
||||
@JsonKey(name: "unit_price") int? unitPrice,
|
||||
@JsonKey(name: "notes") String? notes,
|
||||
@JsonKey(name: "weight") double? weight,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2158,6 +2284,7 @@ class _$OrderItemRequestDtoCopyWithImpl<$Res, $Val extends OrderItemRequestDto>
|
||||
Object? quantity = freezed,
|
||||
Object? unitPrice = freezed,
|
||||
Object? notes = freezed,
|
||||
Object? weight = freezed,
|
||||
}) {
|
||||
return _then(
|
||||
_value.copyWith(
|
||||
@@ -2181,6 +2308,10 @@ class _$OrderItemRequestDtoCopyWithImpl<$Res, $Val extends OrderItemRequestDto>
|
||||
? _value.notes
|
||||
: notes // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
weight: freezed == weight
|
||||
? _value.weight
|
||||
: weight // ignore: cast_nullable_to_non_nullable
|
||||
as double?,
|
||||
)
|
||||
as $Val,
|
||||
);
|
||||
@@ -2202,6 +2333,7 @@ abstract class _$$OrderItemRequestDtoImplCopyWith<$Res>
|
||||
@JsonKey(name: "quantity") int? quantity,
|
||||
@JsonKey(name: "unit_price") int? unitPrice,
|
||||
@JsonKey(name: "notes") String? notes,
|
||||
@JsonKey(name: "weight") double? weight,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2224,6 +2356,7 @@ class __$$OrderItemRequestDtoImplCopyWithImpl<$Res>
|
||||
Object? quantity = freezed,
|
||||
Object? unitPrice = freezed,
|
||||
Object? notes = freezed,
|
||||
Object? weight = freezed,
|
||||
}) {
|
||||
return _then(
|
||||
_$OrderItemRequestDtoImpl(
|
||||
@@ -2247,6 +2380,10 @@ class __$$OrderItemRequestDtoImplCopyWithImpl<$Res>
|
||||
? _value.notes
|
||||
: notes // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
weight: freezed == weight
|
||||
? _value.weight
|
||||
: weight // ignore: cast_nullable_to_non_nullable
|
||||
as double?,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -2261,6 +2398,7 @@ class _$OrderItemRequestDtoImpl extends _OrderItemRequestDto {
|
||||
@JsonKey(name: "quantity") this.quantity,
|
||||
@JsonKey(name: "unit_price") this.unitPrice,
|
||||
@JsonKey(name: "notes") this.notes,
|
||||
@JsonKey(name: "weight") this.weight,
|
||||
}) : super._();
|
||||
|
||||
factory _$OrderItemRequestDtoImpl.fromJson(Map<String, dynamic> json) =>
|
||||
@@ -2281,10 +2419,13 @@ class _$OrderItemRequestDtoImpl extends _OrderItemRequestDto {
|
||||
@override
|
||||
@JsonKey(name: "notes")
|
||||
final String? notes;
|
||||
@override
|
||||
@JsonKey(name: "weight")
|
||||
final double? weight;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'OrderItemRequestDto(productId: $productId, productVariantId: $productVariantId, quantity: $quantity, unitPrice: $unitPrice, notes: $notes)';
|
||||
return 'OrderItemRequestDto(productId: $productId, productVariantId: $productVariantId, quantity: $quantity, unitPrice: $unitPrice, notes: $notes, weight: $weight)';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -2300,7 +2441,8 @@ class _$OrderItemRequestDtoImpl extends _OrderItemRequestDto {
|
||||
other.quantity == quantity) &&
|
||||
(identical(other.unitPrice, unitPrice) ||
|
||||
other.unitPrice == unitPrice) &&
|
||||
(identical(other.notes, notes) || other.notes == notes));
|
||||
(identical(other.notes, notes) || other.notes == notes) &&
|
||||
(identical(other.weight, weight) || other.weight == weight));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@@ -2312,6 +2454,7 @@ class _$OrderItemRequestDtoImpl extends _OrderItemRequestDto {
|
||||
quantity,
|
||||
unitPrice,
|
||||
notes,
|
||||
weight,
|
||||
);
|
||||
|
||||
/// Create a copy of OrderItemRequestDto
|
||||
@@ -2338,6 +2481,7 @@ abstract class _OrderItemRequestDto extends OrderItemRequestDto {
|
||||
@JsonKey(name: "quantity") final int? quantity,
|
||||
@JsonKey(name: "unit_price") final int? unitPrice,
|
||||
@JsonKey(name: "notes") final String? notes,
|
||||
@JsonKey(name: "weight") final double? weight,
|
||||
}) = _$OrderItemRequestDtoImpl;
|
||||
const _OrderItemRequestDto._() : super._();
|
||||
|
||||
@@ -2359,6 +2503,9 @@ abstract class _OrderItemRequestDto extends OrderItemRequestDto {
|
||||
@override
|
||||
@JsonKey(name: "notes")
|
||||
String? get notes;
|
||||
@override
|
||||
@JsonKey(name: "weight")
|
||||
double? get weight;
|
||||
|
||||
/// Create a copy of OrderItemRequestDto
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@@ -4000,6 +4147,8 @@ mixin _$AddItemOrderRequestDto {
|
||||
int? get unitPrice => throw _privateConstructorUsedError;
|
||||
@JsonKey(name: 'notes')
|
||||
String? get notes => throw _privateConstructorUsedError;
|
||||
@JsonKey(name: 'weight')
|
||||
double? get weight => throw _privateConstructorUsedError;
|
||||
|
||||
/// Serializes this AddItemOrderRequestDto to a JSON map.
|
||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
@@ -4024,6 +4173,7 @@ abstract class $AddItemOrderRequestDtoCopyWith<$Res> {
|
||||
@JsonKey(name: 'quantity') int? quantity,
|
||||
@JsonKey(name: 'unit_price') int? unitPrice,
|
||||
@JsonKey(name: 'notes') String? notes,
|
||||
@JsonKey(name: 'weight') double? weight,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4050,6 +4200,7 @@ class _$AddItemOrderRequestDtoCopyWithImpl<
|
||||
Object? quantity = freezed,
|
||||
Object? unitPrice = freezed,
|
||||
Object? notes = freezed,
|
||||
Object? weight = freezed,
|
||||
}) {
|
||||
return _then(
|
||||
_value.copyWith(
|
||||
@@ -4073,6 +4224,10 @@ class _$AddItemOrderRequestDtoCopyWithImpl<
|
||||
? _value.notes
|
||||
: notes // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
weight: freezed == weight
|
||||
? _value.weight
|
||||
: weight // ignore: cast_nullable_to_non_nullable
|
||||
as double?,
|
||||
)
|
||||
as $Val,
|
||||
);
|
||||
@@ -4094,6 +4249,7 @@ abstract class _$$AddItemOrderRequestDtoImplCopyWith<$Res>
|
||||
@JsonKey(name: 'quantity') int? quantity,
|
||||
@JsonKey(name: 'unit_price') int? unitPrice,
|
||||
@JsonKey(name: 'notes') String? notes,
|
||||
@JsonKey(name: 'weight') double? weight,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4117,6 +4273,7 @@ class __$$AddItemOrderRequestDtoImplCopyWithImpl<$Res>
|
||||
Object? quantity = freezed,
|
||||
Object? unitPrice = freezed,
|
||||
Object? notes = freezed,
|
||||
Object? weight = freezed,
|
||||
}) {
|
||||
return _then(
|
||||
_$AddItemOrderRequestDtoImpl(
|
||||
@@ -4140,6 +4297,10 @@ class __$$AddItemOrderRequestDtoImplCopyWithImpl<$Res>
|
||||
? _value.notes
|
||||
: notes // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
weight: freezed == weight
|
||||
? _value.weight
|
||||
: weight // ignore: cast_nullable_to_non_nullable
|
||||
as double?,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -4154,6 +4315,7 @@ class _$AddItemOrderRequestDtoImpl extends _AddItemOrderRequestDto {
|
||||
@JsonKey(name: 'quantity') this.quantity,
|
||||
@JsonKey(name: 'unit_price') this.unitPrice,
|
||||
@JsonKey(name: 'notes') this.notes,
|
||||
@JsonKey(name: 'weight') this.weight,
|
||||
}) : super._();
|
||||
|
||||
factory _$AddItemOrderRequestDtoImpl.fromJson(Map<String, dynamic> json) =>
|
||||
@@ -4174,10 +4336,13 @@ class _$AddItemOrderRequestDtoImpl extends _AddItemOrderRequestDto {
|
||||
@override
|
||||
@JsonKey(name: 'notes')
|
||||
final String? notes;
|
||||
@override
|
||||
@JsonKey(name: 'weight')
|
||||
final double? weight;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'AddItemOrderRequestDto(productId: $productId, productvariantId: $productvariantId, quantity: $quantity, unitPrice: $unitPrice, notes: $notes)';
|
||||
return 'AddItemOrderRequestDto(productId: $productId, productvariantId: $productvariantId, quantity: $quantity, unitPrice: $unitPrice, notes: $notes, weight: $weight)';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -4193,7 +4358,8 @@ class _$AddItemOrderRequestDtoImpl extends _AddItemOrderRequestDto {
|
||||
other.quantity == quantity) &&
|
||||
(identical(other.unitPrice, unitPrice) ||
|
||||
other.unitPrice == unitPrice) &&
|
||||
(identical(other.notes, notes) || other.notes == notes));
|
||||
(identical(other.notes, notes) || other.notes == notes) &&
|
||||
(identical(other.weight, weight) || other.weight == weight));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@@ -4205,6 +4371,7 @@ class _$AddItemOrderRequestDtoImpl extends _AddItemOrderRequestDto {
|
||||
quantity,
|
||||
unitPrice,
|
||||
notes,
|
||||
weight,
|
||||
);
|
||||
|
||||
/// Create a copy of AddItemOrderRequestDto
|
||||
@@ -4232,6 +4399,7 @@ abstract class _AddItemOrderRequestDto extends AddItemOrderRequestDto {
|
||||
@JsonKey(name: 'quantity') final int? quantity,
|
||||
@JsonKey(name: 'unit_price') final int? unitPrice,
|
||||
@JsonKey(name: 'notes') final String? notes,
|
||||
@JsonKey(name: 'weight') final double? weight,
|
||||
}) = _$AddItemOrderRequestDtoImpl;
|
||||
const _AddItemOrderRequestDto._() : super._();
|
||||
|
||||
@@ -4253,6 +4421,9 @@ abstract class _AddItemOrderRequestDto extends AddItemOrderRequestDto {
|
||||
@override
|
||||
@JsonKey(name: 'notes')
|
||||
String? get notes;
|
||||
@override
|
||||
@JsonKey(name: 'weight')
|
||||
double? get weight;
|
||||
|
||||
/// Create a copy of AddItemOrderRequestDto
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
|
||||
@@ -109,9 +109,14 @@ _$OrderItemDtoImpl _$$OrderItemDtoImplFromJson(Map<String, dynamic> json) =>
|
||||
createdAt: json['created_at'] as String?,
|
||||
updatedAt: json['updated_at'] as String?,
|
||||
printerType: json['printer_type'] as String?,
|
||||
printToChecker: json['print_to_checker'] as bool?,
|
||||
paidQuantity: (json['paid_quantity'] as num?)?.toInt(),
|
||||
categoryId: json['category_id'] as String?,
|
||||
categoryName: json['category_name'] as String?,
|
||||
weight: (json['weight'] as num?)?.toDouble(),
|
||||
unitId: json['unit_id'] as String?,
|
||||
unitName: json['unit_name'] as String?,
|
||||
unitAbbreviation: json['unit_abbreviation'] as String?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$OrderItemDtoImplToJson(_$OrderItemDtoImpl instance) =>
|
||||
@@ -131,9 +136,14 @@ Map<String, dynamic> _$$OrderItemDtoImplToJson(_$OrderItemDtoImpl instance) =>
|
||||
'created_at': instance.createdAt,
|
||||
'updated_at': instance.updatedAt,
|
||||
'printer_type': instance.printerType,
|
||||
'print_to_checker': instance.printToChecker,
|
||||
'paid_quantity': instance.paidQuantity,
|
||||
'category_id': instance.categoryId,
|
||||
'category_name': instance.categoryName,
|
||||
'weight': instance.weight,
|
||||
'unit_id': instance.unitId,
|
||||
'unit_name': instance.unitName,
|
||||
'unit_abbreviation': instance.unitAbbreviation,
|
||||
};
|
||||
|
||||
_$OrderRequestDtoImpl _$$OrderRequestDtoImplFromJson(
|
||||
@@ -172,6 +182,7 @@ _$OrderItemRequestDtoImpl _$$OrderItemRequestDtoImplFromJson(
|
||||
quantity: (json['quantity'] as num?)?.toInt(),
|
||||
unitPrice: (json['unit_price'] as num?)?.toInt(),
|
||||
notes: json['notes'] as String?,
|
||||
weight: (json['weight'] as num?)?.toDouble(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$OrderItemRequestDtoImplToJson(
|
||||
@@ -182,6 +193,7 @@ Map<String, dynamic> _$$OrderItemRequestDtoImplToJson(
|
||||
'quantity': instance.quantity,
|
||||
'unit_price': instance.unitPrice,
|
||||
'notes': instance.notes,
|
||||
'weight': instance.weight,
|
||||
};
|
||||
|
||||
_$PaymentRequestDtoImpl _$$PaymentRequestDtoImplFromJson(
|
||||
@@ -315,6 +327,7 @@ _$AddItemOrderRequestDtoImpl _$$AddItemOrderRequestDtoImplFromJson(
|
||||
quantity: (json['quantity'] as num?)?.toInt(),
|
||||
unitPrice: (json['unit_price'] as num?)?.toInt(),
|
||||
notes: json['notes'] as String?,
|
||||
weight: (json['weight'] as num?)?.toDouble(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$AddItemOrderRequestDtoImplToJson(
|
||||
@@ -325,4 +338,5 @@ Map<String, dynamic> _$$AddItemOrderRequestDtoImplToJson(
|
||||
'quantity': instance.quantity,
|
||||
'unit_price': instance.unitPrice,
|
||||
'notes': instance.notes,
|
||||
'weight': instance.weight,
|
||||
};
|
||||
|
||||
@@ -110,6 +110,7 @@ class OrderRepository implements IOrderRepository {
|
||||
Future<Either<OrderFailure, Order>> addItemOrder({
|
||||
required String id,
|
||||
required List<AddItemOrderRequest> request,
|
||||
String? idempotencyKey,
|
||||
}) async {
|
||||
try {
|
||||
final result = await _dataProvider.addItemOrder(
|
||||
@@ -117,6 +118,7 @@ class OrderRepository implements IOrderRepository {
|
||||
request: request
|
||||
.map((e) => AddItemOrderRequestDto.fromDomain(e))
|
||||
.toList(),
|
||||
idempotencyKey: idempotencyKey,
|
||||
);
|
||||
|
||||
if (result.hasError) {
|
||||
@@ -134,10 +136,12 @@ class OrderRepository implements IOrderRepository {
|
||||
@override
|
||||
Future<Either<OrderFailure, Payment>> createPayment({
|
||||
required PaymentRequest request,
|
||||
String? idempotencyKey,
|
||||
}) async {
|
||||
try {
|
||||
final result = await _dataProvider.storePayment(
|
||||
PaymentRequestDto.fromDomain(request),
|
||||
idempotencyKey: idempotencyKey,
|
||||
);
|
||||
|
||||
if (result.hasError) {
|
||||
@@ -158,6 +162,7 @@ class OrderRepository implements IOrderRepository {
|
||||
required String reason,
|
||||
String type = "ITEM",
|
||||
required List<OrderItem> orderItems,
|
||||
String? idempotencyKey,
|
||||
}) async {
|
||||
try {
|
||||
final result = await _dataProvider.voidOrder(
|
||||
@@ -165,6 +170,7 @@ class OrderRepository implements IOrderRepository {
|
||||
reason: reason,
|
||||
type: type,
|
||||
orderItems: orderItems.map((e) => OrderItemDto.fromDomain(e)).toList(),
|
||||
idempotencyKey: idempotencyKey,
|
||||
);
|
||||
|
||||
if (result.hasError) {
|
||||
@@ -180,11 +186,13 @@ class OrderRepository implements IOrderRepository {
|
||||
|
||||
@override
|
||||
Future<Either<OrderFailure, Payment>> createSplitBill(
|
||||
PaymentSplitBillRequest request,
|
||||
) async {
|
||||
PaymentSplitBillRequest request, {
|
||||
String? idempotencyKey,
|
||||
}) async {
|
||||
try {
|
||||
final result = await _dataProvider.createSplitBill(
|
||||
PaymentSplitBillRequestDto.fromDomain(request),
|
||||
idempotencyKey: idempotencyKey,
|
||||
);
|
||||
|
||||
if (result.hasError) {
|
||||
@@ -204,12 +212,14 @@ class OrderRepository implements IOrderRepository {
|
||||
required String id,
|
||||
required String reason,
|
||||
required int refundAmount,
|
||||
String? idempotencyKey,
|
||||
}) async {
|
||||
try {
|
||||
final result = await _dataProvider.refundPayment(
|
||||
id: id,
|
||||
reason: reason,
|
||||
refundAmount: refundAmount,
|
||||
idempotencyKey: idempotencyKey,
|
||||
);
|
||||
if (result.hasError) {
|
||||
return left(result.error!);
|
||||
|
||||
@@ -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
@@ -24,6 +24,25 @@ class ListProductDto with _$ListProductDto {
|
||||
);
|
||||
}
|
||||
|
||||
/// Satuan yang menempel pada produk.
|
||||
///
|
||||
/// Backend hanya dijamin mengirim `unit_id`; sebagian response ikut menyertakan
|
||||
/// objek `unit` lengkap. Kalau ada, dipakai langsung supaya POS tidak perlu
|
||||
/// menunggu master satuan termuat.
|
||||
@freezed
|
||||
class ProductUnitDto with _$ProductUnitDto {
|
||||
const ProductUnitDto._();
|
||||
|
||||
const factory ProductUnitDto({
|
||||
@JsonKey(name: "id") String? id,
|
||||
@JsonKey(name: "name") String? name,
|
||||
@JsonKey(name: "abbreviation") String? abbreviation,
|
||||
}) = _ProductUnitDto;
|
||||
|
||||
factory ProductUnitDto.fromJson(Map<String, dynamic> json) =>
|
||||
_$ProductUnitDtoFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
class ProductDto with _$ProductDto {
|
||||
const ProductDto._();
|
||||
@@ -40,16 +59,34 @@ class ProductDto with _$ProductDto {
|
||||
@JsonKey(name: "business_type") String? businessType,
|
||||
@JsonKey(name: "image_url") String? imageUrl,
|
||||
@JsonKey(name: "printer_type") String? printerType,
|
||||
@JsonKey(name: "print_to_checker") bool? printToChecker,
|
||||
@JsonKey(name: "metadata") Map<String, dynamic>? metadata,
|
||||
@JsonKey(name: "is_active") bool? isActive,
|
||||
@JsonKey(name: "created_at") String? createdAt,
|
||||
@JsonKey(name: "updated_at") String? updatedAt,
|
||||
@JsonKey(name: "variants") List<ProductVariantDto>? variants,
|
||||
@JsonKey(name: "sell_by") String? sellBy,
|
||||
@JsonKey(name: "unit_id") String? unitId,
|
||||
@JsonKey(name: "unit_name") String? unitName,
|
||||
@JsonKey(name: "unit_abbreviation") String? unitAbbreviation,
|
||||
@JsonKey(name: "unit") ProductUnitDto? unit,
|
||||
}) = _ProductDto;
|
||||
|
||||
factory ProductDto.fromJson(Map<String, dynamic> json) =>
|
||||
_$ProductDtoFromJson(json);
|
||||
|
||||
String get _resolvedUnitId =>
|
||||
(unitId?.isNotEmpty == true ? unitId : unit?.id) ?? '';
|
||||
|
||||
String get _resolvedUnitName =>
|
||||
(unitName?.isNotEmpty == true ? unitName : unit?.name) ?? '';
|
||||
|
||||
String get _resolvedUnitAbbreviation =>
|
||||
(unitAbbreviation?.isNotEmpty == true
|
||||
? unitAbbreviation
|
||||
: unit?.abbreviation) ??
|
||||
'';
|
||||
|
||||
/// Mapping ke domain
|
||||
Product toDomain() => Product(
|
||||
id: id ?? '',
|
||||
@@ -63,11 +100,16 @@ class ProductDto with _$ProductDto {
|
||||
businessType: businessType ?? '',
|
||||
imageUrl: imageUrl ?? '',
|
||||
printerType: printerType ?? '',
|
||||
printToChecker: printToChecker ?? false,
|
||||
metadata: metadata ?? {},
|
||||
isActive: isActive ?? false,
|
||||
createdAt: createdAt ?? '',
|
||||
updatedAt: updatedAt ?? '',
|
||||
variants: variants?.map((v) => v.toDomain()).toList() ?? [],
|
||||
sellBy: SellBy.normalize(sellBy),
|
||||
unitId: _resolvedUnitId,
|
||||
unitName: _resolvedUnitName,
|
||||
unitAbbreviation: _resolvedUnitAbbreviation,
|
||||
);
|
||||
|
||||
Map<String, dynamic> toMap() => {
|
||||
@@ -82,10 +124,15 @@ class ProductDto with _$ProductDto {
|
||||
'business_type': businessType,
|
||||
'image_url': imageUrl,
|
||||
'printer_type': printerType,
|
||||
'print_to_checker': printToChecker == true ? 1 : 0,
|
||||
'metadata': metadata != null ? jsonEncode(metadata) : null,
|
||||
'is_active': isActive == true ? 1 : 0,
|
||||
'created_at': createdAt,
|
||||
'updated_at': updatedAt,
|
||||
'sell_by': SellBy.normalize(sellBy),
|
||||
'unit_id': _resolvedUnitId,
|
||||
'unit_name': _resolvedUnitName,
|
||||
'unit_abbreviation': _resolvedUnitAbbreviation,
|
||||
};
|
||||
|
||||
factory ProductDto.fromMap(
|
||||
@@ -103,6 +150,9 @@ class ProductDto with _$ProductDto {
|
||||
businessType: map['business_type'] as String?,
|
||||
imageUrl: map['image_url'] as String?,
|
||||
printerType: map['printer_type'] as String?,
|
||||
printToChecker: map['print_to_checker'] != null
|
||||
? (map['print_to_checker'] as int) == 1
|
||||
: null,
|
||||
metadata: map['metadata'] != null
|
||||
? jsonDecode(map['metadata'] as String) as Map<String, dynamic>
|
||||
: null,
|
||||
@@ -111,6 +161,10 @@ class ProductDto with _$ProductDto {
|
||||
createdAt: map['created_at'] as String?,
|
||||
updatedAt: map['updated_at'] as String?,
|
||||
variants: variants,
|
||||
sellBy: map['sell_by'] as String?,
|
||||
unitId: map['unit_id'] as String?,
|
||||
unitName: map['unit_name'] as String?,
|
||||
unitAbbreviation: map['unit_abbreviation'] as String?,
|
||||
);
|
||||
|
||||
factory ProductDto.fromDomain(Product product) => ProductDto(
|
||||
@@ -125,6 +179,7 @@ class ProductDto with _$ProductDto {
|
||||
businessType: product.businessType,
|
||||
imageUrl: product.imageUrl,
|
||||
printerType: product.printerType,
|
||||
printToChecker: product.printToChecker,
|
||||
metadata: product.metadata,
|
||||
isActive: product.isActive,
|
||||
createdAt: product.createdAt,
|
||||
@@ -132,6 +187,10 @@ class ProductDto with _$ProductDto {
|
||||
variants: product.variants
|
||||
.map((v) => ProductVariantDto.fromDomain(v))
|
||||
.toList(),
|
||||
sellBy: product.sellBy,
|
||||
unitId: product.unitId,
|
||||
unitName: product.unitName,
|
||||
unitAbbreviation: product.unitAbbreviation,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'dart:convert';
|
||||
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
import '../../common/types/sell_by.dart';
|
||||
import '../../domain/product/product.dart';
|
||||
|
||||
part 'product_dtos.freezed.dart';
|
||||
|
||||
@@ -293,6 +293,223 @@ abstract class _ListProductDto extends ListProductDto {
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
ProductUnitDto _$ProductUnitDtoFromJson(Map<String, dynamic> json) {
|
||||
return _ProductUnitDto.fromJson(json);
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
mixin _$ProductUnitDto {
|
||||
@JsonKey(name: "id")
|
||||
String? get id => throw _privateConstructorUsedError;
|
||||
@JsonKey(name: "name")
|
||||
String? get name => throw _privateConstructorUsedError;
|
||||
@JsonKey(name: "abbreviation")
|
||||
String? get abbreviation => throw _privateConstructorUsedError;
|
||||
|
||||
/// Serializes this ProductUnitDto to a JSON map.
|
||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
|
||||
/// Create a copy of ProductUnitDto
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
$ProductUnitDtoCopyWith<ProductUnitDto> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $ProductUnitDtoCopyWith<$Res> {
|
||||
factory $ProductUnitDtoCopyWith(
|
||||
ProductUnitDto value,
|
||||
$Res Function(ProductUnitDto) then,
|
||||
) = _$ProductUnitDtoCopyWithImpl<$Res, ProductUnitDto>;
|
||||
@useResult
|
||||
$Res call({
|
||||
@JsonKey(name: "id") String? id,
|
||||
@JsonKey(name: "name") String? name,
|
||||
@JsonKey(name: "abbreviation") String? abbreviation,
|
||||
});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$ProductUnitDtoCopyWithImpl<$Res, $Val extends ProductUnitDto>
|
||||
implements $ProductUnitDtoCopyWith<$Res> {
|
||||
_$ProductUnitDtoCopyWithImpl(this._value, this._then);
|
||||
|
||||
// ignore: unused_field
|
||||
final $Val _value;
|
||||
// ignore: unused_field
|
||||
final $Res Function($Val) _then;
|
||||
|
||||
/// Create a copy of ProductUnitDto
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? id = freezed,
|
||||
Object? name = freezed,
|
||||
Object? abbreviation = freezed,
|
||||
}) {
|
||||
return _then(
|
||||
_value.copyWith(
|
||||
id: freezed == id
|
||||
? _value.id
|
||||
: id // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
name: freezed == name
|
||||
? _value.name
|
||||
: name // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
abbreviation: freezed == abbreviation
|
||||
? _value.abbreviation
|
||||
: abbreviation // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
)
|
||||
as $Val,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class _$$ProductUnitDtoImplCopyWith<$Res>
|
||||
implements $ProductUnitDtoCopyWith<$Res> {
|
||||
factory _$$ProductUnitDtoImplCopyWith(
|
||||
_$ProductUnitDtoImpl value,
|
||||
$Res Function(_$ProductUnitDtoImpl) then,
|
||||
) = __$$ProductUnitDtoImplCopyWithImpl<$Res>;
|
||||
@override
|
||||
@useResult
|
||||
$Res call({
|
||||
@JsonKey(name: "id") String? id,
|
||||
@JsonKey(name: "name") String? name,
|
||||
@JsonKey(name: "abbreviation") String? abbreviation,
|
||||
});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$$ProductUnitDtoImplCopyWithImpl<$Res>
|
||||
extends _$ProductUnitDtoCopyWithImpl<$Res, _$ProductUnitDtoImpl>
|
||||
implements _$$ProductUnitDtoImplCopyWith<$Res> {
|
||||
__$$ProductUnitDtoImplCopyWithImpl(
|
||||
_$ProductUnitDtoImpl _value,
|
||||
$Res Function(_$ProductUnitDtoImpl) _then,
|
||||
) : super(_value, _then);
|
||||
|
||||
/// Create a copy of ProductUnitDto
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? id = freezed,
|
||||
Object? name = freezed,
|
||||
Object? abbreviation = freezed,
|
||||
}) {
|
||||
return _then(
|
||||
_$ProductUnitDtoImpl(
|
||||
id: freezed == id
|
||||
? _value.id
|
||||
: id // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
name: freezed == name
|
||||
? _value.name
|
||||
: name // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
abbreviation: freezed == abbreviation
|
||||
? _value.abbreviation
|
||||
: abbreviation // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
class _$ProductUnitDtoImpl extends _ProductUnitDto {
|
||||
const _$ProductUnitDtoImpl({
|
||||
@JsonKey(name: "id") this.id,
|
||||
@JsonKey(name: "name") this.name,
|
||||
@JsonKey(name: "abbreviation") this.abbreviation,
|
||||
}) : super._();
|
||||
|
||||
factory _$ProductUnitDtoImpl.fromJson(Map<String, dynamic> json) =>
|
||||
_$$ProductUnitDtoImplFromJson(json);
|
||||
|
||||
@override
|
||||
@JsonKey(name: "id")
|
||||
final String? id;
|
||||
@override
|
||||
@JsonKey(name: "name")
|
||||
final String? name;
|
||||
@override
|
||||
@JsonKey(name: "abbreviation")
|
||||
final String? abbreviation;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ProductUnitDto(id: $id, name: $name, abbreviation: $abbreviation)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$ProductUnitDtoImpl &&
|
||||
(identical(other.id, id) || other.id == id) &&
|
||||
(identical(other.name, name) || other.name == name) &&
|
||||
(identical(other.abbreviation, abbreviation) ||
|
||||
other.abbreviation == abbreviation));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType, id, name, abbreviation);
|
||||
|
||||
/// Create a copy of ProductUnitDto
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
_$$ProductUnitDtoImplCopyWith<_$ProductUnitDtoImpl> get copyWith =>
|
||||
__$$ProductUnitDtoImplCopyWithImpl<_$ProductUnitDtoImpl>(
|
||||
this,
|
||||
_$identity,
|
||||
);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$$ProductUnitDtoImplToJson(this);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _ProductUnitDto extends ProductUnitDto {
|
||||
const factory _ProductUnitDto({
|
||||
@JsonKey(name: "id") final String? id,
|
||||
@JsonKey(name: "name") final String? name,
|
||||
@JsonKey(name: "abbreviation") final String? abbreviation,
|
||||
}) = _$ProductUnitDtoImpl;
|
||||
const _ProductUnitDto._() : super._();
|
||||
|
||||
factory _ProductUnitDto.fromJson(Map<String, dynamic> json) =
|
||||
_$ProductUnitDtoImpl.fromJson;
|
||||
|
||||
@override
|
||||
@JsonKey(name: "id")
|
||||
String? get id;
|
||||
@override
|
||||
@JsonKey(name: "name")
|
||||
String? get name;
|
||||
@override
|
||||
@JsonKey(name: "abbreviation")
|
||||
String? get abbreviation;
|
||||
|
||||
/// Create a copy of ProductUnitDto
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
_$$ProductUnitDtoImplCopyWith<_$ProductUnitDtoImpl> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
ProductDto _$ProductDtoFromJson(Map<String, dynamic> json) {
|
||||
return _ProductDto.fromJson(json);
|
||||
}
|
||||
@@ -321,6 +538,8 @@ mixin _$ProductDto {
|
||||
String? get imageUrl => throw _privateConstructorUsedError;
|
||||
@JsonKey(name: "printer_type")
|
||||
String? get printerType => throw _privateConstructorUsedError;
|
||||
@JsonKey(name: "print_to_checker")
|
||||
bool? get printToChecker => throw _privateConstructorUsedError;
|
||||
@JsonKey(name: "metadata")
|
||||
Map<String, dynamic>? get metadata => throw _privateConstructorUsedError;
|
||||
@JsonKey(name: "is_active")
|
||||
@@ -331,6 +550,16 @@ mixin _$ProductDto {
|
||||
String? get updatedAt => throw _privateConstructorUsedError;
|
||||
@JsonKey(name: "variants")
|
||||
List<ProductVariantDto>? get variants => throw _privateConstructorUsedError;
|
||||
@JsonKey(name: "sell_by")
|
||||
String? get sellBy => throw _privateConstructorUsedError;
|
||||
@JsonKey(name: "unit_id")
|
||||
String? get unitId => throw _privateConstructorUsedError;
|
||||
@JsonKey(name: "unit_name")
|
||||
String? get unitName => throw _privateConstructorUsedError;
|
||||
@JsonKey(name: "unit_abbreviation")
|
||||
String? get unitAbbreviation => throw _privateConstructorUsedError;
|
||||
@JsonKey(name: "unit")
|
||||
ProductUnitDto? get unit => throw _privateConstructorUsedError;
|
||||
|
||||
/// Serializes this ProductDto to a JSON map.
|
||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
@@ -361,12 +590,20 @@ abstract class $ProductDtoCopyWith<$Res> {
|
||||
@JsonKey(name: "business_type") String? businessType,
|
||||
@JsonKey(name: "image_url") String? imageUrl,
|
||||
@JsonKey(name: "printer_type") String? printerType,
|
||||
@JsonKey(name: "print_to_checker") bool? printToChecker,
|
||||
@JsonKey(name: "metadata") Map<String, dynamic>? metadata,
|
||||
@JsonKey(name: "is_active") bool? isActive,
|
||||
@JsonKey(name: "created_at") String? createdAt,
|
||||
@JsonKey(name: "updated_at") String? updatedAt,
|
||||
@JsonKey(name: "variants") List<ProductVariantDto>? variants,
|
||||
@JsonKey(name: "sell_by") String? sellBy,
|
||||
@JsonKey(name: "unit_id") String? unitId,
|
||||
@JsonKey(name: "unit_name") String? unitName,
|
||||
@JsonKey(name: "unit_abbreviation") String? unitAbbreviation,
|
||||
@JsonKey(name: "unit") ProductUnitDto? unit,
|
||||
});
|
||||
|
||||
$ProductUnitDtoCopyWith<$Res>? get unit;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@@ -395,11 +632,17 @@ class _$ProductDtoCopyWithImpl<$Res, $Val extends ProductDto>
|
||||
Object? businessType = freezed,
|
||||
Object? imageUrl = freezed,
|
||||
Object? printerType = freezed,
|
||||
Object? printToChecker = freezed,
|
||||
Object? metadata = freezed,
|
||||
Object? isActive = freezed,
|
||||
Object? createdAt = freezed,
|
||||
Object? updatedAt = freezed,
|
||||
Object? variants = freezed,
|
||||
Object? sellBy = freezed,
|
||||
Object? unitId = freezed,
|
||||
Object? unitName = freezed,
|
||||
Object? unitAbbreviation = freezed,
|
||||
Object? unit = freezed,
|
||||
}) {
|
||||
return _then(
|
||||
_value.copyWith(
|
||||
@@ -447,6 +690,10 @@ class _$ProductDtoCopyWithImpl<$Res, $Val extends ProductDto>
|
||||
? _value.printerType
|
||||
: printerType // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
printToChecker: freezed == printToChecker
|
||||
? _value.printToChecker
|
||||
: printToChecker // ignore: cast_nullable_to_non_nullable
|
||||
as bool?,
|
||||
metadata: freezed == metadata
|
||||
? _value.metadata
|
||||
: metadata // ignore: cast_nullable_to_non_nullable
|
||||
@@ -467,10 +714,44 @@ class _$ProductDtoCopyWithImpl<$Res, $Val extends ProductDto>
|
||||
? _value.variants
|
||||
: variants // ignore: cast_nullable_to_non_nullable
|
||||
as List<ProductVariantDto>?,
|
||||
sellBy: freezed == sellBy
|
||||
? _value.sellBy
|
||||
: sellBy // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
unitId: freezed == unitId
|
||||
? _value.unitId
|
||||
: unitId // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
unitName: freezed == unitName
|
||||
? _value.unitName
|
||||
: unitName // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
unitAbbreviation: freezed == unitAbbreviation
|
||||
? _value.unitAbbreviation
|
||||
: unitAbbreviation // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
unit: freezed == unit
|
||||
? _value.unit
|
||||
: unit // ignore: cast_nullable_to_non_nullable
|
||||
as ProductUnitDto?,
|
||||
)
|
||||
as $Val,
|
||||
);
|
||||
}
|
||||
|
||||
/// Create a copy of ProductDto
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$ProductUnitDtoCopyWith<$Res>? get unit {
|
||||
if (_value.unit == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $ProductUnitDtoCopyWith<$Res>(_value.unit!, (value) {
|
||||
return _then(_value.copyWith(unit: value) as $Val);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@@ -494,12 +775,21 @@ abstract class _$$ProductDtoImplCopyWith<$Res>
|
||||
@JsonKey(name: "business_type") String? businessType,
|
||||
@JsonKey(name: "image_url") String? imageUrl,
|
||||
@JsonKey(name: "printer_type") String? printerType,
|
||||
@JsonKey(name: "print_to_checker") bool? printToChecker,
|
||||
@JsonKey(name: "metadata") Map<String, dynamic>? metadata,
|
||||
@JsonKey(name: "is_active") bool? isActive,
|
||||
@JsonKey(name: "created_at") String? createdAt,
|
||||
@JsonKey(name: "updated_at") String? updatedAt,
|
||||
@JsonKey(name: "variants") List<ProductVariantDto>? variants,
|
||||
@JsonKey(name: "sell_by") String? sellBy,
|
||||
@JsonKey(name: "unit_id") String? unitId,
|
||||
@JsonKey(name: "unit_name") String? unitName,
|
||||
@JsonKey(name: "unit_abbreviation") String? unitAbbreviation,
|
||||
@JsonKey(name: "unit") ProductUnitDto? unit,
|
||||
});
|
||||
|
||||
@override
|
||||
$ProductUnitDtoCopyWith<$Res>? get unit;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@@ -527,11 +817,17 @@ class __$$ProductDtoImplCopyWithImpl<$Res>
|
||||
Object? businessType = freezed,
|
||||
Object? imageUrl = freezed,
|
||||
Object? printerType = freezed,
|
||||
Object? printToChecker = freezed,
|
||||
Object? metadata = freezed,
|
||||
Object? isActive = freezed,
|
||||
Object? createdAt = freezed,
|
||||
Object? updatedAt = freezed,
|
||||
Object? variants = freezed,
|
||||
Object? sellBy = freezed,
|
||||
Object? unitId = freezed,
|
||||
Object? unitName = freezed,
|
||||
Object? unitAbbreviation = freezed,
|
||||
Object? unit = freezed,
|
||||
}) {
|
||||
return _then(
|
||||
_$ProductDtoImpl(
|
||||
@@ -579,6 +875,10 @@ class __$$ProductDtoImplCopyWithImpl<$Res>
|
||||
? _value.printerType
|
||||
: printerType // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
printToChecker: freezed == printToChecker
|
||||
? _value.printToChecker
|
||||
: printToChecker // ignore: cast_nullable_to_non_nullable
|
||||
as bool?,
|
||||
metadata: freezed == metadata
|
||||
? _value._metadata
|
||||
: metadata // ignore: cast_nullable_to_non_nullable
|
||||
@@ -599,6 +899,26 @@ class __$$ProductDtoImplCopyWithImpl<$Res>
|
||||
? _value._variants
|
||||
: variants // ignore: cast_nullable_to_non_nullable
|
||||
as List<ProductVariantDto>?,
|
||||
sellBy: freezed == sellBy
|
||||
? _value.sellBy
|
||||
: sellBy // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
unitId: freezed == unitId
|
||||
? _value.unitId
|
||||
: unitId // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
unitName: freezed == unitName
|
||||
? _value.unitName
|
||||
: unitName // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
unitAbbreviation: freezed == unitAbbreviation
|
||||
? _value.unitAbbreviation
|
||||
: unitAbbreviation // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
unit: freezed == unit
|
||||
? _value.unit
|
||||
: unit // ignore: cast_nullable_to_non_nullable
|
||||
as ProductUnitDto?,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -619,11 +939,17 @@ class _$ProductDtoImpl extends _ProductDto {
|
||||
@JsonKey(name: "business_type") this.businessType,
|
||||
@JsonKey(name: "image_url") this.imageUrl,
|
||||
@JsonKey(name: "printer_type") this.printerType,
|
||||
@JsonKey(name: "print_to_checker") this.printToChecker,
|
||||
@JsonKey(name: "metadata") final Map<String, dynamic>? metadata,
|
||||
@JsonKey(name: "is_active") this.isActive,
|
||||
@JsonKey(name: "created_at") this.createdAt,
|
||||
@JsonKey(name: "updated_at") this.updatedAt,
|
||||
@JsonKey(name: "variants") final List<ProductVariantDto>? variants,
|
||||
@JsonKey(name: "sell_by") this.sellBy,
|
||||
@JsonKey(name: "unit_id") this.unitId,
|
||||
@JsonKey(name: "unit_name") this.unitName,
|
||||
@JsonKey(name: "unit_abbreviation") this.unitAbbreviation,
|
||||
@JsonKey(name: "unit") this.unit,
|
||||
}) : _metadata = metadata,
|
||||
_variants = variants,
|
||||
super._();
|
||||
@@ -664,6 +990,9 @@ class _$ProductDtoImpl extends _ProductDto {
|
||||
@override
|
||||
@JsonKey(name: "printer_type")
|
||||
final String? printerType;
|
||||
@override
|
||||
@JsonKey(name: "print_to_checker")
|
||||
final bool? printToChecker;
|
||||
final Map<String, dynamic>? _metadata;
|
||||
@override
|
||||
@JsonKey(name: "metadata")
|
||||
@@ -695,9 +1024,25 @@ class _$ProductDtoImpl extends _ProductDto {
|
||||
return EqualUnmodifiableListView(value);
|
||||
}
|
||||
|
||||
@override
|
||||
@JsonKey(name: "sell_by")
|
||||
final String? sellBy;
|
||||
@override
|
||||
@JsonKey(name: "unit_id")
|
||||
final String? unitId;
|
||||
@override
|
||||
@JsonKey(name: "unit_name")
|
||||
final String? unitName;
|
||||
@override
|
||||
@JsonKey(name: "unit_abbreviation")
|
||||
final String? unitAbbreviation;
|
||||
@override
|
||||
@JsonKey(name: "unit")
|
||||
final ProductUnitDto? unit;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ProductDto(id: $id, organizationId: $organizationId, categoryId: $categoryId, sku: $sku, name: $name, description: $description, price: $price, cost: $cost, businessType: $businessType, imageUrl: $imageUrl, printerType: $printerType, metadata: $metadata, isActive: $isActive, createdAt: $createdAt, updatedAt: $updatedAt, variants: $variants)';
|
||||
return 'ProductDto(id: $id, organizationId: $organizationId, categoryId: $categoryId, sku: $sku, name: $name, description: $description, price: $price, cost: $cost, businessType: $businessType, imageUrl: $imageUrl, printerType: $printerType, printToChecker: $printToChecker, metadata: $metadata, isActive: $isActive, createdAt: $createdAt, updatedAt: $updatedAt, variants: $variants, sellBy: $sellBy, unitId: $unitId, unitName: $unitName, unitAbbreviation: $unitAbbreviation, unit: $unit)';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -722,6 +1067,8 @@ class _$ProductDtoImpl extends _ProductDto {
|
||||
other.imageUrl == imageUrl) &&
|
||||
(identical(other.printerType, printerType) ||
|
||||
other.printerType == printerType) &&
|
||||
(identical(other.printToChecker, printToChecker) ||
|
||||
other.printToChecker == printToChecker) &&
|
||||
const DeepCollectionEquality().equals(other._metadata, _metadata) &&
|
||||
(identical(other.isActive, isActive) ||
|
||||
other.isActive == isActive) &&
|
||||
@@ -729,12 +1076,19 @@ class _$ProductDtoImpl extends _ProductDto {
|
||||
other.createdAt == createdAt) &&
|
||||
(identical(other.updatedAt, updatedAt) ||
|
||||
other.updatedAt == updatedAt) &&
|
||||
const DeepCollectionEquality().equals(other._variants, _variants));
|
||||
const DeepCollectionEquality().equals(other._variants, _variants) &&
|
||||
(identical(other.sellBy, sellBy) || other.sellBy == sellBy) &&
|
||||
(identical(other.unitId, unitId) || other.unitId == unitId) &&
|
||||
(identical(other.unitName, unitName) ||
|
||||
other.unitName == unitName) &&
|
||||
(identical(other.unitAbbreviation, unitAbbreviation) ||
|
||||
other.unitAbbreviation == unitAbbreviation) &&
|
||||
(identical(other.unit, unit) || other.unit == unit));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
int get hashCode => Object.hashAll([
|
||||
runtimeType,
|
||||
id,
|
||||
organizationId,
|
||||
@@ -747,12 +1101,18 @@ class _$ProductDtoImpl extends _ProductDto {
|
||||
businessType,
|
||||
imageUrl,
|
||||
printerType,
|
||||
printToChecker,
|
||||
const DeepCollectionEquality().hash(_metadata),
|
||||
isActive,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
const DeepCollectionEquality().hash(_variants),
|
||||
);
|
||||
sellBy,
|
||||
unitId,
|
||||
unitName,
|
||||
unitAbbreviation,
|
||||
unit,
|
||||
]);
|
||||
|
||||
/// Create a copy of ProductDto
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@@ -781,11 +1141,17 @@ abstract class _ProductDto extends ProductDto {
|
||||
@JsonKey(name: "business_type") final String? businessType,
|
||||
@JsonKey(name: "image_url") final String? imageUrl,
|
||||
@JsonKey(name: "printer_type") final String? printerType,
|
||||
@JsonKey(name: "print_to_checker") final bool? printToChecker,
|
||||
@JsonKey(name: "metadata") final Map<String, dynamic>? metadata,
|
||||
@JsonKey(name: "is_active") final bool? isActive,
|
||||
@JsonKey(name: "created_at") final String? createdAt,
|
||||
@JsonKey(name: "updated_at") final String? updatedAt,
|
||||
@JsonKey(name: "variants") final List<ProductVariantDto>? variants,
|
||||
@JsonKey(name: "sell_by") final String? sellBy,
|
||||
@JsonKey(name: "unit_id") final String? unitId,
|
||||
@JsonKey(name: "unit_name") final String? unitName,
|
||||
@JsonKey(name: "unit_abbreviation") final String? unitAbbreviation,
|
||||
@JsonKey(name: "unit") final ProductUnitDto? unit,
|
||||
}) = _$ProductDtoImpl;
|
||||
const _ProductDto._() : super._();
|
||||
|
||||
@@ -826,6 +1192,9 @@ abstract class _ProductDto extends ProductDto {
|
||||
@JsonKey(name: "printer_type")
|
||||
String? get printerType;
|
||||
@override
|
||||
@JsonKey(name: "print_to_checker")
|
||||
bool? get printToChecker;
|
||||
@override
|
||||
@JsonKey(name: "metadata")
|
||||
Map<String, dynamic>? get metadata;
|
||||
@override
|
||||
@@ -840,6 +1209,21 @@ abstract class _ProductDto extends ProductDto {
|
||||
@override
|
||||
@JsonKey(name: "variants")
|
||||
List<ProductVariantDto>? get variants;
|
||||
@override
|
||||
@JsonKey(name: "sell_by")
|
||||
String? get sellBy;
|
||||
@override
|
||||
@JsonKey(name: "unit_id")
|
||||
String? get unitId;
|
||||
@override
|
||||
@JsonKey(name: "unit_name")
|
||||
String? get unitName;
|
||||
@override
|
||||
@JsonKey(name: "unit_abbreviation")
|
||||
String? get unitAbbreviation;
|
||||
@override
|
||||
@JsonKey(name: "unit")
|
||||
ProductUnitDto? get unit;
|
||||
|
||||
/// Create a copy of ProductDto
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
|
||||
@@ -27,6 +27,21 @@ Map<String, dynamic> _$$ListProductDtoImplToJson(
|
||||
'total_pages': instance.totalPages,
|
||||
};
|
||||
|
||||
_$ProductUnitDtoImpl _$$ProductUnitDtoImplFromJson(Map<String, dynamic> json) =>
|
||||
_$ProductUnitDtoImpl(
|
||||
id: json['id'] as String?,
|
||||
name: json['name'] as String?,
|
||||
abbreviation: json['abbreviation'] as String?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$ProductUnitDtoImplToJson(
|
||||
_$ProductUnitDtoImpl instance,
|
||||
) => <String, dynamic>{
|
||||
'id': instance.id,
|
||||
'name': instance.name,
|
||||
'abbreviation': instance.abbreviation,
|
||||
};
|
||||
|
||||
_$ProductDtoImpl _$$ProductDtoImplFromJson(Map<String, dynamic> json) =>
|
||||
_$ProductDtoImpl(
|
||||
id: json['id'] as String?,
|
||||
@@ -40,6 +55,7 @@ _$ProductDtoImpl _$$ProductDtoImplFromJson(Map<String, dynamic> json) =>
|
||||
businessType: json['business_type'] as String?,
|
||||
imageUrl: json['image_url'] as String?,
|
||||
printerType: json['printer_type'] as String?,
|
||||
printToChecker: json['print_to_checker'] as bool?,
|
||||
metadata: json['metadata'] as Map<String, dynamic>?,
|
||||
isActive: json['is_active'] as bool?,
|
||||
createdAt: json['created_at'] as String?,
|
||||
@@ -47,6 +63,13 @@ _$ProductDtoImpl _$$ProductDtoImplFromJson(Map<String, dynamic> json) =>
|
||||
variants: (json['variants'] as List<dynamic>?)
|
||||
?.map((e) => ProductVariantDto.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
sellBy: json['sell_by'] as String?,
|
||||
unitId: json['unit_id'] as String?,
|
||||
unitName: json['unit_name'] as String?,
|
||||
unitAbbreviation: json['unit_abbreviation'] as String?,
|
||||
unit: json['unit'] == null
|
||||
? null
|
||||
: ProductUnitDto.fromJson(json['unit'] as Map<String, dynamic>),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$ProductDtoImplToJson(_$ProductDtoImpl instance) =>
|
||||
@@ -62,11 +85,17 @@ Map<String, dynamic> _$$ProductDtoImplToJson(_$ProductDtoImpl instance) =>
|
||||
'business_type': instance.businessType,
|
||||
'image_url': instance.imageUrl,
|
||||
'printer_type': instance.printerType,
|
||||
'print_to_checker': instance.printToChecker,
|
||||
'metadata': instance.metadata,
|
||||
'is_active': instance.isActive,
|
||||
'created_at': instance.createdAt,
|
||||
'updated_at': instance.updatedAt,
|
||||
'variants': instance.variants,
|
||||
'sell_by': instance.sellBy,
|
||||
'unit_id': instance.unitId,
|
||||
'unit_name': instance.unitName,
|
||||
'unit_abbreviation': instance.unitAbbreviation,
|
||||
'unit': instance.unit,
|
||||
};
|
||||
|
||||
_$ProductVariantDtoImpl _$$ProductVariantDtoImplFromJson(
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'dart:developer';
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
|
||||
import '../../../common/service/unit_registry.dart';
|
||||
import '../../../domain/product/product.dart';
|
||||
import '../datasources/local_data_provider.dart';
|
||||
import '../datasources/remote_data_provider.dart';
|
||||
@@ -12,10 +13,38 @@ import '../product_dtos.dart';
|
||||
class ProductRepository implements IProductRepository {
|
||||
final ProductRemoteDataProvider _remoteDataProvider;
|
||||
final ProductLocalDataProvider _localDataProvider;
|
||||
final UnitRegistry _unitRegistry;
|
||||
|
||||
final _logName = 'ProductRepository';
|
||||
|
||||
ProductRepository(this._remoteDataProvider, this._localDataProvider);
|
||||
ProductRepository(
|
||||
this._remoteDataProvider,
|
||||
this._localDataProvider,
|
||||
this._unitRegistry,
|
||||
);
|
||||
|
||||
/// Response produk hanya dijamin membawa `unit_id`. Lengkapi nama dan
|
||||
/// singkatannya dari master satuan supaya keranjang bisa menulis "4,2 ons".
|
||||
Product _withUnit(Product product) {
|
||||
if (!product.isSoldByWeight || product.unitId.isEmpty) return product;
|
||||
if (product.unitAbbreviation.isNotEmpty && product.unitName.isNotEmpty) {
|
||||
return product;
|
||||
}
|
||||
|
||||
final unit = _unitRegistry.byId(product.unitId);
|
||||
if (unit == null) return product;
|
||||
|
||||
return product.copyWith(
|
||||
unitName: product.unitName.isNotEmpty ? product.unitName : unit.name,
|
||||
unitAbbreviation: product.unitAbbreviation.isNotEmpty
|
||||
? product.unitAbbreviation
|
||||
: unit.abbreviation,
|
||||
);
|
||||
}
|
||||
|
||||
List<Product> _withUnits(List<Product> products) =>
|
||||
products.map(_withUnit).toList();
|
||||
|
||||
|
||||
@override
|
||||
Future<Either<ProductFailure, Product>> getProductById(String id) async {
|
||||
@@ -33,7 +62,7 @@ class ProductRepository implements IProductRepository {
|
||||
);
|
||||
}
|
||||
|
||||
final productDomain = product.data!.toDomain();
|
||||
final productDomain = _withUnit(product.data!.toDomain());
|
||||
|
||||
log('✅ Product loaded: ${productDomain.name}', name: _logName);
|
||||
return Right(productDomain);
|
||||
@@ -85,7 +114,9 @@ class ProductRepository implements IProductRepository {
|
||||
|
||||
// 🧱 Bangun entity domain ListProduct
|
||||
final result = ListProduct(
|
||||
products: cachedProducts.data!.map((p) => p.toDomain()).toList(),
|
||||
products: _withUnits(
|
||||
cachedProducts.data!.map((p) => p.toDomain()).toList(),
|
||||
),
|
||||
totalCount: totalCount,
|
||||
page: page,
|
||||
limit: limit,
|
||||
@@ -132,7 +163,9 @@ class ProductRepository implements IProductRepository {
|
||||
return left(cachedProducts.error!);
|
||||
}
|
||||
|
||||
final products = cachedProducts.data!.map((p) => p.toDomain()).toList();
|
||||
final products = _withUnits(
|
||||
cachedProducts.data!.map((p) => p.toDomain()).toList(),
|
||||
);
|
||||
|
||||
final totalCount = await _localDataProvider.getTotalCount(
|
||||
categoryId: categoryId,
|
||||
@@ -180,7 +213,9 @@ class ProductRepository implements IProductRepository {
|
||||
}
|
||||
|
||||
// ✅ Mapping ke domain entity (kalau hasilnya masih berupa DTO)
|
||||
final products = results.data!.map((p) => p.toDomain()).toList();
|
||||
final products = _withUnits(
|
||||
results.data!.map((p) => p.toDomain()).toList(),
|
||||
);
|
||||
|
||||
log(
|
||||
'✅ Local search completed: ${products.length} results',
|
||||
@@ -369,7 +404,7 @@ class ProductRepository implements IProductRepository {
|
||||
|
||||
final products = result.data!.toDomain();
|
||||
|
||||
return right(products);
|
||||
return right(products.copyWith(products: _withUnits(products.products)));
|
||||
} catch (e, s) {
|
||||
log('getProducts', name: _logName, error: e, stackTrace: s);
|
||||
return left(const ProductFailure.unexpectedError());
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import '../../../common/constant/local_storage_key.dart';
|
||||
import '../unit_dtos.dart';
|
||||
|
||||
/// Satuan berjumlah sedikit dan jarang berubah, jadi cukup disimpan sebagai
|
||||
/// satu blob JSON di SharedPreferences — tidak perlu tabel sqflite sendiri.
|
||||
@injectable
|
||||
class UnitLocalDataProvider {
|
||||
final SharedPreferences _sharedPreferences;
|
||||
|
||||
UnitLocalDataProvider(this._sharedPreferences);
|
||||
|
||||
Future<void> saveUnits(List<UnitDto> units) async {
|
||||
final jsonString = jsonEncode(units.map((e) => e.toJson()).toList());
|
||||
await _sharedPreferences.setString(LocalStorageKey.units, jsonString);
|
||||
}
|
||||
|
||||
List<UnitDto> getUnits() {
|
||||
final jsonString = _sharedPreferences.getString(LocalStorageKey.units);
|
||||
if (jsonString == null || jsonString.isEmpty) return const [];
|
||||
|
||||
final decoded = jsonDecode(jsonString);
|
||||
if (decoded is! List) return const [];
|
||||
|
||||
return decoded
|
||||
.whereType<Map<String, dynamic>>()
|
||||
.map(UnitDto.fromJson)
|
||||
.toList();
|
||||
}
|
||||
|
||||
Future<void> clearUnits() async {
|
||||
await _sharedPreferences.remove(LocalStorageKey.units);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:data_channel/data_channel.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
|
||||
import '../../../common/api/api_client.dart';
|
||||
import '../../../common/api/api_failure.dart';
|
||||
import '../../../common/function/app_function.dart';
|
||||
import '../../../common/url/api_path.dart';
|
||||
import '../../../domain/unit/unit.dart';
|
||||
import '../unit_dtos.dart';
|
||||
|
||||
@injectable
|
||||
class UnitRemoteDataProvider {
|
||||
final ApiClient _apiClient;
|
||||
final _logName = 'UnitRemoteDataProvider';
|
||||
|
||||
UnitRemoteDataProvider(this._apiClient);
|
||||
|
||||
Future<DC<UnitFailure, List<UnitDto>>> fetchUnits() async {
|
||||
try {
|
||||
final response = await _apiClient.get(
|
||||
ApiPath.units,
|
||||
headers: getAuthorizationHeader(),
|
||||
);
|
||||
|
||||
if (response.data['data'] == null) {
|
||||
return DC.error(const UnitFailure.empty());
|
||||
}
|
||||
|
||||
return DC.data(UnitDto.listFromResponse(response.data['data']));
|
||||
} on ApiFailure catch (e, s) {
|
||||
log('fetchUnitsError', name: _logName, error: e, stackTrace: s);
|
||||
return DC.error(UnitFailure.serverError(e));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
part of '../unit_dtos.dart';
|
||||
|
||||
@freezed
|
||||
class UnitDto with _$UnitDto {
|
||||
const UnitDto._();
|
||||
|
||||
const factory UnitDto({
|
||||
@JsonKey(name: "id") String? id,
|
||||
@JsonKey(name: "organization_id") String? organizationId,
|
||||
@JsonKey(name: "name") String? name,
|
||||
@JsonKey(name: "abbreviation") String? abbreviation,
|
||||
@JsonKey(name: "is_active") bool? isActive,
|
||||
}) = _UnitDto;
|
||||
|
||||
factory UnitDto.fromJson(Map<String, dynamic> json) =>
|
||||
_$UnitDtoFromJson(json);
|
||||
|
||||
ProductUnit toDomain() => ProductUnit(
|
||||
id: id ?? '',
|
||||
organizationId: organizationId ?? '',
|
||||
name: name ?? '',
|
||||
abbreviation: abbreviation ?? '',
|
||||
isActive: isActive ?? true,
|
||||
);
|
||||
|
||||
factory UnitDto.fromDomain(ProductUnit unit) => UnitDto(
|
||||
id: unit.id,
|
||||
organizationId: unit.organizationId,
|
||||
name: unit.name,
|
||||
abbreviation: unit.abbreviation,
|
||||
isActive: unit.isActive,
|
||||
);
|
||||
|
||||
/// Response `/units` belum tentu berbentuk sama di setiap versi backend —
|
||||
/// bisa `data: [...]` atau `data: { units: [...] }`. Terima keduanya supaya
|
||||
/// POS tidak kehilangan satuan hanya karena pembungkusnya berbeda.
|
||||
static List<UnitDto> listFromResponse(dynamic data) {
|
||||
if (data is List) {
|
||||
return data
|
||||
.whereType<Map<String, dynamic>>()
|
||||
.map(UnitDto.fromJson)
|
||||
.toList();
|
||||
}
|
||||
|
||||
if (data is Map<String, dynamic>) {
|
||||
for (final key in const ['units', 'items', 'data']) {
|
||||
final value = data[key];
|
||||
if (value is List) {
|
||||
return value
|
||||
.whereType<Map<String, dynamic>>()
|
||||
.map(UnitDto.fromJson)
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return const [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
|
||||
import '../../../domain/unit/unit.dart';
|
||||
import '../datasources/local_data_provider.dart';
|
||||
import '../datasources/remote_data_provider.dart';
|
||||
|
||||
@Injectable(as: IUnitRepository)
|
||||
class UnitRepository implements IUnitRepository {
|
||||
final UnitRemoteDataProvider _remoteDataProvider;
|
||||
final UnitLocalDataProvider _localDataProvider;
|
||||
|
||||
final _logName = 'UnitRepository';
|
||||
|
||||
UnitRepository(this._remoteDataProvider, this._localDataProvider);
|
||||
|
||||
@override
|
||||
Future<Either<UnitFailure, List<ProductUnit>>> getUnits({
|
||||
bool forceRefresh = false,
|
||||
}) async {
|
||||
try {
|
||||
final cached = _localDataProvider.getUnits();
|
||||
|
||||
if (!forceRefresh && cached.isNotEmpty) {
|
||||
log('📏 ${cached.length} satuan dari cache', name: _logName);
|
||||
return right(cached.map((e) => e.toDomain()).toList());
|
||||
}
|
||||
|
||||
final result = await _remoteDataProvider.fetchUnits();
|
||||
|
||||
if (result.hasError) {
|
||||
// Offline bukan alasan untuk kehilangan satuan yang sudah pernah ada.
|
||||
if (cached.isNotEmpty) {
|
||||
log('⚠️ Gagal ambil satuan, pakai cache', name: _logName);
|
||||
return right(cached.map((e) => e.toDomain()).toList());
|
||||
}
|
||||
return left(result.error!);
|
||||
}
|
||||
|
||||
final units = result.data!;
|
||||
await _localDataProvider.saveUnits(units);
|
||||
|
||||
log('✅ ${units.length} satuan disinkronkan', name: _logName);
|
||||
return right(units.map((e) => e.toDomain()).toList());
|
||||
} catch (e, s) {
|
||||
log('❌ Error getUnits', name: _logName, error: e, stackTrace: s);
|
||||
return left(UnitFailure.localStorageError(e.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<ProductUnit>> cachedUnits() async =>
|
||||
_localDataProvider.getUnits().map((e) => e.toDomain()).toList();
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
import '../../domain/unit/unit.dart';
|
||||
|
||||
part 'unit_dtos.freezed.dart';
|
||||
part 'unit_dtos.g.dart';
|
||||
|
||||
part 'dtos/unit_dto.dart';
|
||||
@@ -0,0 +1,280 @@
|
||||
// coverage:ignore-file
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'unit_dtos.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
final _privateConstructorUsedError = UnsupportedError(
|
||||
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models',
|
||||
);
|
||||
|
||||
UnitDto _$UnitDtoFromJson(Map<String, dynamic> json) {
|
||||
return _UnitDto.fromJson(json);
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
mixin _$UnitDto {
|
||||
@JsonKey(name: "id")
|
||||
String? get id => throw _privateConstructorUsedError;
|
||||
@JsonKey(name: "organization_id")
|
||||
String? get organizationId => throw _privateConstructorUsedError;
|
||||
@JsonKey(name: "name")
|
||||
String? get name => throw _privateConstructorUsedError;
|
||||
@JsonKey(name: "abbreviation")
|
||||
String? get abbreviation => throw _privateConstructorUsedError;
|
||||
@JsonKey(name: "is_active")
|
||||
bool? get isActive => throw _privateConstructorUsedError;
|
||||
|
||||
/// Serializes this UnitDto to a JSON map.
|
||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
|
||||
/// Create a copy of UnitDto
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
$UnitDtoCopyWith<UnitDto> get copyWith => throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $UnitDtoCopyWith<$Res> {
|
||||
factory $UnitDtoCopyWith(UnitDto value, $Res Function(UnitDto) then) =
|
||||
_$UnitDtoCopyWithImpl<$Res, UnitDto>;
|
||||
@useResult
|
||||
$Res call({
|
||||
@JsonKey(name: "id") String? id,
|
||||
@JsonKey(name: "organization_id") String? organizationId,
|
||||
@JsonKey(name: "name") String? name,
|
||||
@JsonKey(name: "abbreviation") String? abbreviation,
|
||||
@JsonKey(name: "is_active") bool? isActive,
|
||||
});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$UnitDtoCopyWithImpl<$Res, $Val extends UnitDto>
|
||||
implements $UnitDtoCopyWith<$Res> {
|
||||
_$UnitDtoCopyWithImpl(this._value, this._then);
|
||||
|
||||
// ignore: unused_field
|
||||
final $Val _value;
|
||||
// ignore: unused_field
|
||||
final $Res Function($Val) _then;
|
||||
|
||||
/// Create a copy of UnitDto
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? id = freezed,
|
||||
Object? organizationId = freezed,
|
||||
Object? name = freezed,
|
||||
Object? abbreviation = freezed,
|
||||
Object? isActive = freezed,
|
||||
}) {
|
||||
return _then(
|
||||
_value.copyWith(
|
||||
id: freezed == id
|
||||
? _value.id
|
||||
: id // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
organizationId: freezed == organizationId
|
||||
? _value.organizationId
|
||||
: organizationId // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
name: freezed == name
|
||||
? _value.name
|
||||
: name // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
abbreviation: freezed == abbreviation
|
||||
? _value.abbreviation
|
||||
: abbreviation // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
isActive: freezed == isActive
|
||||
? _value.isActive
|
||||
: isActive // ignore: cast_nullable_to_non_nullable
|
||||
as bool?,
|
||||
)
|
||||
as $Val,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class _$$UnitDtoImplCopyWith<$Res> implements $UnitDtoCopyWith<$Res> {
|
||||
factory _$$UnitDtoImplCopyWith(
|
||||
_$UnitDtoImpl value,
|
||||
$Res Function(_$UnitDtoImpl) then,
|
||||
) = __$$UnitDtoImplCopyWithImpl<$Res>;
|
||||
@override
|
||||
@useResult
|
||||
$Res call({
|
||||
@JsonKey(name: "id") String? id,
|
||||
@JsonKey(name: "organization_id") String? organizationId,
|
||||
@JsonKey(name: "name") String? name,
|
||||
@JsonKey(name: "abbreviation") String? abbreviation,
|
||||
@JsonKey(name: "is_active") bool? isActive,
|
||||
});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$$UnitDtoImplCopyWithImpl<$Res>
|
||||
extends _$UnitDtoCopyWithImpl<$Res, _$UnitDtoImpl>
|
||||
implements _$$UnitDtoImplCopyWith<$Res> {
|
||||
__$$UnitDtoImplCopyWithImpl(
|
||||
_$UnitDtoImpl _value,
|
||||
$Res Function(_$UnitDtoImpl) _then,
|
||||
) : super(_value, _then);
|
||||
|
||||
/// Create a copy of UnitDto
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? id = freezed,
|
||||
Object? organizationId = freezed,
|
||||
Object? name = freezed,
|
||||
Object? abbreviation = freezed,
|
||||
Object? isActive = freezed,
|
||||
}) {
|
||||
return _then(
|
||||
_$UnitDtoImpl(
|
||||
id: freezed == id
|
||||
? _value.id
|
||||
: id // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
organizationId: freezed == organizationId
|
||||
? _value.organizationId
|
||||
: organizationId // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
name: freezed == name
|
||||
? _value.name
|
||||
: name // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
abbreviation: freezed == abbreviation
|
||||
? _value.abbreviation
|
||||
: abbreviation // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
isActive: freezed == isActive
|
||||
? _value.isActive
|
||||
: isActive // ignore: cast_nullable_to_non_nullable
|
||||
as bool?,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
class _$UnitDtoImpl extends _UnitDto {
|
||||
const _$UnitDtoImpl({
|
||||
@JsonKey(name: "id") this.id,
|
||||
@JsonKey(name: "organization_id") this.organizationId,
|
||||
@JsonKey(name: "name") this.name,
|
||||
@JsonKey(name: "abbreviation") this.abbreviation,
|
||||
@JsonKey(name: "is_active") this.isActive,
|
||||
}) : super._();
|
||||
|
||||
factory _$UnitDtoImpl.fromJson(Map<String, dynamic> json) =>
|
||||
_$$UnitDtoImplFromJson(json);
|
||||
|
||||
@override
|
||||
@JsonKey(name: "id")
|
||||
final String? id;
|
||||
@override
|
||||
@JsonKey(name: "organization_id")
|
||||
final String? organizationId;
|
||||
@override
|
||||
@JsonKey(name: "name")
|
||||
final String? name;
|
||||
@override
|
||||
@JsonKey(name: "abbreviation")
|
||||
final String? abbreviation;
|
||||
@override
|
||||
@JsonKey(name: "is_active")
|
||||
final bool? isActive;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'UnitDto(id: $id, organizationId: $organizationId, name: $name, abbreviation: $abbreviation, isActive: $isActive)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$UnitDtoImpl &&
|
||||
(identical(other.id, id) || other.id == id) &&
|
||||
(identical(other.organizationId, organizationId) ||
|
||||
other.organizationId == organizationId) &&
|
||||
(identical(other.name, name) || other.name == name) &&
|
||||
(identical(other.abbreviation, abbreviation) ||
|
||||
other.abbreviation == abbreviation) &&
|
||||
(identical(other.isActive, isActive) ||
|
||||
other.isActive == isActive));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
runtimeType,
|
||||
id,
|
||||
organizationId,
|
||||
name,
|
||||
abbreviation,
|
||||
isActive,
|
||||
);
|
||||
|
||||
/// Create a copy of UnitDto
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
_$$UnitDtoImplCopyWith<_$UnitDtoImpl> get copyWith =>
|
||||
__$$UnitDtoImplCopyWithImpl<_$UnitDtoImpl>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$$UnitDtoImplToJson(this);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _UnitDto extends UnitDto {
|
||||
const factory _UnitDto({
|
||||
@JsonKey(name: "id") final String? id,
|
||||
@JsonKey(name: "organization_id") final String? organizationId,
|
||||
@JsonKey(name: "name") final String? name,
|
||||
@JsonKey(name: "abbreviation") final String? abbreviation,
|
||||
@JsonKey(name: "is_active") final bool? isActive,
|
||||
}) = _$UnitDtoImpl;
|
||||
const _UnitDto._() : super._();
|
||||
|
||||
factory _UnitDto.fromJson(Map<String, dynamic> json) = _$UnitDtoImpl.fromJson;
|
||||
|
||||
@override
|
||||
@JsonKey(name: "id")
|
||||
String? get id;
|
||||
@override
|
||||
@JsonKey(name: "organization_id")
|
||||
String? get organizationId;
|
||||
@override
|
||||
@JsonKey(name: "name")
|
||||
String? get name;
|
||||
@override
|
||||
@JsonKey(name: "abbreviation")
|
||||
String? get abbreviation;
|
||||
@override
|
||||
@JsonKey(name: "is_active")
|
||||
bool? get isActive;
|
||||
|
||||
/// Create a copy of UnitDto
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
_$$UnitDtoImplCopyWith<_$UnitDtoImpl> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'unit_dtos.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_$UnitDtoImpl _$$UnitDtoImplFromJson(Map<String, dynamic> json) =>
|
||||
_$UnitDtoImpl(
|
||||
id: json['id'] as String?,
|
||||
organizationId: json['organization_id'] as String?,
|
||||
name: json['name'] as String?,
|
||||
abbreviation: json['abbreviation'] as String?,
|
||||
isActive: json['is_active'] as bool?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$UnitDtoImplToJson(_$UnitDtoImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'organization_id': instance.organizationId,
|
||||
'name': instance.name,
|
||||
'abbreviation': instance.abbreviation,
|
||||
'is_active': instance.isActive,
|
||||
};
|
||||
+52
-25
@@ -88,6 +88,8 @@ import 'package:apskel_pos_flutter_v2/common/network/network_client.dart'
|
||||
import 'package:apskel_pos_flutter_v2/common/service/device_info_service.dart'
|
||||
as _i288;
|
||||
import 'package:apskel_pos_flutter_v2/common/service/fcm_service.dart' as _i312;
|
||||
import 'package:apskel_pos_flutter_v2/common/service/unit_registry.dart'
|
||||
as _i584;
|
||||
import 'package:apskel_pos_flutter_v2/domain/analytic/analytic.dart' as _i346;
|
||||
import 'package:apskel_pos_flutter_v2/domain/auth/auth.dart' as _i776;
|
||||
import 'package:apskel_pos_flutter_v2/domain/category/category.dart' as _i502;
|
||||
@@ -99,6 +101,7 @@ import 'package:apskel_pos_flutter_v2/domain/payment_method/payment_method.dart'
|
||||
import 'package:apskel_pos_flutter_v2/domain/printer/printer.dart' as _i104;
|
||||
import 'package:apskel_pos_flutter_v2/domain/product/product.dart' as _i44;
|
||||
import 'package:apskel_pos_flutter_v2/domain/table/table.dart' as _i983;
|
||||
import 'package:apskel_pos_flutter_v2/domain/unit/unit.dart' as _i320;
|
||||
import 'package:apskel_pos_flutter_v2/env.dart' as _i923;
|
||||
import 'package:apskel_pos_flutter_v2/infrastructure/analytic/datasources/remote_data_provider.dart'
|
||||
as _i708;
|
||||
@@ -148,6 +151,12 @@ import 'package:apskel_pos_flutter_v2/infrastructure/table/datasources/remote_da
|
||||
as _i95;
|
||||
import 'package:apskel_pos_flutter_v2/infrastructure/table/repositories/table_repository.dart'
|
||||
as _i824;
|
||||
import 'package:apskel_pos_flutter_v2/infrastructure/unit/datasources/local_data_provider.dart'
|
||||
as _i393;
|
||||
import 'package:apskel_pos_flutter_v2/infrastructure/unit/datasources/remote_data_provider.dart'
|
||||
as _i1016;
|
||||
import 'package:apskel_pos_flutter_v2/infrastructure/unit/repositories/unit_repository.dart'
|
||||
as _i228;
|
||||
import 'package:apskel_pos_flutter_v2/presentation/router/app_router.dart'
|
||||
as _i800;
|
||||
import 'package:connectivity_plus/connectivity_plus.dart' as _i895;
|
||||
@@ -229,11 +238,14 @@ extension GetItInjectableX on _i174.GetIt {
|
||||
gh.factory<_i693.OutletLocalDatasource>(
|
||||
() => _i693.OutletLocalDatasource(gh<_i460.SharedPreferences>()),
|
||||
);
|
||||
gh.factory<_i393.UnitLocalDataProvider>(
|
||||
() => _i393.UnitLocalDataProvider(gh<_i460.SharedPreferences>()),
|
||||
);
|
||||
gh.lazySingleton<_i457.ApiClient>(
|
||||
() => _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>(),
|
||||
@@ -279,6 +291,9 @@ extension GetItInjectableX on _i174.GetIt {
|
||||
gh.factory<_i95.TableRemoteDataProvider>(
|
||||
() => _i95.TableRemoteDataProvider(gh<_i457.ApiClient>()),
|
||||
);
|
||||
gh.factory<_i1016.UnitRemoteDataProvider>(
|
||||
() => _i1016.UnitRemoteDataProvider(gh<_i457.ApiClient>()),
|
||||
);
|
||||
gh.factory<_i776.IAuthRepository>(
|
||||
() => _i941.AuthRepository(
|
||||
gh<_i370.AuthRemoteDataProvider>(),
|
||||
@@ -291,13 +306,6 @@ extension GetItInjectableX on _i174.GetIt {
|
||||
gh<_i708.CategoryLocalDataProvider>(),
|
||||
),
|
||||
);
|
||||
gh.factory<_i46.LoginFormBloc>(
|
||||
() => _i46.LoginFormBloc(
|
||||
gh<_i776.IAuthRepository>(),
|
||||
gh<_i312.FcmService>(),
|
||||
gh<_i288.DeviceInfoService>(),
|
||||
),
|
||||
);
|
||||
gh.factory<_i983.ITableRepository>(
|
||||
() => _i824.TableRepository(
|
||||
gh<_i95.TableRemoteDataProvider>(),
|
||||
@@ -313,12 +321,6 @@ extension GetItInjectableX on _i174.GetIt {
|
||||
gh.factory<_i424.TableLoaderBloc>(
|
||||
() => _i424.TableLoaderBloc(gh<_i983.ITableRepository>()),
|
||||
);
|
||||
gh.factory<_i44.IProductRepository>(
|
||||
() => _i763.ProductRepository(
|
||||
gh<_i707.ProductRemoteDataProvider>(),
|
||||
gh<_i464.ProductLocalDataProvider>(),
|
||||
),
|
||||
);
|
||||
gh.factory<_i552.IOutletRepository>(
|
||||
() => _i845.OutletRepository(
|
||||
gh<_i132.OutletRemoteDataProvider>(),
|
||||
@@ -342,6 +344,12 @@ extension GetItInjectableX on _i174.GetIt {
|
||||
gh<_i552.IOutletRepository>(),
|
||||
),
|
||||
);
|
||||
gh.factory<_i320.IUnitRepository>(
|
||||
() => _i228.UnitRepository(
|
||||
gh<_i1016.UnitRemoteDataProvider>(),
|
||||
gh<_i393.UnitLocalDataProvider>(),
|
||||
),
|
||||
);
|
||||
gh.factory<_i143.ICustomerRepository>(
|
||||
() => _i385.CustomerRepository(gh<_i841.CustomerRemoteDataProvider>()),
|
||||
);
|
||||
@@ -363,6 +371,9 @@ extension GetItInjectableX on _i174.GetIt {
|
||||
gh.factory<_i822.VoidFormBloc>(
|
||||
() => _i822.VoidFormBloc(gh<_i299.IOrderRepository>()),
|
||||
);
|
||||
gh.lazySingleton<_i584.UnitRegistry>(
|
||||
() => _i584.UnitRegistry(gh<_i320.IUnitRepository>()),
|
||||
);
|
||||
gh.factory<_i683.CustomerLoaderBloc>(
|
||||
() => _i683.CustomerLoaderBloc(gh<_i143.ICustomerRepository>()),
|
||||
);
|
||||
@@ -381,19 +392,19 @@ extension GetItInjectableX on _i174.GetIt {
|
||||
gh.factory<_i76.OutletLoaderBloc>(
|
||||
() => _i76.OutletLoaderBloc(gh<_i552.IOutletRepository>()),
|
||||
);
|
||||
gh.factory<_i13.ProductLoaderBloc>(
|
||||
() => _i13.ProductLoaderBloc(gh<_i44.IProductRepository>()),
|
||||
);
|
||||
gh.factory<_i741.SyncBloc>(
|
||||
() => _i741.SyncBloc(
|
||||
gh<_i44.IProductRepository>(),
|
||||
gh<_i502.ICategoryRepository>(),
|
||||
gh.factory<_i46.LoginFormBloc>(
|
||||
() => _i46.LoginFormBloc(
|
||||
gh<_i776.IAuthRepository>(),
|
||||
gh<_i312.FcmService>(),
|
||||
gh<_i288.DeviceInfoService>(),
|
||||
gh<_i584.UnitRegistry>(),
|
||||
),
|
||||
);
|
||||
gh.factory<_i729.SyncSettingBloc>(
|
||||
() => _i729.SyncSettingBloc(
|
||||
gh<_i44.IProductRepository>(),
|
||||
gh<_i502.ICategoryRepository>(),
|
||||
gh.factory<_i44.IProductRepository>(
|
||||
() => _i763.ProductRepository(
|
||||
gh<_i707.ProductRemoteDataProvider>(),
|
||||
gh<_i464.ProductLocalDataProvider>(),
|
||||
gh<_i584.UnitRegistry>(),
|
||||
),
|
||||
);
|
||||
gh.factory<_i911.CategoryAnalyticLoaderBloc>(
|
||||
@@ -419,6 +430,22 @@ extension GetItInjectableX on _i174.GetIt {
|
||||
gh.factory<_i413.SalesAnalyticLoaderBloc>(
|
||||
() => _i413.SalesAnalyticLoaderBloc(gh<_i346.IAnalyticRepository>()),
|
||||
);
|
||||
gh.factory<_i729.SyncSettingBloc>(
|
||||
() => _i729.SyncSettingBloc(
|
||||
gh<_i44.IProductRepository>(),
|
||||
gh<_i502.ICategoryRepository>(),
|
||||
gh<_i584.UnitRegistry>(),
|
||||
),
|
||||
);
|
||||
gh.factory<_i13.ProductLoaderBloc>(
|
||||
() => _i13.ProductLoaderBloc(gh<_i44.IProductRepository>()),
|
||||
);
|
||||
gh.factory<_i741.SyncBloc>(
|
||||
() => _i741.SyncBloc(
|
||||
gh<_i44.IProductRepository>(),
|
||||
gh<_i502.ICategoryRepository>(),
|
||||
),
|
||||
);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
+7
-4
@@ -5,9 +5,9 @@ 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 'common/service/unit_registry.dart';
|
||||
import 'injection.dart';
|
||||
import 'presentation/app_widget.dart';
|
||||
|
||||
@@ -44,13 +44,16 @@ void main() async {
|
||||
debugPrint = (message, {wrapWidth}) => '';
|
||||
}
|
||||
|
||||
await configureDependencies(
|
||||
kReleaseMode ? Environment.prod : Environment.dev,
|
||||
);
|
||||
const String env = String.fromEnvironment('ENV', defaultValue: 'dev');
|
||||
await configureDependencies(env);
|
||||
|
||||
// Inisialisasi FCM setelah DI siap
|
||||
await getIt<FcmService>().initialize();
|
||||
|
||||
// Master satuan dimuat dari cache lebih dulu supaya produk timbangan
|
||||
// langsung punya label satuan, bahkan saat perangkat offline.
|
||||
unawaited(getIt<UnitRegistry>().load());
|
||||
|
||||
runApp(const AppWidget());
|
||||
},
|
||||
(error, stack) {
|
||||
|
||||
@@ -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(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import '../../../application/checkout/checkout_form/checkout_form_bloc.dart';
|
||||
import '../../../common/extension/extension.dart';
|
||||
import '../../../common/theme/theme.dart';
|
||||
import '../../../domain/product/product.dart';
|
||||
import '../dialog/dialog.dart';
|
||||
import '../image/image.dart';
|
||||
import '../spaces/space.dart';
|
||||
|
||||
@@ -26,8 +27,8 @@ class _OrderMenuState extends State<OrderMenu> {
|
||||
|
||||
_controller.addListener(() {
|
||||
context.read<CheckoutFormBloc>().add(
|
||||
CheckoutFormEvent.updateItemNotes(
|
||||
widget.data.product,
|
||||
CheckoutFormEvent.updateLineNotes(
|
||||
widget.data.lineId,
|
||||
_controller.text,
|
||||
),
|
||||
);
|
||||
@@ -40,6 +41,29 @@ class _OrderMenuState extends State<OrderMenu> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _removeLine() {
|
||||
context.read<CheckoutFormBloc>().add(
|
||||
CheckoutFormEvent.removeLine(widget.data.lineId),
|
||||
);
|
||||
}
|
||||
|
||||
/// Koreksi salah timbang: ubah angka pada baris yang sama, bukan menambah
|
||||
/// baris baru.
|
||||
void _editWeight() {
|
||||
final bloc = context.read<CheckoutFormBloc>();
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => WeightInputDialog(
|
||||
product: widget.data.product,
|
||||
variant: widget.data.variant,
|
||||
initialWeight: widget.data.weight,
|
||||
onSubmit: (weight) => bloc.add(
|
||||
CheckoutFormEvent.updateItemWeight(widget.data.lineId, weight),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
@@ -77,9 +101,10 @@ class _OrderMenuState extends State<OrderMenu> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
(widget.data.product.price +
|
||||
(widget.data.variant?.priceModifier ?? 0))
|
||||
.currencyFormatRp,
|
||||
widget.data.isWeighted &&
|
||||
widget.data.product.unitLabel.isNotEmpty
|
||||
? '${widget.data.unitPrice.currencyFormatRp} / ${widget.data.product.unitLabel}'
|
||||
: widget.data.unitPrice.currencyFormatRp,
|
||||
),
|
||||
if (widget.data.variant != null)
|
||||
Text(widget.data.variant?.name ?? ""),
|
||||
@@ -87,60 +112,15 @@ class _OrderMenuState extends State<OrderMenu> {
|
||||
),
|
||||
),
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
context.read<CheckoutFormBloc>().add(
|
||||
CheckoutFormEvent.removeItem(
|
||||
widget.data.product,
|
||||
widget.data.variant,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
width: 30,
|
||||
height: 30,
|
||||
color: AppColor.white,
|
||||
child: const Icon(
|
||||
Icons.remove_circle,
|
||||
color: AppColor.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: 30.0,
|
||||
child: Center(child: Text(widget.data.quantity.toString())),
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
context.read<CheckoutFormBloc>().add(
|
||||
CheckoutFormEvent.addItem(
|
||||
widget.data.product,
|
||||
widget.data.variant,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
width: 30,
|
||||
height: 30,
|
||||
color: AppColor.white,
|
||||
child: const Icon(
|
||||
Icons.add_circle,
|
||||
color: AppColor.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (widget.data.isWeighted)
|
||||
_buildWeightControl()
|
||||
else
|
||||
_buildQuantityStepper(),
|
||||
const SpaceWidth(8),
|
||||
SizedBox(
|
||||
width: 80.0,
|
||||
child: Text(
|
||||
((widget.data.product.price +
|
||||
(widget.data.variant?.priceModifier ?? 0)) *
|
||||
widget.data.quantity)
|
||||
.currencyFormatRp,
|
||||
widget.data.lineTotal.currencyFormatRp,
|
||||
textAlign: TextAlign.right,
|
||||
style: const TextStyle(
|
||||
color: AppColor.primary,
|
||||
@@ -167,14 +147,7 @@ class _OrderMenuState extends State<OrderMenu> {
|
||||
),
|
||||
const SpaceWidth(16.0),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
context.read<CheckoutFormBloc>().add(
|
||||
CheckoutFormEvent.removeItem(
|
||||
widget.data.product,
|
||||
widget.data.variant,
|
||||
),
|
||||
);
|
||||
},
|
||||
onTap: _removeLine,
|
||||
child: Container(
|
||||
height: 40,
|
||||
width: 40,
|
||||
@@ -194,4 +167,77 @@ class _OrderMenuState extends State<OrderMenu> {
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// Baris berbobot tidak punya stepper — nilainya bukan cacah, dan angka 1
|
||||
/// yang tersimpan di `quantity` akan terbaca seperti "satu ons".
|
||||
Widget _buildWeightControl() {
|
||||
return GestureDetector(
|
||||
onTap: _editWeight,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.primary.withOpacity(0.08),
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
border: Border.all(color: AppColor.primary.withOpacity(0.4)),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
widget.data.quantityLabel,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColor.primary,
|
||||
),
|
||||
),
|
||||
const SpaceWidth(4),
|
||||
const Icon(Icons.edit_outlined, size: 14, color: AppColor.primary),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildQuantityStepper() {
|
||||
return Row(
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
context.read<CheckoutFormBloc>().add(
|
||||
CheckoutFormEvent.removeItem(
|
||||
widget.data.product,
|
||||
widget.data.variant,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
width: 30,
|
||||
height: 30,
|
||||
color: AppColor.white,
|
||||
child: const Icon(Icons.remove_circle, color: AppColor.primary),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: 30.0,
|
||||
child: Center(child: Text(widget.data.quantity.toString())),
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
context.read<CheckoutFormBloc>().add(
|
||||
CheckoutFormEvent.addItem(
|
||||
widget.data.product,
|
||||
widget.data.variant,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
width: 30,
|
||||
height: 30,
|
||||
color: AppColor.white,
|
||||
child: const Icon(Icons.add_circle, color: AppColor.primary),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,18 +17,35 @@ class ProductCard extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
if (product.isActive == true) {
|
||||
if (product.variants.isEmpty) {
|
||||
context.read<CheckoutFormBloc>().add(
|
||||
CheckoutFormEvent.addItem(product, null),
|
||||
);
|
||||
} else {
|
||||
if (product.isActive != true) return;
|
||||
|
||||
if (product.variants.isNotEmpty) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => VariantDialog(product: product),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Produk timbangan tidak bisa langsung masuk keranjang — kasir harus
|
||||
// menimbang dulu, dan tiap penimbangan jadi barisnya sendiri.
|
||||
if (product.isSoldByWeight) {
|
||||
final bloc = context.read<CheckoutFormBloc>();
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => WeightInputDialog(
|
||||
product: product,
|
||||
onSubmit: (weight) => bloc.add(
|
||||
CheckoutFormEvent.addWeightedItem(product, null, weight),
|
||||
),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
context.read<CheckoutFormBloc>().add(
|
||||
CheckoutFormEvent.addItem(product, null),
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
@@ -75,7 +92,11 @@ class ProductCard extends StatelessWidget {
|
||||
Align(
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
product.price.currencyFormatRp,
|
||||
product.isSoldByWeight
|
||||
? '${product.price.currencyFormatRp} ${product.priceUnitLabel}'
|
||||
.trim()
|
||||
: product.price.currencyFormatRp,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 12,
|
||||
@@ -89,31 +110,39 @@ class ProductCard extends StatelessWidget {
|
||||
|
||||
BlocBuilder<CheckoutFormBloc, CheckoutFormState>(
|
||||
builder: (context, state) {
|
||||
final totalQuantity = state.items
|
||||
final lines = state.items
|
||||
.where((item) => item.product.id == product.id)
|
||||
.map((item) => item.quantity)
|
||||
.fold(0, (sum, qty) => sum + qty);
|
||||
.toList();
|
||||
|
||||
if (lines.isEmpty) return const SizedBox.shrink();
|
||||
|
||||
// Untuk produk timbangan, jumlah baris tidak berarti apa-apa
|
||||
// (selalu 1 per penimbangan) — yang informatif adalah total
|
||||
// berat yang sudah masuk keranjang.
|
||||
final label = product.isSoldByWeight
|
||||
? '${lines.fold<double>(0, (sum, item) => sum + (item.weight ?? 0)).weightFormat} ${product.unitLabel}'
|
||||
.trim()
|
||||
: lines
|
||||
.fold<int>(0, (sum, item) => sum + item.quantity)
|
||||
.toString();
|
||||
|
||||
if (totalQuantity == 0) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return Positioned(
|
||||
top: 4,
|
||||
right: 4,
|
||||
child: Container(
|
||||
width: 40,
|
||||
constraints: const BoxConstraints(minWidth: 40),
|
||||
height: 40,
|
||||
padding: const EdgeInsets.all(6),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6),
|
||||
decoration: const BoxDecoration(
|
||||
borderRadius: BorderRadius.all(Radius.circular(9.0)),
|
||||
color: AppColor.primary,
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
totalQuantity.toString(),
|
||||
style: const TextStyle(
|
||||
label,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 20,
|
||||
fontSize: product.isSoldByWeight ? 13 : 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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';
|
||||
@@ -20,6 +24,8 @@ import '../spaces/space.dart';
|
||||
part 'custom_modal_dialog.dart';
|
||||
part 'outlet_dialog.dart';
|
||||
part 'variant_dialog.dart';
|
||||
part 'weight_input_dialog.dart';
|
||||
part 'delivery_dialog.dart';
|
||||
part 'order_type_dialog.dart';
|
||||
part 'printer_bluetooth_dialog.dart';
|
||||
part 'print_progress_dialog.dart';
|
||||
|
||||
@@ -163,7 +163,9 @@ class OrderSaveConfirmDialog extends StatelessWidget {
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
Text(
|
||||
'${item.quantity} qty',
|
||||
item.isWeighted
|
||||
? item.quantityLabel
|
||||
: '${item.quantity} qty',
|
||||
style: AppStyle.sm.copyWith(
|
||||
color: AppColor.textSecondary,
|
||||
),
|
||||
@@ -185,8 +187,7 @@ class OrderSaveConfirmDialog extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
((item.product.price) * item.quantity)
|
||||
.currencyFormatRpV2,
|
||||
item.lineTotal.currencyFormatRpV2,
|
||||
style: AppStyle.md.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColor.primary,
|
||||
|
||||
@@ -28,14 +28,16 @@ class OrderVoidConfirmDialog extends StatelessWidget {
|
||||
children: [
|
||||
Expanded(
|
||||
child: AppElevatedButton.outlined(
|
||||
onPressed: () => context.maybePop(),
|
||||
onPressed: state.isSubmitting ? null : () => context.maybePop(),
|
||||
label: 'Batal',
|
||||
),
|
||||
),
|
||||
const SpaceWidth(16),
|
||||
Expanded(
|
||||
child: AppElevatedButton.filled(
|
||||
onPressed: () {
|
||||
onPressed: state.isSubmitting
|
||||
? null
|
||||
: () {
|
||||
context.read<VoidFormBloc>().add(
|
||||
const VoidFormEvent.submitted(),
|
||||
);
|
||||
|
||||
@@ -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',
|
||||
};
|
||||
}
|
||||
@@ -21,10 +21,33 @@ class VariantDialog extends StatelessWidget {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
// Aksi saat varian dipilih
|
||||
context.maybePop();
|
||||
context.read<CheckoutFormBloc>().add(
|
||||
CheckoutFormEvent.addItem(product, variant),
|
||||
final bloc = context.read<CheckoutFormBloc>();
|
||||
// Context dialog ini mati begitu di-pop, jadi dialog lanjutan
|
||||
// dibuka lewat context milik Navigator.
|
||||
final navigator = Navigator.of(context, rootNavigator: true);
|
||||
navigator.pop();
|
||||
|
||||
// Produk timbangan lanjut ke papan angka; varian hanya mengubah
|
||||
// harga per satuannya.
|
||||
if (product.isSoldByWeight) {
|
||||
showDialog(
|
||||
context: navigator.context,
|
||||
builder: (context) => WeightInputDialog(
|
||||
product: product,
|
||||
variant: variant,
|
||||
onSubmit: (weight) => bloc.add(
|
||||
CheckoutFormEvent.addWeightedItem(
|
||||
product,
|
||||
variant,
|
||||
weight,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
bloc.add(CheckoutFormEvent.addItem(product, variant));
|
||||
},
|
||||
child: Container(
|
||||
width: (context.deviceWidth * 0.4 - 12 - 32) / 2 - 6, // 2 per row
|
||||
|
||||
@@ -0,0 +1,267 @@
|
||||
part of 'dialog.dart';
|
||||
|
||||
/// Papan angka desimal untuk produk yang dijual per timbangan.
|
||||
///
|
||||
/// Satu kali dialog ini ditutup dengan angka = satu penimbangan = satu baris
|
||||
/// keranjang. Untuk mengoreksi salah timbang, buka lagi baris yang sama lewat
|
||||
/// [initialWeight] alih-alih menambah baris baru.
|
||||
class WeightInputDialog extends StatefulWidget {
|
||||
final Product product;
|
||||
final ProductVariant? variant;
|
||||
final double? initialWeight;
|
||||
final ValueChanged<double> onSubmit;
|
||||
|
||||
const WeightInputDialog({
|
||||
super.key,
|
||||
required this.product,
|
||||
required this.onSubmit,
|
||||
this.variant,
|
||||
this.initialWeight,
|
||||
});
|
||||
|
||||
/// Berat terkecil yang diterima backend; di bawah ini membulat ke nol.
|
||||
static const double minWeight = 0.001;
|
||||
|
||||
/// Backend membulatkan berat ke 3 desimal.
|
||||
static const int maxDecimals = 3;
|
||||
|
||||
@override
|
||||
State<WeightInputDialog> createState() => _WeightInputDialogState();
|
||||
}
|
||||
|
||||
class _WeightInputDialogState extends State<WeightInputDialog> {
|
||||
/// Angka disimpan sebagai teks supaya "4," dan "4,20" bisa diketik utuh
|
||||
/// tanpa dinormalisasi di tengah pengetikan.
|
||||
late String _input = _formatInitial(widget.initialWeight);
|
||||
|
||||
static String _formatInitial(double? weight) {
|
||||
if (weight == null || weight <= 0) return '';
|
||||
return weight
|
||||
.toStringAsFixed(WeightInputDialog.maxDecimals)
|
||||
.replaceFirst(RegExp(r'\.?0+$'), '')
|
||||
.replaceAll('.', ',');
|
||||
}
|
||||
|
||||
double get _weight => double.tryParse(_input.replaceAll(',', '.')) ?? 0;
|
||||
|
||||
bool get _isValid => _weight >= WeightInputDialog.minWeight;
|
||||
|
||||
double get _unitPrice =>
|
||||
widget.product.price + (widget.variant?.priceModifier ?? 0);
|
||||
|
||||
String get _unitLabel => widget.product.unitLabel;
|
||||
|
||||
void _tapDigit(String digit) {
|
||||
final separatorIndex = _input.indexOf(',');
|
||||
if (separatorIndex != -1 &&
|
||||
_input.length - separatorIndex - 1 >= WeightInputDialog.maxDecimals) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
// Hindari "007": nol di depan hanya berarti sesuatu sebelum koma.
|
||||
_input = _input == '0' ? digit : '$_input$digit';
|
||||
});
|
||||
}
|
||||
|
||||
void _tapSeparator() {
|
||||
if (_input.contains(',')) return;
|
||||
setState(() => _input = _input.isEmpty ? '0,' : '$_input,');
|
||||
}
|
||||
|
||||
void _tapBackspace() {
|
||||
if (_input.isEmpty) return;
|
||||
setState(() => _input = _input.substring(0, _input.length - 1));
|
||||
}
|
||||
|
||||
void _tapClear() => setState(() => _input = '');
|
||||
|
||||
void _submit() {
|
||||
if (!_isValid) return;
|
||||
final rounded = double.parse(
|
||||
_weight.toStringAsFixed(WeightInputDialog.maxDecimals),
|
||||
);
|
||||
context.maybePop();
|
||||
widget.onSubmit(rounded);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CustomModalDialog(
|
||||
title: widget.product.name,
|
||||
subtitle: _unitLabel.isEmpty
|
||||
? 'Masukkan berat'
|
||||
: 'Masukkan berat dalam $_unitLabel',
|
||||
minWidth: 380,
|
||||
maxWidth: 420,
|
||||
minHeight: 0,
|
||||
contentPadding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_buildDisplay(),
|
||||
const SpaceHeight(16),
|
||||
_buildKeypad(),
|
||||
const SpaceHeight(16),
|
||||
AppElevatedButton.filled(
|
||||
borderRadius: 12,
|
||||
disabled: !_isValid,
|
||||
onPressed: _submit,
|
||||
label: widget.initialWeight == null
|
||||
? 'Tambah ke Pesanan'
|
||||
: 'Simpan Berat',
|
||||
),
|
||||
if (_input.isNotEmpty && !_isValid) ...[
|
||||
const SpaceHeight(8),
|
||||
Text(
|
||||
'Berat minimal 0,001${_unitLabel.isEmpty ? '' : ' $_unitLabel'}',
|
||||
textAlign: TextAlign.center,
|
||||
style: AppStyle.sm.copyWith(color: AppColor.error),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDisplay() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.primary.withOpacity(0.06),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: AppColor.primary.withOpacity(0.3)),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.baseline,
|
||||
textBaseline: TextBaseline.alphabetic,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
_input.isEmpty ? '0' : _input,
|
||||
textAlign: TextAlign.right,
|
||||
style: AppStyle.xxl.copyWith(
|
||||
fontSize: 40,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: _input.isEmpty
|
||||
? AppColor.textSecondary
|
||||
: AppColor.black,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (_unitLabel.isNotEmpty) ...[
|
||||
const SpaceWidth(8),
|
||||
Text(
|
||||
_unitLabel,
|
||||
style: AppStyle.lg.copyWith(color: AppColor.textSecondary),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
const SpaceHeight(4),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'${_unitPrice.currencyFormatRpV2}'
|
||||
'${_unitLabel.isEmpty ? '' : ' / $_unitLabel'}',
|
||||
style: AppStyle.sm.copyWith(color: AppColor.textSecondary),
|
||||
),
|
||||
Text(
|
||||
(_unitPrice * _weight).currencyFormatRpV2,
|
||||
style: AppStyle.lg.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColor.primary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildKeypad() {
|
||||
const keys = [
|
||||
['1', '2', '3'],
|
||||
['4', '5', '6'],
|
||||
['7', '8', '9'],
|
||||
[',', '0', 'hapus'],
|
||||
];
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
for (final row in keys)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
child: Row(
|
||||
children: [
|
||||
for (final key in row)
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
child: _KeypadButton(
|
||||
label: key,
|
||||
onTap: () {
|
||||
switch (key) {
|
||||
case ',':
|
||||
_tapSeparator();
|
||||
case 'hapus':
|
||||
_tapBackspace();
|
||||
default:
|
||||
_tapDigit(key);
|
||||
}
|
||||
},
|
||||
// Tahan lama untuk mengosongkan angka sekaligus.
|
||||
onLongPress: key == 'hapus' ? _tapClear : null,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _KeypadButton extends StatelessWidget {
|
||||
final String label;
|
||||
final VoidCallback onTap;
|
||||
final VoidCallback? onLongPress;
|
||||
|
||||
const _KeypadButton({
|
||||
required this.label,
|
||||
required this.onTap,
|
||||
this.onLongPress,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isBackspace = label == 'hapus';
|
||||
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
onLongPress: onLongPress,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Container(
|
||||
height: 56,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: AppColor.border),
|
||||
),
|
||||
child: isBackspace
|
||||
? const Icon(Icons.backspace_outlined, color: AppColor.primary)
|
||||
: Text(
|
||||
label,
|
||||
style: AppStyle.xxl.copyWith(fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
@@ -30,6 +51,9 @@ class PrintUi {
|
||||
outletName: outlet.name,
|
||||
address: outlet.address,
|
||||
phoneNumber: outlet.phoneNumber,
|
||||
fontType: PosFontType.fontA,
|
||||
width: PosTextSize.size1,
|
||||
height: PosTextSize.size1,
|
||||
);
|
||||
|
||||
bytes += builder.orderInfo(
|
||||
@@ -38,9 +62,14 @@ class PrintUi {
|
||||
cashierName: cashierName,
|
||||
paymentMethod: order.payments.last.paymentMethodName,
|
||||
tableNumber: order.tableNumber,
|
||||
fontType: PosFontType.fontA,
|
||||
width: PosTextSize.size1,
|
||||
height: PosTextSize.size1,
|
||||
);
|
||||
|
||||
bytes += builder.orderType(order.orderType);
|
||||
bytes += builder.orderType(order.orderType, fontType: PosFontType.fontA,
|
||||
width: PosTextSize.size1,
|
||||
height: PosTextSize.size1,);
|
||||
|
||||
bytes += builder.emptyLines(1);
|
||||
|
||||
@@ -48,10 +77,14 @@ class PrintUi {
|
||||
bytes += builder.orderItem(
|
||||
productName: item.productName,
|
||||
quantity: item.quantity,
|
||||
quantityLabel: item.isWeighted ? item.quantityLabel : null,
|
||||
unitPrice: item.unitPrice.currencyFormatRpV2,
|
||||
totalPrice: item.totalPrice.currencyFormatRpV2,
|
||||
variantName: item.productVariantName,
|
||||
notes: item.notes,
|
||||
fontType: PosFontType.fontA,
|
||||
width: PosTextSize.size1,
|
||||
height: PosTextSize.size1,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -61,6 +94,9 @@ class PrintUi {
|
||||
discount: order.discountAmount.currencyFormatRpV2,
|
||||
total: order.totalAmount.currencyFormatRpV2,
|
||||
paid: order.totalPaid.currencyFormatRpV2,
|
||||
fontType: PosFontType.fontA,
|
||||
width: PosTextSize.size1,
|
||||
height: PosTextSize.size1,
|
||||
);
|
||||
|
||||
bytes += builder.footer();
|
||||
@@ -76,7 +112,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,26 +126,22 @@ class PrintUi {
|
||||
|
||||
// Header
|
||||
bytes += builder.textCenter('Table Checker', bold: true);
|
||||
bytes += builder.separator();
|
||||
bytes += builder.textCenter(
|
||||
'Table : ${order.tableNumber.isNotEmpty ? order.tableNumber : '-'}',
|
||||
bold: true,
|
||||
);
|
||||
bytes += builder.separator();
|
||||
bytes += builder.tableName(order.tableNumber.isNotEmpty ? order.tableNumber : '-', height: PosTextSize.size3, width: PosTextSize.size3);
|
||||
|
||||
// Order info — label : value, left aligned
|
||||
bytes += builder.orderInfoSimple(
|
||||
orderNumber: order.orderNumber,
|
||||
orderType: order.orderType,
|
||||
cashierName: cashierName,
|
||||
customerName: order.metadata['customer_name'] ?? '-',
|
||||
);
|
||||
|
||||
bytes += builder.separator();
|
||||
|
||||
// Items — qty NAMA (uppercase), variant indent
|
||||
for (final item in order.orderItems) {
|
||||
for (final item in order.orderItems.where((e) => e.printToChecker)) {
|
||||
final name = item.productName.toUpperCase();
|
||||
bytes += builder.itemText('${item.quantity} $name');
|
||||
bytes += builder.itemText('${item.quantityLabel} $name');
|
||||
if (item.productVariantName.isNotEmpty) {
|
||||
bytes += builder.itemText(' ${item.productVariantName.toUpperCase()}', bold: false);
|
||||
}
|
||||
@@ -132,7 +164,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,
|
||||
@@ -146,7 +178,7 @@ class PrintUi {
|
||||
bytes += generator.reset();
|
||||
|
||||
// Header
|
||||
bytes += builder.row2Columns('Kitchen', order.orderType, bold: true);
|
||||
bytes += builder.row2Columns('Kitchen', order.orderType.toUpperCase(), bold: true);
|
||||
bytes += builder.separator();
|
||||
bytes += builder.textCenter(
|
||||
'Table : ${order.tableNumber.isNotEmpty ? order.tableNumber : '-'}',
|
||||
@@ -158,13 +190,14 @@ class PrintUi {
|
||||
bytes += builder.orderInfoSimple(
|
||||
orderNumber: order.orderNumber,
|
||||
cashierName: cashierName,
|
||||
customerName: order.metadata['customer_name'] ?? '-',
|
||||
);
|
||||
|
||||
bytes += builder.separator();
|
||||
|
||||
// Single item
|
||||
final name = item.productName.toUpperCase();
|
||||
bytes += builder.itemText('${item.quantity} $name');
|
||||
bytes += builder.itemText('${item.quantityLabel} $name');
|
||||
if (item.productVariantName.isNotEmpty) {
|
||||
bytes += builder.itemText(' ${item.productVariantName.toUpperCase()}', bold: false);
|
||||
}
|
||||
@@ -187,7 +220,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,
|
||||
@@ -201,7 +234,7 @@ class PrintUi {
|
||||
bytes += generator.reset();
|
||||
|
||||
// Header
|
||||
bytes += builder.textCenter('Bar', bold: true);
|
||||
bytes += builder.row2Columns('Bar', order.orderType.toUpperCase(), bold: true);
|
||||
bytes += builder.separator();
|
||||
bytes += builder.textCenter(
|
||||
'Table : ${order.tableNumber.isNotEmpty ? order.tableNumber : '-'}',
|
||||
@@ -212,7 +245,7 @@ class PrintUi {
|
||||
// Order info
|
||||
bytes += builder.orderInfoSimple(
|
||||
orderNumber: order.orderNumber,
|
||||
orderType: order.orderType,
|
||||
customerName: order.metadata['customer_name'] ?? '-',
|
||||
cashierName: cashierName,
|
||||
);
|
||||
|
||||
@@ -220,7 +253,7 @@ class PrintUi {
|
||||
|
||||
// Single item
|
||||
final name = item.productName.toUpperCase();
|
||||
bytes += builder.itemText('${item.quantity} $name');
|
||||
bytes += builder.itemText('${item.quantityLabel} $name');
|
||||
if (item.productVariantName.isNotEmpty) {
|
||||
bytes += builder.itemText(' ${item.productVariantName.toUpperCase()}', bold: false);
|
||||
}
|
||||
@@ -242,7 +275,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,
|
||||
@@ -283,7 +316,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,
|
||||
@@ -299,6 +332,9 @@ class PrintUi {
|
||||
outletName: outlet.name,
|
||||
address: outlet.address,
|
||||
phoneNumber: outlet.phoneNumber,
|
||||
fontType: PosFontType.fontA,
|
||||
width: PosTextSize.size1,
|
||||
height: PosTextSize.size1,
|
||||
);
|
||||
|
||||
bytes += builder.orderInfo(
|
||||
@@ -309,9 +345,14 @@ class PrintUi {
|
||||
? null
|
||||
: order.payments.last.paymentMethodName,
|
||||
tableNumber: order.tableNumber,
|
||||
fontType: PosFontType.fontA,
|
||||
width: PosTextSize.size1,
|
||||
height: PosTextSize.size1,
|
||||
);
|
||||
|
||||
bytes += builder.orderType(order.orderType);
|
||||
bytes += builder.orderType(order.orderType, fontType: PosFontType.fontA,
|
||||
width: PosTextSize.size1,
|
||||
height: PosTextSize.size1,);
|
||||
|
||||
bytes += builder.emptyLines(1);
|
||||
|
||||
@@ -319,10 +360,14 @@ class PrintUi {
|
||||
bytes += builder.orderItem(
|
||||
productName: item.productName,
|
||||
quantity: item.quantity,
|
||||
quantityLabel: item.isWeighted ? item.quantityLabel : null,
|
||||
unitPrice: item.unitPrice.currencyFormatRpV2,
|
||||
totalPrice: item.totalPrice.currencyFormatRpV2,
|
||||
variantName: item.productVariantName,
|
||||
notes: item.notes,
|
||||
fontType: PosFontType.fontA,
|
||||
width: PosTextSize.size1,
|
||||
height: PosTextSize.size1,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -332,6 +377,9 @@ class PrintUi {
|
||||
discount: order.discountAmount.currencyFormatRpV2,
|
||||
total: order.totalAmount.currencyFormatRpV2,
|
||||
paid: order.totalPaid.currencyFormatRpV2,
|
||||
fontType: PosFontType.fontA,
|
||||
width: PosTextSize.size1,
|
||||
height: PosTextSize.size1,
|
||||
);
|
||||
|
||||
bytes += builder.footer(message: 'Kasir');
|
||||
@@ -347,7 +395,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,
|
||||
@@ -363,6 +411,9 @@ class PrintUi {
|
||||
outletName: outlet.name,
|
||||
address: outlet.address,
|
||||
phoneNumber: outlet.phoneNumber,
|
||||
fontType: PosFontType.fontA,
|
||||
width: PosTextSize.size1,
|
||||
height: PosTextSize.size1,
|
||||
);
|
||||
|
||||
bytes += builder.dateTime(DateTime.now());
|
||||
@@ -375,9 +426,14 @@ class PrintUi {
|
||||
? null
|
||||
: order.payments.last.paymentMethodName,
|
||||
tableNumber: order.tableNumber,
|
||||
fontType: PosFontType.fontA,
|
||||
width: PosTextSize.size1,
|
||||
height: PosTextSize.size1,
|
||||
);
|
||||
|
||||
bytes += builder.orderType('Void');
|
||||
bytes += builder.orderType('Void', fontType: PosFontType.fontA,
|
||||
width: PosTextSize.size1,
|
||||
height: PosTextSize.size1,);
|
||||
|
||||
bytes += builder.emptyLines(1);
|
||||
|
||||
@@ -385,10 +441,14 @@ class PrintUi {
|
||||
bytes += builder.orderItem(
|
||||
productName: item.productName,
|
||||
quantity: item.quantity,
|
||||
quantityLabel: item.isWeighted ? item.quantityLabel : null,
|
||||
unitPrice: item.unitPrice.currencyFormatRpV2,
|
||||
totalPrice: item.totalPrice.currencyFormatRpV2,
|
||||
variantName: item.productVariantName,
|
||||
notes: item.notes,
|
||||
fontType: PosFontType.fontA,
|
||||
width: PosTextSize.size1,
|
||||
height: PosTextSize.size1,
|
||||
);
|
||||
}
|
||||
bytes += builder.summary(
|
||||
@@ -397,6 +457,9 @@ class PrintUi {
|
||||
discount: order.discountAmount.currencyFormatRpV2,
|
||||
total: order.totalAmount.currencyFormatRpV2,
|
||||
paid: order.totalPaid.currencyFormatRpV2,
|
||||
fontType: PosFontType.fontA,
|
||||
width: PosTextSize.size1,
|
||||
height: PosTextSize.size1,
|
||||
);
|
||||
|
||||
bytes += builder.footer(message: 'Kasir');
|
||||
@@ -412,7 +475,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,
|
||||
@@ -428,9 +491,14 @@ class PrintUi {
|
||||
outletName: outlet.name,
|
||||
address: outlet.address,
|
||||
phoneNumber: outlet.phoneNumber,
|
||||
fontType: PosFontType.fontA,
|
||||
width: PosTextSize.size1,
|
||||
height: PosTextSize.size1,
|
||||
);
|
||||
|
||||
bytes += builder.dateTime(DateTime.now());
|
||||
bytes += builder.dateTime(DateTime.now(), fontType: PosFontType.fontA,
|
||||
width: PosTextSize.size1,
|
||||
height: PosTextSize.size1,);
|
||||
|
||||
bytes += builder.orderInfo(
|
||||
orderNumber: order.orderNumber,
|
||||
@@ -440,13 +508,21 @@ class PrintUi {
|
||||
? null
|
||||
: order.payments.last.paymentMethodName,
|
||||
tableNumber: order.tableNumber,
|
||||
fontType: PosFontType.fontA,
|
||||
width: PosTextSize.size1,
|
||||
height: PosTextSize.size1,
|
||||
);
|
||||
|
||||
bytes += builder.orderType('Split');
|
||||
bytes += builder.orderType('Split', fontType: PosFontType.fontA,
|
||||
width: PosTextSize.size1,
|
||||
height: PosTextSize.size1,);
|
||||
|
||||
bytes += builder.row2Columns(
|
||||
'Split',
|
||||
'${order.payments.last.splitNumber} / ${order.payments.last.splitTotal}',
|
||||
fontType: PosFontType.fontA,
|
||||
width: PosTextSize.size1,
|
||||
height: PosTextSize.size1,
|
||||
);
|
||||
|
||||
bytes += builder.separator();
|
||||
@@ -457,10 +533,14 @@ class PrintUi {
|
||||
bytes += builder.orderItem(
|
||||
productName: item.productName,
|
||||
quantity: item.quantity,
|
||||
quantityLabel: item.isWeighted ? item.quantityLabel : null,
|
||||
unitPrice: item.unitPrice.currencyFormatRpV2,
|
||||
totalPrice: item.totalPrice.currencyFormatRpV2,
|
||||
variantName: item.productVariantName,
|
||||
notes: item.notes,
|
||||
fontType: PosFontType.fontA,
|
||||
width: PosTextSize.size1,
|
||||
height: PosTextSize.size1,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -472,6 +552,9 @@ class PrintUi {
|
||||
discount: 0.currencyFormatRpV2,
|
||||
total: order.payments.last.amount.currencyFormatRpV2,
|
||||
paid: order.payments.last.amount.currencyFormatRpV2,
|
||||
fontType: PosFontType.fontA,
|
||||
width: PosTextSize.size1,
|
||||
height: PosTextSize.size1,
|
||||
);
|
||||
|
||||
bytes += builder.footer(message: 'Terima Kasih');
|
||||
|
||||
@@ -9,26 +9,26 @@ class ReceiptComponentBuilder {
|
||||
|
||||
ReceiptComponentBuilder({required this.generator, this.paperSize = 58});
|
||||
|
||||
/// Helper: returns size2 for 80mm paper, size1 for 58mm
|
||||
PosTextSize get _titleSize =>
|
||||
paperSize == 80 ? PosTextSize.size3 : PosTextSize.size1;
|
||||
|
||||
/// Font type per paper size — easy to change here
|
||||
/// 58mm → fontA, 80mm → fontA
|
||||
PosFontType get _font =>
|
||||
paperSize == 80 ? PosFontType.fontA : PosFontType.fontA;
|
||||
paperSize == 80 ? PosFontType.fontB : PosFontType.fontA;
|
||||
|
||||
/// Text size for body — height size2 for 80mm to appear taller, size1 for 58mm
|
||||
PosTextSize get _bodySize =>
|
||||
PosTextSize get _bodyHeight =>
|
||||
paperSize == 80 ? PosTextSize.size2 : PosTextSize.size1;
|
||||
|
||||
/// Body width stays size1 always to prevent overflow
|
||||
PosTextSize get _bodyWidth => PosTextSize.size1;
|
||||
PosTextSize get _bodyWidth => paperSize == 80 ? PosTextSize.size2 : PosTextSize.size1;
|
||||
|
||||
/// Characters per line based on paper size (always size1 font)
|
||||
String get _separatorLine => paperSize == 80
|
||||
? '------------------------------------------------'
|
||||
/// Characters per line based on paper size and font
|
||||
String get _separatorLine {
|
||||
if (paperSize == 80) {
|
||||
return _font == PosFontType.fontB
|
||||
? '----------------------------------------------------------------'
|
||||
: '------------------------------------------------';
|
||||
} else {
|
||||
return _font == PosFontType.fontB
|
||||
? '------------------------------------------'
|
||||
: '--------------------------------';
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Basic text
|
||||
@@ -37,43 +37,48 @@ class ReceiptComponentBuilder {
|
||||
List<int> textCenter(
|
||||
String text, {
|
||||
bool bold = false,
|
||||
PosTextSize height = PosTextSize.size1,
|
||||
PosTextSize width = PosTextSize.size1,
|
||||
PosTextSize? height,
|
||||
PosTextSize? width,
|
||||
PosFontType? fontType,
|
||||
}) {
|
||||
return generator.text(
|
||||
text,
|
||||
styles: PosStyles(
|
||||
bold: bold,
|
||||
align: PosAlign.center,
|
||||
height: height,
|
||||
width: width,
|
||||
fontType: _font,
|
||||
height: height ?? _bodyHeight,
|
||||
width: width ?? _bodyWidth,
|
||||
fontType: fontType ?? _font,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<int> textLeft(String text, {bool bold = false}) {
|
||||
List<int> textLeft(String text, {bool bold = false, PosTextSize? height,
|
||||
PosTextSize? width,
|
||||
PosFontType? fontType,}) {
|
||||
return generator.text(
|
||||
text,
|
||||
styles: PosStyles(
|
||||
bold: bold,
|
||||
align: PosAlign.left,
|
||||
fontType: _font,
|
||||
height: _bodySize,
|
||||
width: _bodyWidth,
|
||||
height: height ?? _bodyHeight,
|
||||
width: width ?? _bodyWidth,
|
||||
fontType: fontType ?? _font,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<int> textRight(String text, {bool bold = false}) {
|
||||
List<int> textRight(String text, {bool bold = false, PosTextSize? height,
|
||||
PosTextSize? width,
|
||||
PosFontType? fontType,}) {
|
||||
return generator.text(
|
||||
text,
|
||||
styles: PosStyles(
|
||||
bold: bold,
|
||||
align: PosAlign.right,
|
||||
fontType: _font,
|
||||
height: _bodySize,
|
||||
width: _bodyWidth,
|
||||
height: height ?? _bodyHeight,
|
||||
width: width ?? _bodyWidth,
|
||||
fontType: fontType ?? _font,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -101,6 +106,9 @@ class ReceiptComponentBuilder {
|
||||
bool bold = false,
|
||||
int leftWidth = 6,
|
||||
int rightWidth = 6,
|
||||
PosTextSize? height,
|
||||
PosTextSize? width,
|
||||
PosFontType? fontType,
|
||||
}) {
|
||||
return generator.row([
|
||||
PosColumn(
|
||||
@@ -109,9 +117,9 @@ class ReceiptComponentBuilder {
|
||||
styles: PosStyles(
|
||||
align: PosAlign.left,
|
||||
bold: bold,
|
||||
fontType: _font,
|
||||
height: _bodySize,
|
||||
width: _bodyWidth,
|
||||
height: height ?? _bodyHeight,
|
||||
width: width ?? _bodyWidth,
|
||||
fontType: fontType ?? _font,
|
||||
),
|
||||
),
|
||||
PosColumn(
|
||||
@@ -120,9 +128,9 @@ class ReceiptComponentBuilder {
|
||||
styles: PosStyles(
|
||||
align: PosAlign.right,
|
||||
bold: bold,
|
||||
fontType: _font,
|
||||
height: _bodySize,
|
||||
width: _bodyWidth,
|
||||
height: height ?? _bodyHeight,
|
||||
width: width ?? _bodyWidth,
|
||||
fontType: fontType ?? _font,
|
||||
),
|
||||
),
|
||||
]);
|
||||
@@ -163,8 +171,8 @@ class ReceiptComponentBuilder {
|
||||
bold: bold,
|
||||
align: PosAlign.left,
|
||||
fontType: _font,
|
||||
height: PosTextSize.size2,
|
||||
width: PosTextSize.size2,
|
||||
height: _bodyHeight,
|
||||
width: _bodyWidth,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -182,25 +190,34 @@ class ReceiptComponentBuilder {
|
||||
required String outletName,
|
||||
required String address,
|
||||
required String phoneNumber,
|
||||
PosTextSize? height,
|
||||
PosTextSize? width,
|
||||
PosFontType? fontType,
|
||||
}) {
|
||||
List<int> bytes = [];
|
||||
bytes += textCenter(outletName, bold: true, height: _titleSize, width: _titleSize);
|
||||
bytes += textCenter(address);
|
||||
bytes += textCenter(phoneNumber);
|
||||
bytes += textCenter(outletName, height: PosTextSize.size2, width: PosTextSize.size2, fontType: fontType, bold: true);
|
||||
bytes += textCenter(address, fontType: fontType, height: height ?? _bodyHeight, width: width ?? _bodyWidth);
|
||||
bytes += textCenter(phoneNumber, fontType: fontType, height: height ?? _bodyHeight, width: width ?? _bodyWidth);
|
||||
bytes += separator();
|
||||
return bytes;
|
||||
}
|
||||
|
||||
/// Centered printer type label (e.g. KITCHEN, BAR)
|
||||
List<int> printerType({required String printerType}) {
|
||||
return textCenter(printerType, bold: true, height: _titleSize, width: _titleSize);
|
||||
return textCenter(printerType, bold: true);
|
||||
}
|
||||
|
||||
/// Date + time row (receipt style)
|
||||
List<int> dateTime(DateTime dateTime) {
|
||||
List<int> dateTime(DateTime dateTime, { PosTextSize? height,
|
||||
PosTextSize? width,
|
||||
PosFontType? fontType,
|
||||
}) {
|
||||
return row2Columns(
|
||||
DateFormat('dd MMM yyyy').format(dateTime),
|
||||
DateFormat('HH:mm').format(dateTime),
|
||||
height: height ?? _bodyHeight,
|
||||
width: width ?? _bodyWidth,
|
||||
fontType: fontType ?? _font,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -209,13 +226,19 @@ class ReceiptComponentBuilder {
|
||||
required String orderNumber,
|
||||
String? orderType,
|
||||
required String cashierName,
|
||||
String? customerName,
|
||||
}) {
|
||||
List<int> bytes = [];
|
||||
final dateStr = DateFormat('dd-MM-yyyy HH:mm').format(DateTime.now());
|
||||
bytes += textLeft('Order : $orderNumber');
|
||||
bytes += textLeft('Date : $dateStr');
|
||||
|
||||
|
||||
if(customerName != null) {
|
||||
bytes += textLeft('Customer : $customerName');
|
||||
}
|
||||
if(orderType != null) {
|
||||
bytes += textLeft('Purpose : $orderType');
|
||||
bytes += textLeft('Purpose : ${orderType.toUpperCase()}');
|
||||
}
|
||||
bytes += textLeft('Waiter : $cashierName');
|
||||
return bytes;
|
||||
@@ -228,29 +251,48 @@ class ReceiptComponentBuilder {
|
||||
required String cashierName,
|
||||
String? paymentMethod,
|
||||
String? tableNumber,
|
||||
PosTextSize? height,
|
||||
PosTextSize? width,
|
||||
PosFontType? fontType,
|
||||
}) {
|
||||
List<int> bytes = [];
|
||||
final dateStr = DateFormat('dd-MM-yyyy HH:mm').format(DateTime.now());
|
||||
bytes += textLeft('Order : $orderNumber');
|
||||
bytes += textLeft('Date : $dateStr');
|
||||
bytes += textLeft('Order : $orderNumber', fontType: fontType, height: height ?? _bodyHeight, width: width ?? _bodyWidth);
|
||||
bytes += textLeft('Date : $dateStr', fontType: fontType, height: height ?? _bodyHeight, width: width ?? _bodyWidth);
|
||||
if (tableNumber != null && tableNumber.isNotEmpty) {
|
||||
bytes += textLeft('Table : $tableNumber');
|
||||
bytes += textLeft('Table : $tableNumber', fontType: fontType, height: height ?? _bodyHeight, width: width ?? _bodyWidth);
|
||||
}
|
||||
bytes += textLeft('Waiter : $cashierName');
|
||||
bytes += textLeft('Customer : $customerName');
|
||||
bytes += textLeft('Waiter : $cashierName', fontType: fontType, height: height ?? _bodyHeight, width: width ?? _bodyWidth);
|
||||
bytes += textLeft('Customer : $customerName', fontType: fontType, height: height ?? _bodyHeight, width: width ?? _bodyWidth);
|
||||
|
||||
if (paymentMethod != null) {
|
||||
bytes += textLeft('Payment : $paymentMethod');
|
||||
bytes += textLeft('Payment : $paymentMethod', fontType: fontType, height: height ?? _bodyHeight, width: width ?? _bodyWidth);
|
||||
}
|
||||
|
||||
return bytes;
|
||||
}
|
||||
|
||||
/// Order type banner (separator + type + separator)
|
||||
List<int> orderType(String type) {
|
||||
List<int> orderType(String type, {
|
||||
PosTextSize? height,
|
||||
PosTextSize? width,
|
||||
PosFontType? fontType,
|
||||
}) {
|
||||
List<int> bytes = [];
|
||||
bytes += separator();
|
||||
bytes += textCenter(type, bold: true, height: _titleSize, width: _titleSize);
|
||||
bytes += textCenter(type.toUpperCase(), fontType: fontType, height: height ?? _bodyHeight, width: width ?? _bodyWidth, bold: true);
|
||||
bytes += separator();
|
||||
return bytes;
|
||||
}
|
||||
|
||||
List<int> tableName(String table, {
|
||||
PosTextSize? height,
|
||||
PosTextSize? width,
|
||||
PosFontType? fontType,
|
||||
}) {
|
||||
List<int> bytes = [];
|
||||
bytes += separator();
|
||||
bytes += textCenter( 'Table : ${table.isNotEmpty ? table : '-'}', fontType: fontType, height: height ?? _bodyHeight, width: width ?? _bodyWidth, bold: true);
|
||||
bytes += separator();
|
||||
return bytes;
|
||||
}
|
||||
@@ -259,19 +301,29 @@ class ReceiptComponentBuilder {
|
||||
List<int> orderItem({
|
||||
required String productName,
|
||||
required int quantity,
|
||||
|
||||
/// Untuk baris berbobot, isi dengan "4,2 ons" — angka quantity-nya selalu
|
||||
/// 1 dan akan salah dibaca di struk.
|
||||
String? quantityLabel,
|
||||
required String unitPrice,
|
||||
required String totalPrice,
|
||||
String? variantName,
|
||||
String? notes,
|
||||
PosTextSize? height,
|
||||
PosTextSize? width,
|
||||
PosFontType? fontType,
|
||||
}) {
|
||||
List<int> bytes = [];
|
||||
final displayName = (variantName != null && variantName.isNotEmpty)
|
||||
? '$productName ($variantName)'
|
||||
: productName;
|
||||
bytes += textLeft(displayName, bold: paperSize == 80);
|
||||
bytes += row2Columns('$quantity x $unitPrice', totalPrice, leftWidth: 8, rightWidth: 4);
|
||||
bytes += textLeft(displayName, bold: true, fontType: fontType, height: height ?? _bodyHeight, width: width ?? _bodyWidth);
|
||||
final quantityText = quantityLabel != null && quantityLabel.isNotEmpty
|
||||
? '$quantityLabel x $unitPrice'
|
||||
: '${quantity}x $unitPrice';
|
||||
bytes += row2Columns(quantityText, totalPrice, fontType: fontType, height: height ?? _bodyHeight, width: width ?? _bodyWidth,);
|
||||
if (notes != null && notes.isNotEmpty) {
|
||||
bytes += row2Columns('Note', notes, leftWidth: 4, rightWidth: 8);
|
||||
bytes += row2Columns('Note', notes, fontType: fontType, height: height ?? _bodyHeight, width: width ?? _bodyWidth, leftWidth: 4, rightWidth: 8);
|
||||
}
|
||||
bytes += emptyLines(1);
|
||||
return bytes;
|
||||
@@ -284,17 +336,20 @@ class ReceiptComponentBuilder {
|
||||
required String discount,
|
||||
required String total,
|
||||
required String paid,
|
||||
PosTextSize? height,
|
||||
PosTextSize? width,
|
||||
PosFontType? fontType,
|
||||
}) {
|
||||
List<int> bytes = [];
|
||||
bytes += separator();
|
||||
if (totalItems > 0) {
|
||||
bytes += row2Columns('Total Item', totalItems.toString());
|
||||
bytes += row2Columns('Total Item', totalItems.toString(), fontType: fontType, height: height ?? _bodyHeight, width: width ?? _bodyWidth,);
|
||||
}
|
||||
bytes += row2Columns('Subtotal', subtotal);
|
||||
bytes += row2Columns('Diskon', discount);
|
||||
bytes += row2Columns('Subtotal', subtotal, fontType: fontType, height: height ?? _bodyHeight, width: width ?? _bodyWidth,);
|
||||
bytes += row2Columns('Diskon', discount, fontType: fontType, height: height ?? _bodyHeight, width: width ?? _bodyWidth,);
|
||||
bytes += separator();
|
||||
bytes += row2Columns('Total', total, bold: true);
|
||||
bytes += row2Columns('Bayar', paid);
|
||||
bytes += row2Columns('Total', total, bold: true, fontType: fontType, height: height ?? _bodyHeight, width: width ?? _bodyWidth,);
|
||||
bytes += row2Columns('Bayar', paid, fontType: fontType, height: height ?? _bodyHeight, width: width ?? _bodyWidth,);
|
||||
bytes += separator();
|
||||
return bytes;
|
||||
}
|
||||
@@ -303,7 +358,7 @@ class ReceiptComponentBuilder {
|
||||
List<int> footer({String message = 'Terima kasih'}) {
|
||||
List<int> bytes = [];
|
||||
bytes += emptyLines(2);
|
||||
bytes += textCenter(message, bold: true, height: _titleSize, width: _titleSize);
|
||||
bytes += textCenter(message, bold: true);
|
||||
if (kDebugMode) {
|
||||
bytes += textCenter('$paperSize MM');
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import '../../../application/order/order_form/order_form_bloc.dart';
|
||||
import '../../../application/payment_method/payment_method_loader/payment_method_loader_bloc.dart';
|
||||
import '../../../application/table/table_loader/table_loader_bloc.dart';
|
||||
import '../../../common/theme/theme.dart';
|
||||
import '../../../domain/product/product.dart';
|
||||
import '../../../injection.dart';
|
||||
import '../../components/spaces/space.dart';
|
||||
import '../../components/toast/flushbar.dart';
|
||||
@@ -86,14 +87,7 @@ class CheckoutPage extends StatelessWidget implements AutoRouteWrapper {
|
||||
tag: 'checkout_screen',
|
||||
child: BlocBuilder<CheckoutFormBloc, CheckoutFormState>(
|
||||
builder: (context, state) {
|
||||
final price = state.items.fold(
|
||||
0,
|
||||
(previousValue, element) =>
|
||||
previousValue +
|
||||
(element.product.price.toInt() +
|
||||
(element.variant?.priceModifier.toInt() ?? 0)) *
|
||||
element.quantity,
|
||||
);
|
||||
final price = state.items.cartTotal.toInt();
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: AppColor.white,
|
||||
|
||||
@@ -7,6 +7,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../../../../../application/checkout/checkout_form/checkout_form_bloc.dart';
|
||||
import '../../../../../../common/extension/extension.dart';
|
||||
import '../../../../../../common/theme/theme.dart';
|
||||
import '../../../../../../domain/product/product.dart';
|
||||
import '../../../../../components/button/button.dart';
|
||||
import '../../../../../components/card/order_menu.dart';
|
||||
import '../../../../../components/spaces/space.dart';
|
||||
@@ -21,15 +22,7 @@ class HomeRightPanel extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<CheckoutFormBloc, CheckoutFormState>(
|
||||
builder: (context, state) {
|
||||
final price = state.items.isEmpty
|
||||
? 0.0
|
||||
: state.items
|
||||
.map(
|
||||
(e) =>
|
||||
(e.product.price + (e.variant?.priceModifier ?? 0)) *
|
||||
e.quantity,
|
||||
)
|
||||
.reduce((value, element) => value + element);
|
||||
final price = state.items.cartTotal;
|
||||
log('🛒 Total Price: $price');
|
||||
return Align(
|
||||
alignment: Alignment.topCenter,
|
||||
@@ -87,8 +80,12 @@ class HomeRightPanel extends StatelessWidget {
|
||||
: ListView.separated(
|
||||
shrinkWrap: true,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
itemBuilder: (context, index) =>
|
||||
OrderMenu(data: state.items[index]),
|
||||
itemBuilder: (context, index) => OrderMenu(
|
||||
// Baris berbobot bisa dihapus satu per satu, jadi
|
||||
// state tiap baris harus terikat ke identitasnya.
|
||||
key: ValueKey(state.items[index].lineId),
|
||||
data: state.items[index],
|
||||
),
|
||||
separatorBuilder: (context, index) =>
|
||||
const SpaceHeight(1.0),
|
||||
itemCount: state.items.length,
|
||||
|
||||
@@ -32,6 +32,8 @@ class SettingPrinterForm extends StatefulWidget {
|
||||
|
||||
class _SettingPrinterFormState extends State<SettingPrinterForm> {
|
||||
final TextEditingController _nameController = TextEditingController();
|
||||
final TextEditingController _networkController = TextEditingController();
|
||||
|
||||
void setup() {
|
||||
if (widget.printer != null) {
|
||||
context.read<PrinterFormBloc>().add(
|
||||
@@ -50,6 +52,7 @@ class _SettingPrinterFormState extends State<SettingPrinterForm> {
|
||||
PrinterFormEvent.paperChanged(widget.printer!.paper),
|
||||
);
|
||||
_nameController.text = widget.printer!.name;
|
||||
_networkController.text = widget.printer!.address;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,6 +166,7 @@ class _SettingPrinterFormState extends State<SettingPrinterForm> {
|
||||
)
|
||||
: AppTextFormField(
|
||||
label: 'Network',
|
||||
controller: _networkController,
|
||||
onChanged: (value) {
|
||||
context.read<PrinterFormBloc>().add(
|
||||
PrinterFormEvent.addressChanged(value),
|
||||
|
||||
@@ -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(
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user