feat: dukung penjualan produk per timbangan

Produk kini punya cara jual `unit` (cacah) atau `weight` (timbangan),
mengikuti kontrak backend di docs/integration-weight-based-products.md.

- tipe SellBy, domain/infrastructure unit baru, dan endpoint /api/v1/units
- dialog input berat saat produk timbangan ditambahkan ke keranjang
- tiap penimbangan jadi baris keranjang sendiri; total baris dihitung
  berat x harga satuan, sedangkan produk cacah tetap digabung
- request ke backend mengirim quantity 1 dan membawa berat
- migrasi DB v3: kolom sell_by, unit_id, unit_name, unit_abbreviation
  di tabel products supaya satuan tetap tampil saat offline
- tampilan keranjang, struk, split bill, void, dan refund memakai berat
- test/weight_based_products_test.dart menutup perilaku di atas (9 test)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
efrilm
2026-09-06 23:03:31 +07:00
co-authored by Claude Opus 5
parent 9777f63426
commit 1505a83dd3
72 changed files with 5388 additions and 584 deletions
+279
View File
@@ -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 'package:injectable/injectable.dart';
import '../../../common/service/device_info_service.dart'; import '../../../common/service/device_info_service.dart';
import '../../../common/service/unit_registry.dart';
import '../../../common/service/fcm_service.dart'; import '../../../common/service/fcm_service.dart';
import '../../../domain/auth/auth.dart'; import '../../../domain/auth/auth.dart';
@@ -18,11 +19,13 @@ class LoginFormBloc extends Bloc<LoginFormEvent, LoginFormState> {
final IAuthRepository _authRepository; final IAuthRepository _authRepository;
final FcmService _fcmService; final FcmService _fcmService;
final DeviceInfoService _deviceInfoService; final DeviceInfoService _deviceInfoService;
final UnitRegistry _unitRegistry;
LoginFormBloc( LoginFormBloc(
this._authRepository, this._authRepository,
this._fcmService, this._fcmService,
this._deviceInfoService, this._deviceInfoService,
this._unitRegistry,
) : super(LoginFormState.initial()) { ) : super(LoginFormState.initial()) {
on<LoginFormEvent>(_onLoginFormEvent); on<LoginFormEvent>(_onLoginFormEvent);
} }
@@ -62,6 +65,12 @@ class LoginFormBloc extends Bloc<LoginFormEvent, LoginFormState> {
devicePayload: devicePayload, 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( emit(
state.copyWith( state.copyWith(
isSubmitting: false, isSubmitting: false,
@@ -3,6 +3,7 @@ import 'dart:developer';
import 'package:bloc/bloc.dart'; import 'package:bloc/bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:injectable/injectable.dart' hide Order; import 'package:injectable/injectable.dart' hide Order;
import 'package:uuid/uuid.dart';
import '../../../common/types/order_type.dart'; import '../../../common/types/order_type.dart';
import '../../../domain/delivery/delivery.dart'; import '../../../domain/delivery/delivery.dart';
@@ -19,6 +20,33 @@ class CheckoutFormBloc extends Bloc<CheckoutFormEvent, CheckoutFormState> {
CheckoutFormBloc() : super(CheckoutFormState.initial()) { CheckoutFormBloc() : super(CheckoutFormState.initial()) {
on<CheckoutFormEvent>(_onCheckoutFormEvent); 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( Future<void> _onCheckoutFormEvent(
CheckoutFormEvent event, CheckoutFormEvent event,
Emitter<CheckoutFormState> emit, Emitter<CheckoutFormState> emit,
@@ -26,12 +54,21 @@ class CheckoutFormBloc extends Bloc<CheckoutFormEvent, CheckoutFormState> {
return event.map( return event.map(
addItem: (e) async { addItem: (e) async {
final currentState = state; 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)); emit(currentState.copyWith(isLoading: true));
List<ProductQuantity> items = [...currentState.items]; List<ProductQuantity> items = [...currentState.items];
final index = items.indexWhere( final index = items.indexWhere(
(element) => (element) =>
!element.isWeighted &&
element.product.id == e.product.id && element.product.id == e.product.id &&
element.variant?.id == e.variant?.id, element.variant?.id == e.variant?.id,
); );
@@ -49,35 +86,47 @@ class CheckoutFormBloc extends Bloc<CheckoutFormEvent, CheckoutFormState> {
quantity: 1, quantity: 1,
variant: e.variant, variant: e.variant,
notes: '', notes: '',
lineId: const Uuid().v4(),
), ),
); );
} }
log('🛒 Items updated: ${items.length} items total ${items.toList()}'); log('🛒 Items updated: ${items.length} items total ${items.toList()}');
final totalQuantity = items.fold<int>( emit(_recalculate(currentState, items));
0, },
(sum, item) => sum + item.quantity, addWeightedItem: (e) async {
); final currentState = state;
emit(currentState.copyWith(isLoading: true));
final totalPrice = state.items.isEmpty // Sengaja tidak digabung dengan baris lain: dua penimbangan adalah dua
? 0.0 // ikan berbeda, masing-masing bisa di-void atau dibayar terpisah.
: state.items final items = [
.map( ...currentState.items,
(e) => ProductQuantity(
(e.product.price * e.quantity) + product: e.product,
(e.variant?.priceModifier ?? 0), variant: e.variant,
) quantity: 1,
.reduce((value, element) => value + element); notes: '',
lineId: const Uuid().v4(),
emit( weight: e.weight,
currentState.copyWith(
items: items,
totalQuantity: totalQuantity,
totalPrice: totalPrice.toInt(),
isLoading: false,
), ),
); ];
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 { removeItem: (e) async {
final currentState = state; final currentState = state;
@@ -86,6 +135,7 @@ class CheckoutFormBloc extends Bloc<CheckoutFormEvent, CheckoutFormState> {
final index = items.indexWhere( final index = items.indexWhere(
(element) => (element) =>
!element.isWeighted &&
element.product.id == e.product.id && element.product.id == e.product.id &&
element.variant?.id == e.variant?.id, element.variant?.id == e.variant?.id,
); );
@@ -103,32 +153,18 @@ class CheckoutFormBloc extends Bloc<CheckoutFormEvent, CheckoutFormState> {
} }
} }
final totalQuantity = items.fold<int>( log('🗑️ Item removed. Total items: ${items.length}');
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( emit(_recalculate(currentState, items));
'🗑️ Item removed. Total items: ${items.length}, totalQuantity: $totalQuantity, totalPrice: $totalPrice', },
); removeLine: (e) async {
final currentState = state;
final items = [...currentState.items]
..removeWhere((item) => item.lineId == e.lineId);
// Emit state baru log('🗑️ Baris dihapus. Sisa ${items.length} baris');
emit(
currentState.copyWith( emit(_recalculate(currentState, items));
items: items,
totalQuantity: totalQuantity,
totalPrice: totalPrice,
isLoading: false,
),
);
}, },
started: (e) async { started: (e) async {
emit(CheckoutFormState.initial().copyWith(isLoading: true)); 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; 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 items = [...currentState.items];
final index = items.indexWhere( final index = items.indexWhere((item) => item.lineId == e.lineId);
(element) => element.product.id == e.product.id,
);
if (index != -1) { if (index != -1) {
items[index] = items[index].copyWith(notes: e.notes); items[index] = items[index].copyWith(notes: e.notes);
@@ -181,8 +216,8 @@ class CheckoutFormBloc extends Bloc<CheckoutFormEvent, CheckoutFormState> {
emit(state.copyWith(orderAdded: e.order)); emit(state.copyWith(orderAdded: e.order));
}, },
customerNameChanged: (e) async { 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, ProductVariant? variant,
) = _AddItem; ) = _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( const factory CheckoutFormEvent.removeItem(
Product product, Product product,
ProductVariant? variant, ProductVariant? variant,
) = _RemoveItem; ) = _RemoveItem;
const factory CheckoutFormEvent.updateItemNotes( /// Hapus satu baris utuh — satu-satunya cara membatalkan baris berbobot.
Product product, const factory CheckoutFormEvent.removeLine(String lineId) = _RemoveLine;
const factory CheckoutFormEvent.updateLineNotes(
String lineId,
String notes, String notes,
) = _UpdateItemNotes; ) = _UpdateLineNotes;
const factory CheckoutFormEvent.updateOrderType(OrderType orderType) = const factory CheckoutFormEvent.updateOrderType(OrderType orderType) =
_UpdateOrderType; _UpdateOrderType;
@@ -30,6 +47,6 @@ class CheckoutFormEvent with _$CheckoutFormEvent {
const factory CheckoutFormEvent.orderAddedItems(Order? order) = const factory CheckoutFormEvent.orderAddedItems(Order? order) =
_OrderAddedItems; _OrderAddedItems;
const factory CheckoutFormEvent.customerNameChanged(String name) = const factory CheckoutFormEvent.customerNameChanged(String name) =
_CustomerNameChanged; _CustomerNameChanged;
} }
@@ -60,15 +60,7 @@ class OrderFormBloc extends Bloc<OrderFormEvent, OrderFormState> {
notes: '', notes: '',
customerName: state.customerName ?? "", customerName: state.customerName ?? "",
orderItems: e.items orderItems: e.items
.map( .map(OrderItemRequest.fromProductQuantity)
(item) => OrderItemRequest(
productId: item.product.id,
productVariantId: item.variant?.id ?? "",
quantity: item.quantity,
unitPrice: item.product.price.toInt(),
notes: item.notes,
),
)
.toList(), .toList(),
); );
@@ -100,15 +92,7 @@ class OrderFormBloc extends Bloc<OrderFormEvent, OrderFormState> {
notes: '', notes: '',
customerName: state.customerName ?? "", customerName: state.customerName ?? "",
orderItems: e.items orderItems: e.items
.map( .map(OrderItemRequest.fromProductQuantity)
(item) => OrderItemRequest(
productId: item.product.id,
productVariantId: item.variant?.id ?? "",
quantity: item.quantity,
unitPrice: item.product.price.toInt(),
notes: item.notes,
),
)
.toList(), .toList(),
); );
@@ -137,15 +121,7 @@ class OrderFormBloc extends Bloc<OrderFormEvent, OrderFormState> {
); );
final request = e.items final request = e.items
.map( .map(AddItemOrderRequest.fromProductQuantity)
(item) => AddItemOrderRequest(
productId: item.product.id,
productVariantId: item.variant?.id ?? '',
quantity: item.quantity,
unitPrice: item.product.price.toInt(),
notes: item.notes,
),
)
.toList(); .toList();
failureOrAddItemOrder = await _repository.addItemOrder( failureOrAddItemOrder = await _repository.addItemOrder(
@@ -67,7 +67,11 @@ class SplitBillFormBloc extends Bloc<SplitBillFormEvent, SplitBillFormState> {
final item = state.pendingItems.firstWhere( final item = state.pendingItems.firstWhere(
(item) => item.id == entry.key, (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( emit(
@@ -3,6 +3,7 @@ import 'package:dartz/dartz.dart';
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:injectable/injectable.dart'; import 'package:injectable/injectable.dart';
import '../../../common/service/unit_registry.dart';
import '../../../domain/category/category.dart'; import '../../../domain/category/category.dart';
import '../../../domain/product/product.dart'; import '../../../domain/product/product.dart';
@@ -14,9 +15,13 @@ part 'sync_setting_bloc.freezed.dart';
class SyncSettingBloc extends Bloc<SyncSettingEvent, SyncSettingState> { class SyncSettingBloc extends Bloc<SyncSettingEvent, SyncSettingState> {
final IProductRepository _productRepository; final IProductRepository _productRepository;
final ICategoryRepository _categoryRepository; final ICategoryRepository _categoryRepository;
final UnitRegistry _unitRegistry;
SyncSettingBloc(this._productRepository, this._categoryRepository) SyncSettingBloc(
: super(SyncSettingState.initial()) { this._productRepository,
this._categoryRepository,
this._unitRegistry,
) : super(SyncSettingState.initial()) {
on<SyncSettingEvent>(_onSyncSettingEvent); 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 // Sync categories first
final categoryResult = await _categoryRepository.syncAllCategories(); final categoryResult = await _categoryRepository.syncAllCategories();
@@ -169,6 +178,8 @@ class SyncSettingBloc extends Bloc<SyncSettingEvent, SyncSettingState> {
Future<void> _onSyncProducts(Emitter<SyncSettingState> emit) async { Future<void> _onSyncProducts(Emitter<SyncSettingState> emit) async {
emit(state.copyWith(isLoading: true, failureOptionSyncProduct: none())); emit(state.copyWith(isLoading: true, failureOptionSyncProduct: none()));
await _unitRegistry.load(forceRefresh: true);
final result = await _productRepository.syncAllProducts(); final result = await _productRepository.syncAllProducts();
result.fold( result.fold(
@@ -55,7 +55,10 @@ class VoidFormBloc extends Bloc<VoidFormEvent, VoidFormState> {
(item) => item.id == entry.key, (item) => item.id == entry.key,
); );
if (item == null) continue; 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( emit(
@@ -92,7 +95,13 @@ class VoidFormBloc extends Bloc<VoidFormEvent, VoidFormState> {
productVariantName: originalItem.productVariantName, productVariantName: originalItem.productVariantName,
quantity: voidQty, quantity: voidQty,
unitPrice: originalItem.unitPrice, 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, modifiers: originalItem.modifiers,
notes: originalItem.notes, notes: originalItem.notes,
status: originalItem.status, status: originalItem.status,
@@ -2,4 +2,5 @@ class LocalStorageKey {
static const String token = 'token'; static const String token = 'token';
static const String user = 'user'; static const String user = 'user';
static const String outlet = 'outlet'; static const String outlet = 'outlet';
static const String units = 'units';
} }
+17 -2
View File
@@ -17,7 +17,7 @@ class DatabaseHelper {
return await openDatabase( return await openDatabase(
path, path,
version: 2, version: 3,
onCreate: _onCreate, onCreate: _onCreate,
onUpgrade: _onUpgrade, onUpgrade: _onUpgrade,
); );
@@ -42,7 +42,11 @@ class DatabaseHelper {
metadata TEXT, metadata TEXT,
is_active INTEGER, is_active INTEGER,
created_at TEXT, created_at TEXT,
updated_at TEXT updated_at TEXT,
sell_by TEXT DEFAULT 'unit',
unit_id TEXT,
unit_name TEXT,
unit_abbreviation TEXT
) )
'''); ''');
@@ -114,6 +118,17 @@ class DatabaseHelper {
'ALTER TABLE products ADD COLUMN print_to_checker INTEGER DEFAULT 0', '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 { Future<void> close() async {
@@ -13,3 +13,10 @@ extension DoubleExt on double {
decimalDigits: 0, decimalDigits: 0,
).format(this); ).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);
}
+22
View File
@@ -75,3 +75,25 @@ int generateRandomNumber() {
final random = Random(); final random = Random();
return 1 + random.nextInt(10000); 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');
}
+48
View File
@@ -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 = {};
}
+19
View File
@@ -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;
}
+1
View File
@@ -4,6 +4,7 @@ class ApiPath {
static const String outlets = '/api/v1/outlets'; static const String outlets = '/api/v1/outlets';
static const String categories = '/api/v1/categories'; static const String categories = '/api/v1/categories';
static const String products = '/api/v1/products'; static const String products = '/api/v1/products';
static const String units = '/api/v1/units';
static const String tables = '/api/v1/tables'; static const String tables = '/api/v1/tables';
static const String customers = '/api/v1/customers'; static const String customers = '/api/v1/customers';
static const String paymentMethods = '/api/v1/payment-methods'; static const String paymentMethods = '/api/v1/payment-methods';
@@ -8,6 +8,9 @@ class AddItemOrderRequest with _$AddItemOrderRequest {
required int quantity, required int quantity,
required int unitPrice, required int unitPrice,
required String notes, required String notes,
/// Hanya diisi untuk produk `sell_by: "weight"`.
double? weight,
}) = _AddItemOrderRequest; }) = _AddItemOrderRequest;
factory AddItemOrderRequest.empty() => const AddItemOrderRequest( factory AddItemOrderRequest.empty() => const AddItemOrderRequest(
@@ -17,4 +20,14 @@ class AddItemOrderRequest with _$AddItemOrderRequest {
unitPrice: 0, unitPrice: 0,
notes: '', 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,
);
} }
+48 -16
View File
@@ -197,19 +197,19 @@ class Order with _$Order {
); );
// For Add Item Order // For Add Item Order
factory Order.fromLastOrder(Order order, List<ProductQuantity> products) => factory Order.fromLastOrder(Order order, List<ProductQuantity> products) {
order.copyWith( final subtotal = products.fold<int>(
orderItems: products 0,
.map((e) => OrderItem.fromProductQuantity(e)) (sum, item) => sum + item.lineTotal.toInt(),
.toList(), );
totalAmount: products
.map((e) => e.product.price.toInt() * e.quantity) return order.copyWith(
.reduce((value, element) => value + element), orderItems: products.map(OrderItem.fromProductQuantity).toList(),
subtotal: products totalAmount: subtotal,
.map((e) => e.product.price.toInt() * e.quantity) subtotal: subtotal,
.reduce((value, element) => value + element), metadata: order.metadata,
metadata: order.metadata, );
); }
List<OrderItem> get orderItemNotCancelled => List<OrderItem> get orderItemNotCancelled =>
orderItems.where((item) => item.status != 'cancelled').toList(); orderItems.where((item) => item.status != 'cancelled').toList();
@@ -217,6 +217,8 @@ class Order with _$Order {
@freezed @freezed
class OrderItem with _$OrderItem { class OrderItem with _$OrderItem {
const OrderItem._();
const factory OrderItem({ const factory OrderItem({
required String id, required String id,
required String orderId, required String orderId,
@@ -237,6 +239,12 @@ class OrderItem with _$OrderItem {
required int paidQuantity, required int paidQuantity,
required String categoryId, required String categoryId,
required String categoryName, required String categoryName,
/// Angka timbangan baris ini. `null` untuk produk cacah.
double? weight,
@Default('') String unitId,
@Default('') String unitName,
@Default('') String unitAbbreviation,
}) = _OrderItem; }) = _OrderItem;
factory OrderItem.empty() => OrderItem( factory OrderItem.empty() => OrderItem(
@@ -269,10 +277,9 @@ class OrderItem with _$OrderItem {
productName: productQuantity.product.name, productName: productQuantity.product.name,
productVariantId: productQuantity.variant?.id ?? '', productVariantId: productQuantity.variant?.id ?? '',
productVariantName: productQuantity.variant?.name ?? '', productVariantName: productQuantity.variant?.name ?? '',
quantity: productQuantity.quantity, quantity: productQuantity.isWeighted ? 1 : productQuantity.quantity,
unitPrice: productQuantity.product.price.toInt(), unitPrice: productQuantity.product.price.toInt(),
totalPrice: (productQuantity.product.price * productQuantity.quantity) totalPrice: productQuantity.lineTotal.toInt(),
.toInt(),
modifiers: [], modifiers: [],
notes: productQuantity.notes, notes: productQuantity.notes,
status: 'pending', status: 'pending',
@@ -283,5 +290,30 @@ class OrderItem with _$OrderItem {
paidQuantity: 0, paidQuantity: 0,
categoryId: '', categoryId: '',
categoryName: '', 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 quantity,
required int unitPrice, required int unitPrice,
required String notes, required String notes,
/// Hanya diisi untuk produk `sell_by: "weight"`; produk cacah tidak boleh
/// mengirim berat sama sekali.
double? weight,
}) = _OrderItemRequest; }) = _OrderItemRequest;
factory OrderItemRequest.empty() => const OrderItemRequest( factory OrderItemRequest.empty() => const OrderItemRequest(
@@ -42,4 +46,16 @@ class OrderItemRequest with _$OrderItemRequest {
unitPrice: 0, unitPrice: 0,
notes: '', 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,
);
} }
+1
View File
@@ -3,6 +3,7 @@ import 'package:freezed_annotation/freezed_annotation.dart';
import '../../common/api/api_failure.dart'; import '../../common/api/api_failure.dart';
import '../../common/types/split_type.dart'; import '../../common/types/split_type.dart';
import '../../common/extension/extension.dart';
import '../product/product.dart'; import '../product/product.dart';
part 'order.freezed.dart'; part 'order.freezed.dart';
+163 -9
View File
@@ -1007,6 +1007,12 @@ mixin _$OrderItem {
String get categoryId => throw _privateConstructorUsedError; String get categoryId => throw _privateConstructorUsedError;
String get categoryName => 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 /// Create a copy of OrderItem
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@@ -1039,6 +1045,10 @@ abstract class $OrderItemCopyWith<$Res> {
int paidQuantity, int paidQuantity,
String categoryId, String categoryId,
String categoryName, String categoryName,
double? weight,
String unitId,
String unitName,
String unitAbbreviation,
}); });
} }
@@ -1076,6 +1086,10 @@ class _$OrderItemCopyWithImpl<$Res, $Val extends OrderItem>
Object? paidQuantity = null, Object? paidQuantity = null,
Object? categoryId = null, Object? categoryId = null,
Object? categoryName = null, Object? categoryName = null,
Object? weight = freezed,
Object? unitId = null,
Object? unitName = null,
Object? unitAbbreviation = null,
}) { }) {
return _then( return _then(
_value.copyWith( _value.copyWith(
@@ -1155,6 +1169,22 @@ class _$OrderItemCopyWithImpl<$Res, $Val extends OrderItem>
? _value.categoryName ? _value.categoryName
: categoryName // ignore: cast_nullable_to_non_nullable : categoryName // ignore: cast_nullable_to_non_nullable
as String, 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, as $Val,
); );
@@ -1190,6 +1220,10 @@ abstract class _$$OrderItemImplCopyWith<$Res>
int paidQuantity, int paidQuantity,
String categoryId, String categoryId,
String categoryName, String categoryName,
double? weight,
String unitId,
String unitName,
String unitAbbreviation,
}); });
} }
@@ -1226,6 +1260,10 @@ class __$$OrderItemImplCopyWithImpl<$Res>
Object? paidQuantity = null, Object? paidQuantity = null,
Object? categoryId = null, Object? categoryId = null,
Object? categoryName = null, Object? categoryName = null,
Object? weight = freezed,
Object? unitId = null,
Object? unitName = null,
Object? unitAbbreviation = null,
}) { }) {
return _then( return _then(
_$OrderItemImpl( _$OrderItemImpl(
@@ -1305,6 +1343,22 @@ class __$$OrderItemImplCopyWithImpl<$Res>
? _value.categoryName ? _value.categoryName
: categoryName // ignore: cast_nullable_to_non_nullable : categoryName // ignore: cast_nullable_to_non_nullable
as String, 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,
), ),
); );
} }
@@ -1312,7 +1366,7 @@ class __$$OrderItemImplCopyWithImpl<$Res>
/// @nodoc /// @nodoc
class _$OrderItemImpl implements _OrderItem { class _$OrderItemImpl extends _OrderItem {
const _$OrderItemImpl({ const _$OrderItemImpl({
required this.id, required this.id,
required this.orderId, required this.orderId,
@@ -1333,7 +1387,12 @@ class _$OrderItemImpl implements _OrderItem {
required this.paidQuantity, required this.paidQuantity,
required this.categoryId, required this.categoryId,
required this.categoryName, required this.categoryName,
}) : _modifiers = modifiers; this.weight,
this.unitId = '',
this.unitName = '',
this.unitAbbreviation = '',
}) : _modifiers = modifiers,
super._();
@override @override
final String id; final String id;
@@ -1380,9 +1439,22 @@ class _$OrderItemImpl implements _OrderItem {
@override @override
final String categoryName; 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 @override
String toString() { 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, printToChecker: $printToChecker, 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 @override
@@ -1425,7 +1497,13 @@ class _$OrderItemImpl implements _OrderItem {
(identical(other.categoryId, categoryId) || (identical(other.categoryId, categoryId) ||
other.categoryId == categoryId) && other.categoryId == categoryId) &&
(identical(other.categoryName, categoryName) || (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 @override
@@ -1450,6 +1528,10 @@ class _$OrderItemImpl implements _OrderItem {
paidQuantity, paidQuantity,
categoryId, categoryId,
categoryName, categoryName,
weight,
unitId,
unitName,
unitAbbreviation,
]); ]);
/// Create a copy of OrderItem /// Create a copy of OrderItem
@@ -1461,7 +1543,7 @@ class _$OrderItemImpl implements _OrderItem {
__$$OrderItemImplCopyWithImpl<_$OrderItemImpl>(this, _$identity); __$$OrderItemImplCopyWithImpl<_$OrderItemImpl>(this, _$identity);
} }
abstract class _OrderItem implements OrderItem { abstract class _OrderItem extends OrderItem {
const factory _OrderItem({ const factory _OrderItem({
required final String id, required final String id,
required final String orderId, required final String orderId,
@@ -1482,7 +1564,12 @@ abstract class _OrderItem implements OrderItem {
required final int paidQuantity, required final int paidQuantity,
required final String categoryId, required final String categoryId,
required final String categoryName, required final String categoryName,
final double? weight,
final String unitId,
final String unitName,
final String unitAbbreviation,
}) = _$OrderItemImpl; }) = _$OrderItemImpl;
const _OrderItem._() : super._();
@override @override
String get id; String get id;
@@ -1523,6 +1610,16 @@ abstract class _OrderItem implements OrderItem {
@override @override
String get categoryName; 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 /// Create a copy of OrderItem
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@override @override
@@ -1847,6 +1944,10 @@ mixin _$OrderItemRequest {
int get unitPrice => throw _privateConstructorUsedError; int get unitPrice => throw _privateConstructorUsedError;
String get notes => 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 /// Create a copy of OrderItemRequest
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@@ -1867,6 +1968,7 @@ abstract class $OrderItemRequestCopyWith<$Res> {
int quantity, int quantity,
int unitPrice, int unitPrice,
String notes, String notes,
double? weight,
}); });
} }
@@ -1890,6 +1992,7 @@ class _$OrderItemRequestCopyWithImpl<$Res, $Val extends OrderItemRequest>
Object? quantity = null, Object? quantity = null,
Object? unitPrice = null, Object? unitPrice = null,
Object? notes = null, Object? notes = null,
Object? weight = freezed,
}) { }) {
return _then( return _then(
_value.copyWith( _value.copyWith(
@@ -1913,6 +2016,10 @@ class _$OrderItemRequestCopyWithImpl<$Res, $Val extends OrderItemRequest>
? _value.notes ? _value.notes
: notes // ignore: cast_nullable_to_non_nullable : notes // ignore: cast_nullable_to_non_nullable
as String, as String,
weight: freezed == weight
? _value.weight
: weight // ignore: cast_nullable_to_non_nullable
as double?,
) )
as $Val, as $Val,
); );
@@ -1934,6 +2041,7 @@ abstract class _$$OrderItemRequestImplCopyWith<$Res>
int quantity, int quantity,
int unitPrice, int unitPrice,
String notes, String notes,
double? weight,
}); });
} }
@@ -1956,6 +2064,7 @@ class __$$OrderItemRequestImplCopyWithImpl<$Res>
Object? quantity = null, Object? quantity = null,
Object? unitPrice = null, Object? unitPrice = null,
Object? notes = null, Object? notes = null,
Object? weight = freezed,
}) { }) {
return _then( return _then(
_$OrderItemRequestImpl( _$OrderItemRequestImpl(
@@ -1979,6 +2088,10 @@ class __$$OrderItemRequestImplCopyWithImpl<$Res>
? _value.notes ? _value.notes
: notes // ignore: cast_nullable_to_non_nullable : notes // ignore: cast_nullable_to_non_nullable
as String, as String,
weight: freezed == weight
? _value.weight
: weight // ignore: cast_nullable_to_non_nullable
as double?,
), ),
); );
} }
@@ -1993,6 +2106,7 @@ class _$OrderItemRequestImpl implements _OrderItemRequest {
required this.quantity, required this.quantity,
required this.unitPrice, required this.unitPrice,
required this.notes, required this.notes,
this.weight,
}); });
@override @override
@@ -2006,9 +2120,14 @@ class _$OrderItemRequestImpl implements _OrderItemRequest {
@override @override
final String notes; final String notes;
/// Hanya diisi untuk produk `sell_by: "weight"`; produk cacah tidak boleh
/// mengirim berat sama sekali.
@override
final double? weight;
@override @override
String toString() { 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 @override
@@ -2024,7 +2143,8 @@ class _$OrderItemRequestImpl implements _OrderItemRequest {
other.quantity == quantity) && other.quantity == quantity) &&
(identical(other.unitPrice, unitPrice) || (identical(other.unitPrice, unitPrice) ||
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 @override
@@ -2035,6 +2155,7 @@ class _$OrderItemRequestImpl implements _OrderItemRequest {
quantity, quantity,
unitPrice, unitPrice,
notes, notes,
weight,
); );
/// Create a copy of OrderItemRequest /// Create a copy of OrderItemRequest
@@ -2056,6 +2177,7 @@ abstract class _OrderItemRequest implements OrderItemRequest {
required final int quantity, required final int quantity,
required final int unitPrice, required final int unitPrice,
required final String notes, required final String notes,
final double? weight,
}) = _$OrderItemRequestImpl; }) = _$OrderItemRequestImpl;
@override @override
@@ -2069,6 +2191,11 @@ abstract class _OrderItemRequest implements OrderItemRequest {
@override @override
String get notes; 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 /// Create a copy of OrderItemRequest
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@override @override
@@ -3448,6 +3575,9 @@ mixin _$AddItemOrderRequest {
int get unitPrice => throw _privateConstructorUsedError; int get unitPrice => throw _privateConstructorUsedError;
String get notes => throw _privateConstructorUsedError; String get notes => throw _privateConstructorUsedError;
/// Hanya diisi untuk produk `sell_by: "weight"`.
double? get weight => throw _privateConstructorUsedError;
/// Create a copy of AddItemOrderRequest /// Create a copy of AddItemOrderRequest
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@@ -3468,6 +3598,7 @@ abstract class $AddItemOrderRequestCopyWith<$Res> {
int quantity, int quantity,
int unitPrice, int unitPrice,
String notes, String notes,
double? weight,
}); });
} }
@@ -3491,6 +3622,7 @@ class _$AddItemOrderRequestCopyWithImpl<$Res, $Val extends AddItemOrderRequest>
Object? quantity = null, Object? quantity = null,
Object? unitPrice = null, Object? unitPrice = null,
Object? notes = null, Object? notes = null,
Object? weight = freezed,
}) { }) {
return _then( return _then(
_value.copyWith( _value.copyWith(
@@ -3514,6 +3646,10 @@ class _$AddItemOrderRequestCopyWithImpl<$Res, $Val extends AddItemOrderRequest>
? _value.notes ? _value.notes
: notes // ignore: cast_nullable_to_non_nullable : notes // ignore: cast_nullable_to_non_nullable
as String, as String,
weight: freezed == weight
? _value.weight
: weight // ignore: cast_nullable_to_non_nullable
as double?,
) )
as $Val, as $Val,
); );
@@ -3535,6 +3671,7 @@ abstract class _$$AddItemOrderRequestImplCopyWith<$Res>
int quantity, int quantity,
int unitPrice, int unitPrice,
String notes, String notes,
double? weight,
}); });
} }
@@ -3557,6 +3694,7 @@ class __$$AddItemOrderRequestImplCopyWithImpl<$Res>
Object? quantity = null, Object? quantity = null,
Object? unitPrice = null, Object? unitPrice = null,
Object? notes = null, Object? notes = null,
Object? weight = freezed,
}) { }) {
return _then( return _then(
_$AddItemOrderRequestImpl( _$AddItemOrderRequestImpl(
@@ -3580,6 +3718,10 @@ class __$$AddItemOrderRequestImplCopyWithImpl<$Res>
? _value.notes ? _value.notes
: notes // ignore: cast_nullable_to_non_nullable : notes // ignore: cast_nullable_to_non_nullable
as String, as String,
weight: freezed == weight
? _value.weight
: weight // ignore: cast_nullable_to_non_nullable
as double?,
), ),
); );
} }
@@ -3594,6 +3736,7 @@ class _$AddItemOrderRequestImpl implements _AddItemOrderRequest {
required this.quantity, required this.quantity,
required this.unitPrice, required this.unitPrice,
required this.notes, required this.notes,
this.weight,
}); });
@override @override
@@ -3607,9 +3750,13 @@ class _$AddItemOrderRequestImpl implements _AddItemOrderRequest {
@override @override
final String notes; final String notes;
/// Hanya diisi untuk produk `sell_by: "weight"`.
@override
final double? weight;
@override @override
String toString() { 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 @override
@@ -3625,7 +3772,8 @@ class _$AddItemOrderRequestImpl implements _AddItemOrderRequest {
other.quantity == quantity) && other.quantity == quantity) &&
(identical(other.unitPrice, unitPrice) || (identical(other.unitPrice, unitPrice) ||
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 @override
@@ -3636,6 +3784,7 @@ class _$AddItemOrderRequestImpl implements _AddItemOrderRequest {
quantity, quantity,
unitPrice, unitPrice,
notes, notes,
weight,
); );
/// Create a copy of AddItemOrderRequest /// Create a copy of AddItemOrderRequest
@@ -3657,6 +3806,7 @@ abstract class _AddItemOrderRequest implements AddItemOrderRequest {
required final int quantity, required final int quantity,
required final int unitPrice, required final int unitPrice,
required final String notes, required final String notes,
final double? weight,
}) = _$AddItemOrderRequestImpl; }) = _$AddItemOrderRequestImpl;
@override @override
@@ -3670,6 +3820,10 @@ abstract class _AddItemOrderRequest implements AddItemOrderRequest {
@override @override
String get notes; String get notes;
/// Hanya diisi untuk produk `sell_by: "weight"`.
@override
double? get weight;
/// Create a copy of AddItemOrderRequest /// Create a copy of AddItemOrderRequest
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@override @override
@@ -21,6 +21,8 @@ class ListProduct with _$ListProduct {
@freezed @freezed
class Product with _$Product { class Product with _$Product {
const Product._();
const factory Product({ const factory Product({
required String id, required String id,
required String organizationId, required String organizationId,
@@ -39,6 +41,11 @@ class Product with _$Product {
required String createdAt, required String createdAt,
required String updatedAt, required String updatedAt,
required List<ProductVariant> variants, 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; }) = _Product;
factory Product.empty() => const Product( factory Product.empty() => const Product(
@@ -60,6 +67,18 @@ class Product with _$Product {
updatedAt: '', updatedAt: '',
variants: [], 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 @freezed
@@ -1,14 +1,53 @@
part of '../product.dart'; 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 @freezed
class ProductQuantity with _$ProductQuantity { class ProductQuantity with _$ProductQuantity {
const ProductQuantity._();
const factory ProductQuantity({ const factory ProductQuantity({
required Product product, required Product product,
ProductVariant? variant, ProductVariant? variant,
required int quantity, required int quantity,
required String notes, required String notes,
@Default('') String lineId,
/// Angka timbangan dalam satuan produk. `null` untuk produk cacah.
double? weight,
}) = _ProductQuantity; }) = _ProductQuantity;
factory ProductQuantity.empty() => factory ProductQuantity.empty() =>
ProductQuantity(product: Product.empty(), quantity: 0, notes: ''); 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));
} }
+2
View File
@@ -3,6 +3,8 @@ import 'package:flutter/foundation.dart';
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
import '../../common/api/api_failure.dart'; import '../../common/api/api_failure.dart';
import '../../common/extension/extension.dart';
import '../../common/types/sell_by.dart';
part 'product.freezed.dart'; part 'product.freezed.dart';
+176 -18
View File
@@ -286,6 +286,12 @@ mixin _$Product {
String get updatedAt => throw _privateConstructorUsedError; String get updatedAt => throw _privateConstructorUsedError;
List<ProductVariant> get variants => 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 /// Create a copy of Product
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@@ -315,6 +321,10 @@ abstract class $ProductCopyWith<$Res> {
String createdAt, String createdAt,
String updatedAt, String updatedAt,
List<ProductVariant> variants, List<ProductVariant> variants,
String sellBy,
String unitId,
String unitName,
String unitAbbreviation,
}); });
} }
@@ -350,6 +360,10 @@ class _$ProductCopyWithImpl<$Res, $Val extends Product>
Object? createdAt = null, Object? createdAt = null,
Object? updatedAt = null, Object? updatedAt = null,
Object? variants = null, Object? variants = null,
Object? sellBy = null,
Object? unitId = null,
Object? unitName = null,
Object? unitAbbreviation = null,
}) { }) {
return _then( return _then(
_value.copyWith( _value.copyWith(
@@ -421,6 +435,22 @@ class _$ProductCopyWithImpl<$Res, $Val extends Product>
? _value.variants ? _value.variants
: variants // ignore: cast_nullable_to_non_nullable : variants // ignore: cast_nullable_to_non_nullable
as List<ProductVariant>, 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, as $Val,
); );
@@ -453,6 +483,10 @@ abstract class _$$ProductImplCopyWith<$Res> implements $ProductCopyWith<$Res> {
String createdAt, String createdAt,
String updatedAt, String updatedAt,
List<ProductVariant> variants, List<ProductVariant> variants,
String sellBy,
String unitId,
String unitName,
String unitAbbreviation,
}); });
} }
@@ -487,6 +521,10 @@ class __$$ProductImplCopyWithImpl<$Res>
Object? createdAt = null, Object? createdAt = null,
Object? updatedAt = null, Object? updatedAt = null,
Object? variants = null, Object? variants = null,
Object? sellBy = null,
Object? unitId = null,
Object? unitName = null,
Object? unitAbbreviation = null,
}) { }) {
return _then( return _then(
_$ProductImpl( _$ProductImpl(
@@ -558,6 +596,22 @@ class __$$ProductImplCopyWithImpl<$Res>
? _value._variants ? _value._variants
: variants // ignore: cast_nullable_to_non_nullable : variants // ignore: cast_nullable_to_non_nullable
as List<ProductVariant>, 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,
), ),
); );
} }
@@ -565,7 +619,7 @@ class __$$ProductImplCopyWithImpl<$Res>
/// @nodoc /// @nodoc
class _$ProductImpl with DiagnosticableTreeMixin implements _Product { class _$ProductImpl extends _Product with DiagnosticableTreeMixin {
const _$ProductImpl({ const _$ProductImpl({
required this.id, required this.id,
required this.organizationId, required this.organizationId,
@@ -584,8 +638,13 @@ class _$ProductImpl with DiagnosticableTreeMixin implements _Product {
required this.createdAt, required this.createdAt,
required this.updatedAt, required this.updatedAt,
required final List<ProductVariant> variants, required final List<ProductVariant> variants,
this.sellBy = SellBy.unit,
this.unitId = '',
this.unitName = '',
this.unitAbbreviation = '',
}) : _metadata = metadata, }) : _metadata = metadata,
_variants = variants; _variants = variants,
super._();
@override @override
final String id; final String id;
@@ -633,9 +692,23 @@ class _$ProductImpl with DiagnosticableTreeMixin implements _Product {
return EqualUnmodifiableListView(_variants); 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 @override
String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { 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, printToChecker: $printToChecker, 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 @override
@@ -659,7 +732,11 @@ class _$ProductImpl with DiagnosticableTreeMixin implements _Product {
..add(DiagnosticsProperty('isActive', isActive)) ..add(DiagnosticsProperty('isActive', isActive))
..add(DiagnosticsProperty('createdAt', createdAt)) ..add(DiagnosticsProperty('createdAt', createdAt))
..add(DiagnosticsProperty('updatedAt', updatedAt)) ..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 @override
@@ -693,11 +770,17 @@ class _$ProductImpl with DiagnosticableTreeMixin implements _Product {
other.createdAt == createdAt) && other.createdAt == createdAt) &&
(identical(other.updatedAt, updatedAt) || (identical(other.updatedAt, updatedAt) ||
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 @override
int get hashCode => Object.hash( int get hashCode => Object.hashAll([
runtimeType, runtimeType,
id, id,
organizationId, organizationId,
@@ -716,7 +799,11 @@ class _$ProductImpl with DiagnosticableTreeMixin implements _Product {
createdAt, createdAt,
updatedAt, updatedAt,
const DeepCollectionEquality().hash(_variants), const DeepCollectionEquality().hash(_variants),
); sellBy,
unitId,
unitName,
unitAbbreviation,
]);
/// Create a copy of Product /// Create a copy of Product
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@@ -727,7 +814,7 @@ class _$ProductImpl with DiagnosticableTreeMixin implements _Product {
__$$ProductImplCopyWithImpl<_$ProductImpl>(this, _$identity); __$$ProductImplCopyWithImpl<_$ProductImpl>(this, _$identity);
} }
abstract class _Product implements Product { abstract class _Product extends Product {
const factory _Product({ const factory _Product({
required final String id, required final String id,
required final String organizationId, required final String organizationId,
@@ -746,7 +833,12 @@ abstract class _Product implements Product {
required final String createdAt, required final String createdAt,
required final String updatedAt, required final String updatedAt,
required final List<ProductVariant> variants, required final List<ProductVariant> variants,
final String sellBy,
final String unitId,
final String unitName,
final String unitAbbreviation,
}) = _$ProductImpl; }) = _$ProductImpl;
const _Product._() : super._();
@override @override
String get id; String get id;
@@ -783,6 +875,16 @@ abstract class _Product implements Product {
@override @override
List<ProductVariant> get variants; 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 /// Create a copy of Product
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@override @override
@@ -1121,6 +1223,10 @@ mixin _$ProductQuantity {
ProductVariant? get variant => throw _privateConstructorUsedError; ProductVariant? get variant => throw _privateConstructorUsedError;
int get quantity => throw _privateConstructorUsedError; int get quantity => throw _privateConstructorUsedError;
String get notes => 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 /// Create a copy of ProductQuantity
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@@ -1141,6 +1247,8 @@ abstract class $ProductQuantityCopyWith<$Res> {
ProductVariant? variant, ProductVariant? variant,
int quantity, int quantity,
String notes, String notes,
String lineId,
double? weight,
}); });
$ProductCopyWith<$Res> get product; $ProductCopyWith<$Res> get product;
@@ -1166,6 +1274,8 @@ class _$ProductQuantityCopyWithImpl<$Res, $Val extends ProductQuantity>
Object? variant = freezed, Object? variant = freezed,
Object? quantity = null, Object? quantity = null,
Object? notes = null, Object? notes = null,
Object? lineId = null,
Object? weight = freezed,
}) { }) {
return _then( return _then(
_value.copyWith( _value.copyWith(
@@ -1185,6 +1295,14 @@ class _$ProductQuantityCopyWithImpl<$Res, $Val extends ProductQuantity>
? _value.notes ? _value.notes
: notes // ignore: cast_nullable_to_non_nullable : notes // ignore: cast_nullable_to_non_nullable
as String, 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, as $Val,
); );
@@ -1229,6 +1347,8 @@ abstract class _$$ProductQuantityImplCopyWith<$Res>
ProductVariant? variant, ProductVariant? variant,
int quantity, int quantity,
String notes, String notes,
String lineId,
double? weight,
}); });
@override @override
@@ -1255,6 +1375,8 @@ class __$$ProductQuantityImplCopyWithImpl<$Res>
Object? variant = freezed, Object? variant = freezed,
Object? quantity = null, Object? quantity = null,
Object? notes = null, Object? notes = null,
Object? lineId = null,
Object? weight = freezed,
}) { }) {
return _then( return _then(
_$ProductQuantityImpl( _$ProductQuantityImpl(
@@ -1274,6 +1396,14 @@ class __$$ProductQuantityImplCopyWithImpl<$Res>
? _value.notes ? _value.notes
: notes // ignore: cast_nullable_to_non_nullable : notes // ignore: cast_nullable_to_non_nullable
as String, 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?,
), ),
); );
} }
@@ -1281,15 +1411,16 @@ class __$$ProductQuantityImplCopyWithImpl<$Res>
/// @nodoc /// @nodoc
class _$ProductQuantityImpl class _$ProductQuantityImpl extends _ProductQuantity
with DiagnosticableTreeMixin with DiagnosticableTreeMixin {
implements _ProductQuantity {
const _$ProductQuantityImpl({ const _$ProductQuantityImpl({
required this.product, required this.product,
this.variant, this.variant,
required this.quantity, required this.quantity,
required this.notes, required this.notes,
}); this.lineId = '',
this.weight,
}) : super._();
@override @override
final Product product; final Product product;
@@ -1299,10 +1430,17 @@ class _$ProductQuantityImpl
final int quantity; final int quantity;
@override @override
final String notes; final String notes;
@override
@JsonKey()
final String lineId;
/// Angka timbangan dalam satuan produk. `null` untuk produk cacah.
@override
final double? weight;
@override @override
String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { 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 @override
@@ -1313,7 +1451,9 @@ class _$ProductQuantityImpl
..add(DiagnosticsProperty('product', product)) ..add(DiagnosticsProperty('product', product))
..add(DiagnosticsProperty('variant', variant)) ..add(DiagnosticsProperty('variant', variant))
..add(DiagnosticsProperty('quantity', quantity)) ..add(DiagnosticsProperty('quantity', quantity))
..add(DiagnosticsProperty('notes', notes)); ..add(DiagnosticsProperty('notes', notes))
..add(DiagnosticsProperty('lineId', lineId))
..add(DiagnosticsProperty('weight', weight));
} }
@override @override
@@ -1325,12 +1465,21 @@ class _$ProductQuantityImpl
(identical(other.variant, variant) || other.variant == variant) && (identical(other.variant, variant) || other.variant == variant) &&
(identical(other.quantity, quantity) || (identical(other.quantity, quantity) ||
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 @override
int get hashCode => int get hashCode => Object.hash(
Object.hash(runtimeType, product, variant, quantity, notes); runtimeType,
product,
variant,
quantity,
notes,
lineId,
weight,
);
/// Create a copy of ProductQuantity /// Create a copy of ProductQuantity
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@@ -1344,13 +1493,16 @@ class _$ProductQuantityImpl
); );
} }
abstract class _ProductQuantity implements ProductQuantity { abstract class _ProductQuantity extends ProductQuantity {
const factory _ProductQuantity({ const factory _ProductQuantity({
required final Product product, required final Product product,
final ProductVariant? variant, final ProductVariant? variant,
required final int quantity, required final int quantity,
required final String notes, required final String notes,
final String lineId,
final double? weight,
}) = _$ProductQuantityImpl; }) = _$ProductQuantityImpl;
const _ProductQuantity._() : super._();
@override @override
Product get product; Product get product;
@@ -1360,6 +1512,12 @@ abstract class _ProductQuantity implements ProductQuantity {
int get quantity; int get quantity;
@override @override
String get notes; 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 /// Create a copy of ProductQuantity
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
+23
View File
@@ -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();
}
+10
View File
@@ -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
@@ -55,6 +55,12 @@ class OrderRemoteDataProvider {
); );
if (response.data['success'] == false) { 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()); return DC.error(const OrderFailure.unexpectedError());
} }
@@ -77,6 +83,12 @@ class OrderRemoteDataProvider {
); );
if (response.data['success'] == false) { 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()); return DC.error(const OrderFailure.unexpectedError());
} }
@@ -102,6 +114,12 @@ class OrderRemoteDataProvider {
); );
if (response.data['success'] == false) { 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()); return DC.error(const OrderFailure.unexpectedError());
} }
@@ -128,6 +146,12 @@ class OrderRemoteDataProvider {
); );
if (response.data['success'] == false) { 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()); return DC.error(const OrderFailure.unexpectedError());
} else { } else {
final order = OrderDto.fromJson( final order = OrderDto.fromJson(
@@ -171,6 +195,12 @@ class OrderRemoteDataProvider {
); );
if (response.data['success'] == false) { 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()); return DC.error(const OrderFailure.unexpectedError());
} }
@@ -201,6 +231,12 @@ class OrderRemoteDataProvider {
); );
if (response.data['success'] == false) { 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()); return DC.error(const OrderFailure.unexpectedError());
} }
@@ -239,6 +275,12 @@ class OrderRemoteDataProvider {
); );
if (response.data['success'] == false) { 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()); return DC.error(const OrderFailure.unexpectedError());
} }
@@ -262,6 +304,12 @@ class OrderRemoteDataProvider {
); );
if (response.data['success'] == false) { 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()); return DC.error(const OrderFailure.unexpectedError());
} }
@@ -290,6 +338,12 @@ class OrderRemoteDataProvider {
); );
if (response.data['success'] == false) { 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()); return DC.error(const OrderFailure.unexpectedError());
} }
@@ -10,6 +10,7 @@ class AddItemOrderRequestDto with _$AddItemOrderRequestDto {
@JsonKey(name: 'quantity') int? quantity, @JsonKey(name: 'quantity') int? quantity,
@JsonKey(name: 'unit_price') int? unitPrice, @JsonKey(name: 'unit_price') int? unitPrice,
@JsonKey(name: 'notes') String? notes, @JsonKey(name: 'notes') String? notes,
@JsonKey(name: 'weight') double? weight,
}) = _AddItemOrderRequestDto; }) = _AddItemOrderRequestDto;
factory AddItemOrderRequestDto.fromJson(Map<String, dynamic> json) => factory AddItemOrderRequestDto.fromJson(Map<String, dynamic> json) =>
@@ -21,6 +22,7 @@ class AddItemOrderRequestDto with _$AddItemOrderRequestDto {
quantity: quantity ?? 0, quantity: quantity ?? 0,
unitPrice: unitPrice ?? 0, unitPrice: unitPrice ?? 0,
notes: notes ?? '', notes: notes ?? '',
weight: weight,
); );
factory AddItemOrderRequestDto.fromDomain( factory AddItemOrderRequestDto.fromDomain(
@@ -31,6 +33,7 @@ class AddItemOrderRequestDto with _$AddItemOrderRequestDto {
quantity: addItemOrderRequest.quantity, quantity: addItemOrderRequest.quantity,
unitPrice: addItemOrderRequest.unitPrice, unitPrice: addItemOrderRequest.unitPrice,
notes: addItemOrderRequest.notes, notes: addItemOrderRequest.notes,
weight: addItemOrderRequest.weight,
); );
Map<String, dynamic> toRequest() { Map<String, dynamic> toRequest() {
@@ -45,6 +48,11 @@ class AddItemOrderRequestDto with _$AddItemOrderRequestDto {
data['product_variant_id'] = productvariantId; data['product_variant_id'] = productvariantId;
} }
// Produk satuan ditolak backend bila membawa berat.
if (weight != null) {
data['weight'] = weight;
}
return data; return data;
} }
} }
@@ -118,6 +118,10 @@ class OrderItemDto with _$OrderItemDto {
@JsonKey(name: "paid_quantity") int? paidQuantity, @JsonKey(name: "paid_quantity") int? paidQuantity,
@JsonKey(name: "category_id") String? categoryId, @JsonKey(name: "category_id") String? categoryId,
@JsonKey(name: "category_name") String? categoryName, @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; }) = _OrderItemDto;
factory OrderItemDto.fromJson(Map<String, dynamic> json) => factory OrderItemDto.fromJson(Map<String, dynamic> json) =>
@@ -144,6 +148,10 @@ class OrderItemDto with _$OrderItemDto {
paidQuantity: paidQuantity ?? 0, paidQuantity: paidQuantity ?? 0,
categoryId: categoryId ?? '', categoryId: categoryId ?? '',
categoryName: categoryName ?? '', categoryName: categoryName ?? '',
weight: weight,
unitId: unitId ?? '',
unitName: unitName ?? '',
unitAbbreviation: unitAbbreviation ?? '',
); );
factory OrderItemDto.fromDomain(OrderItem orderItem) => OrderItemDto( factory OrderItemDto.fromDomain(OrderItem orderItem) => OrderItemDto(
@@ -166,5 +174,9 @@ class OrderItemDto with _$OrderItemDto {
paidQuantity: orderItem.paidQuantity, paidQuantity: orderItem.paidQuantity,
categoryId: orderItem.categoryId, categoryId: orderItem.categoryId,
categoryName: orderItem.categoryName, 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: "quantity") int? quantity,
@JsonKey(name: "unit_price") int? unitPrice, @JsonKey(name: "unit_price") int? unitPrice,
@JsonKey(name: "notes") String? notes, @JsonKey(name: "notes") String? notes,
@JsonKey(name: "weight") double? weight,
}) = _OrderItemRequestDto; }) = _OrderItemRequestDto;
factory OrderItemRequestDto.fromJson(Map<String, dynamic> json) => factory OrderItemRequestDto.fromJson(Map<String, dynamic> json) =>
@@ -87,6 +88,7 @@ class OrderItemRequestDto with _$OrderItemRequestDto {
quantity: quantity ?? 0, quantity: quantity ?? 0,
unitPrice: unitPrice ?? 0, unitPrice: unitPrice ?? 0,
notes: notes ?? '', notes: notes ?? '',
weight: weight,
); );
factory OrderItemRequestDto.fromDomain(OrderItemRequest request) => factory OrderItemRequestDto.fromDomain(OrderItemRequest request) =>
@@ -96,6 +98,7 @@ class OrderItemRequestDto with _$OrderItemRequestDto {
quantity: request.quantity, quantity: request.quantity,
unitPrice: request.unitPrice, unitPrice: request.unitPrice,
notes: request.notes, notes: request.notes,
weight: request.weight,
); );
Map<String, dynamic> toRequest() { Map<String, dynamic> toRequest() {
@@ -110,6 +113,12 @@ class OrderItemRequestDto with _$OrderItemRequestDto {
data["product_variant_id"] = productVariantId; 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; return data;
} }
} }
@@ -1177,6 +1177,14 @@ mixin _$OrderItemDto {
String? get categoryId => throw _privateConstructorUsedError; String? get categoryId => throw _privateConstructorUsedError;
@JsonKey(name: "category_name") @JsonKey(name: "category_name")
String? get categoryName => throw _privateConstructorUsedError; 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. /// Serializes this OrderItemDto to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError; Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@@ -1215,6 +1223,10 @@ abstract class $OrderItemDtoCopyWith<$Res> {
@JsonKey(name: "paid_quantity") int? paidQuantity, @JsonKey(name: "paid_quantity") int? paidQuantity,
@JsonKey(name: "category_id") String? categoryId, @JsonKey(name: "category_id") String? categoryId,
@JsonKey(name: "category_name") String? categoryName, @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,
}); });
} }
@@ -1252,6 +1264,10 @@ class _$OrderItemDtoCopyWithImpl<$Res, $Val extends OrderItemDto>
Object? paidQuantity = freezed, Object? paidQuantity = freezed,
Object? categoryId = freezed, Object? categoryId = freezed,
Object? categoryName = freezed, Object? categoryName = freezed,
Object? weight = freezed,
Object? unitId = freezed,
Object? unitName = freezed,
Object? unitAbbreviation = freezed,
}) { }) {
return _then( return _then(
_value.copyWith( _value.copyWith(
@@ -1331,6 +1347,22 @@ class _$OrderItemDtoCopyWithImpl<$Res, $Val extends OrderItemDto>
? _value.categoryName ? _value.categoryName
: categoryName // ignore: cast_nullable_to_non_nullable : categoryName // ignore: cast_nullable_to_non_nullable
as String?, 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, as $Val,
); );
@@ -1366,6 +1398,10 @@ abstract class _$$OrderItemDtoImplCopyWith<$Res>
@JsonKey(name: "paid_quantity") int? paidQuantity, @JsonKey(name: "paid_quantity") int? paidQuantity,
@JsonKey(name: "category_id") String? categoryId, @JsonKey(name: "category_id") String? categoryId,
@JsonKey(name: "category_name") String? categoryName, @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,
}); });
} }
@@ -1402,6 +1438,10 @@ class __$$OrderItemDtoImplCopyWithImpl<$Res>
Object? paidQuantity = freezed, Object? paidQuantity = freezed,
Object? categoryId = freezed, Object? categoryId = freezed,
Object? categoryName = freezed, Object? categoryName = freezed,
Object? weight = freezed,
Object? unitId = freezed,
Object? unitName = freezed,
Object? unitAbbreviation = freezed,
}) { }) {
return _then( return _then(
_$OrderItemDtoImpl( _$OrderItemDtoImpl(
@@ -1481,6 +1521,22 @@ class __$$OrderItemDtoImplCopyWithImpl<$Res>
? _value.categoryName ? _value.categoryName
: categoryName // ignore: cast_nullable_to_non_nullable : categoryName // ignore: cast_nullable_to_non_nullable
as String?, 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?,
), ),
); );
} }
@@ -1509,6 +1565,10 @@ class _$OrderItemDtoImpl extends _OrderItemDto {
@JsonKey(name: "paid_quantity") this.paidQuantity, @JsonKey(name: "paid_quantity") this.paidQuantity,
@JsonKey(name: "category_id") this.categoryId, @JsonKey(name: "category_id") this.categoryId,
@JsonKey(name: "category_name") this.categoryName, @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, }) : _modifiers = modifiers,
super._(); super._();
@@ -1580,10 +1640,22 @@ class _$OrderItemDtoImpl extends _OrderItemDto {
@override @override
@JsonKey(name: "category_name") @JsonKey(name: "category_name")
final String? categoryName; 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 @override
String toString() { 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, printToChecker: $printToChecker, 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 @override
@@ -1626,7 +1698,13 @@ class _$OrderItemDtoImpl extends _OrderItemDto {
(identical(other.categoryId, categoryId) || (identical(other.categoryId, categoryId) ||
other.categoryId == categoryId) && other.categoryId == categoryId) &&
(identical(other.categoryName, categoryName) || (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) @JsonKey(includeFromJson: false, includeToJson: false)
@@ -1652,6 +1730,10 @@ class _$OrderItemDtoImpl extends _OrderItemDto {
paidQuantity, paidQuantity,
categoryId, categoryId,
categoryName, categoryName,
weight,
unitId,
unitName,
unitAbbreviation,
]); ]);
/// Create a copy of OrderItemDto /// Create a copy of OrderItemDto
@@ -1689,6 +1771,10 @@ abstract class _OrderItemDto extends OrderItemDto {
@JsonKey(name: "paid_quantity") final int? paidQuantity, @JsonKey(name: "paid_quantity") final int? paidQuantity,
@JsonKey(name: "category_id") final String? categoryId, @JsonKey(name: "category_id") final String? categoryId,
@JsonKey(name: "category_name") final String? categoryName, @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; }) = _$OrderItemDtoImpl;
const _OrderItemDto._() : super._(); const _OrderItemDto._() : super._();
@@ -1752,6 +1838,18 @@ abstract class _OrderItemDto extends OrderItemDto {
@override @override
@JsonKey(name: "category_name") @JsonKey(name: "category_name")
String? get categoryName; 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 /// Create a copy of OrderItemDto
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@@ -2136,6 +2234,8 @@ mixin _$OrderItemRequestDto {
int? get unitPrice => throw _privateConstructorUsedError; int? get unitPrice => throw _privateConstructorUsedError;
@JsonKey(name: "notes") @JsonKey(name: "notes")
String? get notes => throw _privateConstructorUsedError; String? get notes => throw _privateConstructorUsedError;
@JsonKey(name: "weight")
double? get weight => throw _privateConstructorUsedError;
/// Serializes this OrderItemRequestDto to a JSON map. /// Serializes this OrderItemRequestDto to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError; Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@@ -2160,6 +2260,7 @@ abstract class $OrderItemRequestDtoCopyWith<$Res> {
@JsonKey(name: "quantity") int? quantity, @JsonKey(name: "quantity") int? quantity,
@JsonKey(name: "unit_price") int? unitPrice, @JsonKey(name: "unit_price") int? unitPrice,
@JsonKey(name: "notes") String? notes, @JsonKey(name: "notes") String? notes,
@JsonKey(name: "weight") double? weight,
}); });
} }
@@ -2183,6 +2284,7 @@ class _$OrderItemRequestDtoCopyWithImpl<$Res, $Val extends OrderItemRequestDto>
Object? quantity = freezed, Object? quantity = freezed,
Object? unitPrice = freezed, Object? unitPrice = freezed,
Object? notes = freezed, Object? notes = freezed,
Object? weight = freezed,
}) { }) {
return _then( return _then(
_value.copyWith( _value.copyWith(
@@ -2206,6 +2308,10 @@ class _$OrderItemRequestDtoCopyWithImpl<$Res, $Val extends OrderItemRequestDto>
? _value.notes ? _value.notes
: notes // ignore: cast_nullable_to_non_nullable : notes // ignore: cast_nullable_to_non_nullable
as String?, as String?,
weight: freezed == weight
? _value.weight
: weight // ignore: cast_nullable_to_non_nullable
as double?,
) )
as $Val, as $Val,
); );
@@ -2227,6 +2333,7 @@ abstract class _$$OrderItemRequestDtoImplCopyWith<$Res>
@JsonKey(name: "quantity") int? quantity, @JsonKey(name: "quantity") int? quantity,
@JsonKey(name: "unit_price") int? unitPrice, @JsonKey(name: "unit_price") int? unitPrice,
@JsonKey(name: "notes") String? notes, @JsonKey(name: "notes") String? notes,
@JsonKey(name: "weight") double? weight,
}); });
} }
@@ -2249,6 +2356,7 @@ class __$$OrderItemRequestDtoImplCopyWithImpl<$Res>
Object? quantity = freezed, Object? quantity = freezed,
Object? unitPrice = freezed, Object? unitPrice = freezed,
Object? notes = freezed, Object? notes = freezed,
Object? weight = freezed,
}) { }) {
return _then( return _then(
_$OrderItemRequestDtoImpl( _$OrderItemRequestDtoImpl(
@@ -2272,6 +2380,10 @@ class __$$OrderItemRequestDtoImplCopyWithImpl<$Res>
? _value.notes ? _value.notes
: notes // ignore: cast_nullable_to_non_nullable : notes // ignore: cast_nullable_to_non_nullable
as String?, as String?,
weight: freezed == weight
? _value.weight
: weight // ignore: cast_nullable_to_non_nullable
as double?,
), ),
); );
} }
@@ -2286,6 +2398,7 @@ class _$OrderItemRequestDtoImpl extends _OrderItemRequestDto {
@JsonKey(name: "quantity") this.quantity, @JsonKey(name: "quantity") this.quantity,
@JsonKey(name: "unit_price") this.unitPrice, @JsonKey(name: "unit_price") this.unitPrice,
@JsonKey(name: "notes") this.notes, @JsonKey(name: "notes") this.notes,
@JsonKey(name: "weight") this.weight,
}) : super._(); }) : super._();
factory _$OrderItemRequestDtoImpl.fromJson(Map<String, dynamic> json) => factory _$OrderItemRequestDtoImpl.fromJson(Map<String, dynamic> json) =>
@@ -2306,10 +2419,13 @@ class _$OrderItemRequestDtoImpl extends _OrderItemRequestDto {
@override @override
@JsonKey(name: "notes") @JsonKey(name: "notes")
final String? notes; final String? notes;
@override
@JsonKey(name: "weight")
final double? weight;
@override @override
String toString() { 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 @override
@@ -2325,7 +2441,8 @@ class _$OrderItemRequestDtoImpl extends _OrderItemRequestDto {
other.quantity == quantity) && other.quantity == quantity) &&
(identical(other.unitPrice, unitPrice) || (identical(other.unitPrice, unitPrice) ||
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) @JsonKey(includeFromJson: false, includeToJson: false)
@@ -2337,6 +2454,7 @@ class _$OrderItemRequestDtoImpl extends _OrderItemRequestDto {
quantity, quantity,
unitPrice, unitPrice,
notes, notes,
weight,
); );
/// Create a copy of OrderItemRequestDto /// Create a copy of OrderItemRequestDto
@@ -2363,6 +2481,7 @@ abstract class _OrderItemRequestDto extends OrderItemRequestDto {
@JsonKey(name: "quantity") final int? quantity, @JsonKey(name: "quantity") final int? quantity,
@JsonKey(name: "unit_price") final int? unitPrice, @JsonKey(name: "unit_price") final int? unitPrice,
@JsonKey(name: "notes") final String? notes, @JsonKey(name: "notes") final String? notes,
@JsonKey(name: "weight") final double? weight,
}) = _$OrderItemRequestDtoImpl; }) = _$OrderItemRequestDtoImpl;
const _OrderItemRequestDto._() : super._(); const _OrderItemRequestDto._() : super._();
@@ -2384,6 +2503,9 @@ abstract class _OrderItemRequestDto extends OrderItemRequestDto {
@override @override
@JsonKey(name: "notes") @JsonKey(name: "notes")
String? get notes; String? get notes;
@override
@JsonKey(name: "weight")
double? get weight;
/// Create a copy of OrderItemRequestDto /// Create a copy of OrderItemRequestDto
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@@ -4025,6 +4147,8 @@ mixin _$AddItemOrderRequestDto {
int? get unitPrice => throw _privateConstructorUsedError; int? get unitPrice => throw _privateConstructorUsedError;
@JsonKey(name: 'notes') @JsonKey(name: 'notes')
String? get notes => throw _privateConstructorUsedError; String? get notes => throw _privateConstructorUsedError;
@JsonKey(name: 'weight')
double? get weight => throw _privateConstructorUsedError;
/// Serializes this AddItemOrderRequestDto to a JSON map. /// Serializes this AddItemOrderRequestDto to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError; Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@@ -4049,6 +4173,7 @@ abstract class $AddItemOrderRequestDtoCopyWith<$Res> {
@JsonKey(name: 'quantity') int? quantity, @JsonKey(name: 'quantity') int? quantity,
@JsonKey(name: 'unit_price') int? unitPrice, @JsonKey(name: 'unit_price') int? unitPrice,
@JsonKey(name: 'notes') String? notes, @JsonKey(name: 'notes') String? notes,
@JsonKey(name: 'weight') double? weight,
}); });
} }
@@ -4075,6 +4200,7 @@ class _$AddItemOrderRequestDtoCopyWithImpl<
Object? quantity = freezed, Object? quantity = freezed,
Object? unitPrice = freezed, Object? unitPrice = freezed,
Object? notes = freezed, Object? notes = freezed,
Object? weight = freezed,
}) { }) {
return _then( return _then(
_value.copyWith( _value.copyWith(
@@ -4098,6 +4224,10 @@ class _$AddItemOrderRequestDtoCopyWithImpl<
? _value.notes ? _value.notes
: notes // ignore: cast_nullable_to_non_nullable : notes // ignore: cast_nullable_to_non_nullable
as String?, as String?,
weight: freezed == weight
? _value.weight
: weight // ignore: cast_nullable_to_non_nullable
as double?,
) )
as $Val, as $Val,
); );
@@ -4119,6 +4249,7 @@ abstract class _$$AddItemOrderRequestDtoImplCopyWith<$Res>
@JsonKey(name: 'quantity') int? quantity, @JsonKey(name: 'quantity') int? quantity,
@JsonKey(name: 'unit_price') int? unitPrice, @JsonKey(name: 'unit_price') int? unitPrice,
@JsonKey(name: 'notes') String? notes, @JsonKey(name: 'notes') String? notes,
@JsonKey(name: 'weight') double? weight,
}); });
} }
@@ -4142,6 +4273,7 @@ class __$$AddItemOrderRequestDtoImplCopyWithImpl<$Res>
Object? quantity = freezed, Object? quantity = freezed,
Object? unitPrice = freezed, Object? unitPrice = freezed,
Object? notes = freezed, Object? notes = freezed,
Object? weight = freezed,
}) { }) {
return _then( return _then(
_$AddItemOrderRequestDtoImpl( _$AddItemOrderRequestDtoImpl(
@@ -4165,6 +4297,10 @@ class __$$AddItemOrderRequestDtoImplCopyWithImpl<$Res>
? _value.notes ? _value.notes
: notes // ignore: cast_nullable_to_non_nullable : notes // ignore: cast_nullable_to_non_nullable
as String?, as String?,
weight: freezed == weight
? _value.weight
: weight // ignore: cast_nullable_to_non_nullable
as double?,
), ),
); );
} }
@@ -4179,6 +4315,7 @@ class _$AddItemOrderRequestDtoImpl extends _AddItemOrderRequestDto {
@JsonKey(name: 'quantity') this.quantity, @JsonKey(name: 'quantity') this.quantity,
@JsonKey(name: 'unit_price') this.unitPrice, @JsonKey(name: 'unit_price') this.unitPrice,
@JsonKey(name: 'notes') this.notes, @JsonKey(name: 'notes') this.notes,
@JsonKey(name: 'weight') this.weight,
}) : super._(); }) : super._();
factory _$AddItemOrderRequestDtoImpl.fromJson(Map<String, dynamic> json) => factory _$AddItemOrderRequestDtoImpl.fromJson(Map<String, dynamic> json) =>
@@ -4199,10 +4336,13 @@ class _$AddItemOrderRequestDtoImpl extends _AddItemOrderRequestDto {
@override @override
@JsonKey(name: 'notes') @JsonKey(name: 'notes')
final String? notes; final String? notes;
@override
@JsonKey(name: 'weight')
final double? weight;
@override @override
String toString() { 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 @override
@@ -4218,7 +4358,8 @@ class _$AddItemOrderRequestDtoImpl extends _AddItemOrderRequestDto {
other.quantity == quantity) && other.quantity == quantity) &&
(identical(other.unitPrice, unitPrice) || (identical(other.unitPrice, unitPrice) ||
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) @JsonKey(includeFromJson: false, includeToJson: false)
@@ -4230,6 +4371,7 @@ class _$AddItemOrderRequestDtoImpl extends _AddItemOrderRequestDto {
quantity, quantity,
unitPrice, unitPrice,
notes, notes,
weight,
); );
/// Create a copy of AddItemOrderRequestDto /// Create a copy of AddItemOrderRequestDto
@@ -4257,6 +4399,7 @@ abstract class _AddItemOrderRequestDto extends AddItemOrderRequestDto {
@JsonKey(name: 'quantity') final int? quantity, @JsonKey(name: 'quantity') final int? quantity,
@JsonKey(name: 'unit_price') final int? unitPrice, @JsonKey(name: 'unit_price') final int? unitPrice,
@JsonKey(name: 'notes') final String? notes, @JsonKey(name: 'notes') final String? notes,
@JsonKey(name: 'weight') final double? weight,
}) = _$AddItemOrderRequestDtoImpl; }) = _$AddItemOrderRequestDtoImpl;
const _AddItemOrderRequestDto._() : super._(); const _AddItemOrderRequestDto._() : super._();
@@ -4278,6 +4421,9 @@ abstract class _AddItemOrderRequestDto extends AddItemOrderRequestDto {
@override @override
@JsonKey(name: 'notes') @JsonKey(name: 'notes')
String? get notes; String? get notes;
@override
@JsonKey(name: 'weight')
double? get weight;
/// Create a copy of AddItemOrderRequestDto /// Create a copy of AddItemOrderRequestDto
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@@ -113,6 +113,10 @@ _$OrderItemDtoImpl _$$OrderItemDtoImplFromJson(Map<String, dynamic> json) =>
paidQuantity: (json['paid_quantity'] as num?)?.toInt(), paidQuantity: (json['paid_quantity'] as num?)?.toInt(),
categoryId: json['category_id'] as String?, categoryId: json['category_id'] as String?,
categoryName: json['category_name'] 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) => Map<String, dynamic> _$$OrderItemDtoImplToJson(_$OrderItemDtoImpl instance) =>
@@ -136,6 +140,10 @@ Map<String, dynamic> _$$OrderItemDtoImplToJson(_$OrderItemDtoImpl instance) =>
'paid_quantity': instance.paidQuantity, 'paid_quantity': instance.paidQuantity,
'category_id': instance.categoryId, 'category_id': instance.categoryId,
'category_name': instance.categoryName, 'category_name': instance.categoryName,
'weight': instance.weight,
'unit_id': instance.unitId,
'unit_name': instance.unitName,
'unit_abbreviation': instance.unitAbbreviation,
}; };
_$OrderRequestDtoImpl _$$OrderRequestDtoImplFromJson( _$OrderRequestDtoImpl _$$OrderRequestDtoImplFromJson(
@@ -174,6 +182,7 @@ _$OrderItemRequestDtoImpl _$$OrderItemRequestDtoImplFromJson(
quantity: (json['quantity'] as num?)?.toInt(), quantity: (json['quantity'] as num?)?.toInt(),
unitPrice: (json['unit_price'] as num?)?.toInt(), unitPrice: (json['unit_price'] as num?)?.toInt(),
notes: json['notes'] as String?, notes: json['notes'] as String?,
weight: (json['weight'] as num?)?.toDouble(),
); );
Map<String, dynamic> _$$OrderItemRequestDtoImplToJson( Map<String, dynamic> _$$OrderItemRequestDtoImplToJson(
@@ -184,6 +193,7 @@ Map<String, dynamic> _$$OrderItemRequestDtoImplToJson(
'quantity': instance.quantity, 'quantity': instance.quantity,
'unit_price': instance.unitPrice, 'unit_price': instance.unitPrice,
'notes': instance.notes, 'notes': instance.notes,
'weight': instance.weight,
}; };
_$PaymentRequestDtoImpl _$$PaymentRequestDtoImplFromJson( _$PaymentRequestDtoImpl _$$PaymentRequestDtoImplFromJson(
@@ -317,6 +327,7 @@ _$AddItemOrderRequestDtoImpl _$$AddItemOrderRequestDtoImplFromJson(
quantity: (json['quantity'] as num?)?.toInt(), quantity: (json['quantity'] as num?)?.toInt(),
unitPrice: (json['unit_price'] as num?)?.toInt(), unitPrice: (json['unit_price'] as num?)?.toInt(),
notes: json['notes'] as String?, notes: json['notes'] as String?,
weight: (json['weight'] as num?)?.toDouble(),
); );
Map<String, dynamic> _$$AddItemOrderRequestDtoImplToJson( Map<String, dynamic> _$$AddItemOrderRequestDtoImplToJson(
@@ -327,4 +338,5 @@ Map<String, dynamic> _$$AddItemOrderRequestDtoImplToJson(
'quantity': instance.quantity, 'quantity': instance.quantity,
'unit_price': instance.unitPrice, 'unit_price': instance.unitPrice,
'notes': instance.notes, 'notes': instance.notes,
'weight': instance.weight,
}; };
@@ -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 @freezed
class ProductDto with _$ProductDto { class ProductDto with _$ProductDto {
const ProductDto._(); const ProductDto._();
@@ -46,11 +65,28 @@ class ProductDto with _$ProductDto {
@JsonKey(name: "created_at") String? createdAt, @JsonKey(name: "created_at") String? createdAt,
@JsonKey(name: "updated_at") String? updatedAt, @JsonKey(name: "updated_at") String? updatedAt,
@JsonKey(name: "variants") List<ProductVariantDto>? variants, @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; }) = _ProductDto;
factory ProductDto.fromJson(Map<String, dynamic> json) => factory ProductDto.fromJson(Map<String, dynamic> json) =>
_$ProductDtoFromJson(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 /// Mapping ke domain
Product toDomain() => Product( Product toDomain() => Product(
id: id ?? '', id: id ?? '',
@@ -70,6 +106,10 @@ class ProductDto with _$ProductDto {
createdAt: createdAt ?? '', createdAt: createdAt ?? '',
updatedAt: updatedAt ?? '', updatedAt: updatedAt ?? '',
variants: variants?.map((v) => v.toDomain()).toList() ?? [], variants: variants?.map((v) => v.toDomain()).toList() ?? [],
sellBy: SellBy.normalize(sellBy),
unitId: _resolvedUnitId,
unitName: _resolvedUnitName,
unitAbbreviation: _resolvedUnitAbbreviation,
); );
Map<String, dynamic> toMap() => { Map<String, dynamic> toMap() => {
@@ -89,6 +129,10 @@ class ProductDto with _$ProductDto {
'is_active': isActive == true ? 1 : 0, 'is_active': isActive == true ? 1 : 0,
'created_at': createdAt, 'created_at': createdAt,
'updated_at': updatedAt, 'updated_at': updatedAt,
'sell_by': SellBy.normalize(sellBy),
'unit_id': _resolvedUnitId,
'unit_name': _resolvedUnitName,
'unit_abbreviation': _resolvedUnitAbbreviation,
}; };
factory ProductDto.fromMap( factory ProductDto.fromMap(
@@ -117,6 +161,10 @@ class ProductDto with _$ProductDto {
createdAt: map['created_at'] as String?, createdAt: map['created_at'] as String?,
updatedAt: map['updated_at'] as String?, updatedAt: map['updated_at'] as String?,
variants: variants, 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( factory ProductDto.fromDomain(Product product) => ProductDto(
@@ -139,6 +187,10 @@ class ProductDto with _$ProductDto {
variants: product.variants variants: product.variants
.map((v) => ProductVariantDto.fromDomain(v)) .map((v) => ProductVariantDto.fromDomain(v))
.toList(), .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 'package:freezed_annotation/freezed_annotation.dart';
import '../../common/types/sell_by.dart';
import '../../domain/product/product.dart'; import '../../domain/product/product.dart';
part 'product_dtos.freezed.dart'; part 'product_dtos.freezed.dart';
@@ -293,6 +293,223 @@ abstract class _ListProductDto extends ListProductDto {
throw _privateConstructorUsedError; 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) { ProductDto _$ProductDtoFromJson(Map<String, dynamic> json) {
return _ProductDto.fromJson(json); return _ProductDto.fromJson(json);
} }
@@ -333,6 +550,16 @@ mixin _$ProductDto {
String? get updatedAt => throw _privateConstructorUsedError; String? get updatedAt => throw _privateConstructorUsedError;
@JsonKey(name: "variants") @JsonKey(name: "variants")
List<ProductVariantDto>? get variants => throw _privateConstructorUsedError; 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. /// Serializes this ProductDto to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError; Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@@ -369,7 +596,14 @@ abstract class $ProductDtoCopyWith<$Res> {
@JsonKey(name: "created_at") String? createdAt, @JsonKey(name: "created_at") String? createdAt,
@JsonKey(name: "updated_at") String? updatedAt, @JsonKey(name: "updated_at") String? updatedAt,
@JsonKey(name: "variants") List<ProductVariantDto>? variants, @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 /// @nodoc
@@ -404,6 +638,11 @@ class _$ProductDtoCopyWithImpl<$Res, $Val extends ProductDto>
Object? createdAt = freezed, Object? createdAt = freezed,
Object? updatedAt = freezed, Object? updatedAt = freezed,
Object? variants = freezed, Object? variants = freezed,
Object? sellBy = freezed,
Object? unitId = freezed,
Object? unitName = freezed,
Object? unitAbbreviation = freezed,
Object? unit = freezed,
}) { }) {
return _then( return _then(
_value.copyWith( _value.copyWith(
@@ -475,10 +714,44 @@ class _$ProductDtoCopyWithImpl<$Res, $Val extends ProductDto>
? _value.variants ? _value.variants
: variants // ignore: cast_nullable_to_non_nullable : variants // ignore: cast_nullable_to_non_nullable
as List<ProductVariantDto>?, 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, 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 /// @nodoc
@@ -508,7 +781,15 @@ abstract class _$$ProductDtoImplCopyWith<$Res>
@JsonKey(name: "created_at") String? createdAt, @JsonKey(name: "created_at") String? createdAt,
@JsonKey(name: "updated_at") String? updatedAt, @JsonKey(name: "updated_at") String? updatedAt,
@JsonKey(name: "variants") List<ProductVariantDto>? variants, @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 /// @nodoc
@@ -542,6 +823,11 @@ class __$$ProductDtoImplCopyWithImpl<$Res>
Object? createdAt = freezed, Object? createdAt = freezed,
Object? updatedAt = freezed, Object? updatedAt = freezed,
Object? variants = freezed, Object? variants = freezed,
Object? sellBy = freezed,
Object? unitId = freezed,
Object? unitName = freezed,
Object? unitAbbreviation = freezed,
Object? unit = freezed,
}) { }) {
return _then( return _then(
_$ProductDtoImpl( _$ProductDtoImpl(
@@ -613,6 +899,26 @@ class __$$ProductDtoImplCopyWithImpl<$Res>
? _value._variants ? _value._variants
: variants // ignore: cast_nullable_to_non_nullable : variants // ignore: cast_nullable_to_non_nullable
as List<ProductVariantDto>?, 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?,
), ),
); );
} }
@@ -639,6 +945,11 @@ class _$ProductDtoImpl extends _ProductDto {
@JsonKey(name: "created_at") this.createdAt, @JsonKey(name: "created_at") this.createdAt,
@JsonKey(name: "updated_at") this.updatedAt, @JsonKey(name: "updated_at") this.updatedAt,
@JsonKey(name: "variants") final List<ProductVariantDto>? variants, @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, }) : _metadata = metadata,
_variants = variants, _variants = variants,
super._(); super._();
@@ -713,9 +1024,25 @@ class _$ProductDtoImpl extends _ProductDto {
return EqualUnmodifiableListView(value); 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 @override
String toString() { 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, printToChecker: $printToChecker, 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 @override
@@ -749,12 +1076,19 @@ class _$ProductDtoImpl extends _ProductDto {
other.createdAt == createdAt) && other.createdAt == createdAt) &&
(identical(other.updatedAt, updatedAt) || (identical(other.updatedAt, updatedAt) ||
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) @JsonKey(includeFromJson: false, includeToJson: false)
@override @override
int get hashCode => Object.hash( int get hashCode => Object.hashAll([
runtimeType, runtimeType,
id, id,
organizationId, organizationId,
@@ -773,7 +1107,12 @@ class _$ProductDtoImpl extends _ProductDto {
createdAt, createdAt,
updatedAt, updatedAt,
const DeepCollectionEquality().hash(_variants), const DeepCollectionEquality().hash(_variants),
); sellBy,
unitId,
unitName,
unitAbbreviation,
unit,
]);
/// Create a copy of ProductDto /// Create a copy of ProductDto
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@@ -808,6 +1147,11 @@ abstract class _ProductDto extends ProductDto {
@JsonKey(name: "created_at") final String? createdAt, @JsonKey(name: "created_at") final String? createdAt,
@JsonKey(name: "updated_at") final String? updatedAt, @JsonKey(name: "updated_at") final String? updatedAt,
@JsonKey(name: "variants") final List<ProductVariantDto>? variants, @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; }) = _$ProductDtoImpl;
const _ProductDto._() : super._(); const _ProductDto._() : super._();
@@ -865,6 +1209,21 @@ abstract class _ProductDto extends ProductDto {
@override @override
@JsonKey(name: "variants") @JsonKey(name: "variants")
List<ProductVariantDto>? get 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 /// Create a copy of ProductDto
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@@ -27,6 +27,21 @@ Map<String, dynamic> _$$ListProductDtoImplToJson(
'total_pages': instance.totalPages, '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 _$$ProductDtoImplFromJson(Map<String, dynamic> json) =>
_$ProductDtoImpl( _$ProductDtoImpl(
id: json['id'] as String?, id: json['id'] as String?,
@@ -48,6 +63,13 @@ _$ProductDtoImpl _$$ProductDtoImplFromJson(Map<String, dynamic> json) =>
variants: (json['variants'] as List<dynamic>?) variants: (json['variants'] as List<dynamic>?)
?.map((e) => ProductVariantDto.fromJson(e as Map<String, dynamic>)) ?.map((e) => ProductVariantDto.fromJson(e as Map<String, dynamic>))
.toList(), .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) => Map<String, dynamic> _$$ProductDtoImplToJson(_$ProductDtoImpl instance) =>
@@ -69,6 +91,11 @@ Map<String, dynamic> _$$ProductDtoImplToJson(_$ProductDtoImpl instance) =>
'created_at': instance.createdAt, 'created_at': instance.createdAt,
'updated_at': instance.updatedAt, 'updated_at': instance.updatedAt,
'variants': instance.variants, 'variants': instance.variants,
'sell_by': instance.sellBy,
'unit_id': instance.unitId,
'unit_name': instance.unitName,
'unit_abbreviation': instance.unitAbbreviation,
'unit': instance.unit,
}; };
_$ProductVariantDtoImpl _$$ProductVariantDtoImplFromJson( _$ProductVariantDtoImpl _$$ProductVariantDtoImplFromJson(
@@ -3,6 +3,7 @@ import 'dart:developer';
import 'package:dartz/dartz.dart'; import 'package:dartz/dartz.dart';
import 'package:injectable/injectable.dart'; import 'package:injectable/injectable.dart';
import '../../../common/service/unit_registry.dart';
import '../../../domain/product/product.dart'; import '../../../domain/product/product.dart';
import '../datasources/local_data_provider.dart'; import '../datasources/local_data_provider.dart';
import '../datasources/remote_data_provider.dart'; import '../datasources/remote_data_provider.dart';
@@ -12,10 +13,38 @@ import '../product_dtos.dart';
class ProductRepository implements IProductRepository { class ProductRepository implements IProductRepository {
final ProductRemoteDataProvider _remoteDataProvider; final ProductRemoteDataProvider _remoteDataProvider;
final ProductLocalDataProvider _localDataProvider; final ProductLocalDataProvider _localDataProvider;
final UnitRegistry _unitRegistry;
final _logName = 'ProductRepository'; 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 @override
Future<Either<ProductFailure, Product>> getProductById(String id) async { 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); log('✅ Product loaded: ${productDomain.name}', name: _logName);
return Right(productDomain); return Right(productDomain);
@@ -85,7 +114,9 @@ class ProductRepository implements IProductRepository {
// 🧱 Bangun entity domain ListProduct // 🧱 Bangun entity domain ListProduct
final result = ListProduct( final result = ListProduct(
products: cachedProducts.data!.map((p) => p.toDomain()).toList(), products: _withUnits(
cachedProducts.data!.map((p) => p.toDomain()).toList(),
),
totalCount: totalCount, totalCount: totalCount,
page: page, page: page,
limit: limit, limit: limit,
@@ -132,7 +163,9 @@ class ProductRepository implements IProductRepository {
return left(cachedProducts.error!); 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( final totalCount = await _localDataProvider.getTotalCount(
categoryId: categoryId, categoryId: categoryId,
@@ -180,7 +213,9 @@ class ProductRepository implements IProductRepository {
} }
// ✅ Mapping ke domain entity (kalau hasilnya masih berupa DTO) // ✅ 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( log(
'✅ Local search completed: ${products.length} results', '✅ Local search completed: ${products.length} results',
@@ -369,7 +404,7 @@ class ProductRepository implements IProductRepository {
final products = result.data!.toDomain(); final products = result.data!.toDomain();
return right(products); return right(products.copyWith(products: _withUnits(products.products)));
} catch (e, s) { } catch (e, s) {
log('getProducts', name: _logName, error: e, stackTrace: s); log('getProducts', name: _logName, error: e, stackTrace: s);
return left(const ProductFailure.unexpectedError()); 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();
}
+8
View File
@@ -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;
}
+25
View File
@@ -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,
};
+51 -24
View File
@@ -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' import 'package:apskel_pos_flutter_v2/common/service/device_info_service.dart'
as _i288; as _i288;
import 'package:apskel_pos_flutter_v2/common/service/fcm_service.dart' as _i312; 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/analytic/analytic.dart' as _i346;
import 'package:apskel_pos_flutter_v2/domain/auth/auth.dart' as _i776; import 'package:apskel_pos_flutter_v2/domain/auth/auth.dart' as _i776;
import 'package:apskel_pos_flutter_v2/domain/category/category.dart' as _i502; 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/printer/printer.dart' as _i104;
import 'package:apskel_pos_flutter_v2/domain/product/product.dart' as _i44; 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/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/env.dart' as _i923;
import 'package:apskel_pos_flutter_v2/infrastructure/analytic/datasources/remote_data_provider.dart' import 'package:apskel_pos_flutter_v2/infrastructure/analytic/datasources/remote_data_provider.dart'
as _i708; as _i708;
@@ -148,6 +151,12 @@ import 'package:apskel_pos_flutter_v2/infrastructure/table/datasources/remote_da
as _i95; as _i95;
import 'package:apskel_pos_flutter_v2/infrastructure/table/repositories/table_repository.dart' import 'package:apskel_pos_flutter_v2/infrastructure/table/repositories/table_repository.dart'
as _i824; 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' import 'package:apskel_pos_flutter_v2/presentation/router/app_router.dart'
as _i800; as _i800;
import 'package:connectivity_plus/connectivity_plus.dart' as _i895; import 'package:connectivity_plus/connectivity_plus.dart' as _i895;
@@ -229,6 +238,9 @@ extension GetItInjectableX on _i174.GetIt {
gh.factory<_i693.OutletLocalDatasource>( gh.factory<_i693.OutletLocalDatasource>(
() => _i693.OutletLocalDatasource(gh<_i460.SharedPreferences>()), () => _i693.OutletLocalDatasource(gh<_i460.SharedPreferences>()),
); );
gh.factory<_i393.UnitLocalDataProvider>(
() => _i393.UnitLocalDataProvider(gh<_i460.SharedPreferences>()),
);
gh.lazySingleton<_i457.ApiClient>( gh.lazySingleton<_i457.ApiClient>(
() => _i457.ApiClient(gh<_i361.Dio>(), gh<_i923.Env>()), () => _i457.ApiClient(gh<_i361.Dio>(), gh<_i923.Env>()),
); );
@@ -279,6 +291,9 @@ extension GetItInjectableX on _i174.GetIt {
gh.factory<_i95.TableRemoteDataProvider>( gh.factory<_i95.TableRemoteDataProvider>(
() => _i95.TableRemoteDataProvider(gh<_i457.ApiClient>()), () => _i95.TableRemoteDataProvider(gh<_i457.ApiClient>()),
); );
gh.factory<_i1016.UnitRemoteDataProvider>(
() => _i1016.UnitRemoteDataProvider(gh<_i457.ApiClient>()),
);
gh.factory<_i776.IAuthRepository>( gh.factory<_i776.IAuthRepository>(
() => _i941.AuthRepository( () => _i941.AuthRepository(
gh<_i370.AuthRemoteDataProvider>(), gh<_i370.AuthRemoteDataProvider>(),
@@ -291,13 +306,6 @@ extension GetItInjectableX on _i174.GetIt {
gh<_i708.CategoryLocalDataProvider>(), gh<_i708.CategoryLocalDataProvider>(),
), ),
); );
gh.factory<_i46.LoginFormBloc>(
() => _i46.LoginFormBloc(
gh<_i776.IAuthRepository>(),
gh<_i312.FcmService>(),
gh<_i288.DeviceInfoService>(),
),
);
gh.factory<_i983.ITableRepository>( gh.factory<_i983.ITableRepository>(
() => _i824.TableRepository( () => _i824.TableRepository(
gh<_i95.TableRemoteDataProvider>(), gh<_i95.TableRemoteDataProvider>(),
@@ -313,12 +321,6 @@ extension GetItInjectableX on _i174.GetIt {
gh.factory<_i424.TableLoaderBloc>( gh.factory<_i424.TableLoaderBloc>(
() => _i424.TableLoaderBloc(gh<_i983.ITableRepository>()), () => _i424.TableLoaderBloc(gh<_i983.ITableRepository>()),
); );
gh.factory<_i44.IProductRepository>(
() => _i763.ProductRepository(
gh<_i707.ProductRemoteDataProvider>(),
gh<_i464.ProductLocalDataProvider>(),
),
);
gh.factory<_i552.IOutletRepository>( gh.factory<_i552.IOutletRepository>(
() => _i845.OutletRepository( () => _i845.OutletRepository(
gh<_i132.OutletRemoteDataProvider>(), gh<_i132.OutletRemoteDataProvider>(),
@@ -342,6 +344,12 @@ extension GetItInjectableX on _i174.GetIt {
gh<_i552.IOutletRepository>(), gh<_i552.IOutletRepository>(),
), ),
); );
gh.factory<_i320.IUnitRepository>(
() => _i228.UnitRepository(
gh<_i1016.UnitRemoteDataProvider>(),
gh<_i393.UnitLocalDataProvider>(),
),
);
gh.factory<_i143.ICustomerRepository>( gh.factory<_i143.ICustomerRepository>(
() => _i385.CustomerRepository(gh<_i841.CustomerRemoteDataProvider>()), () => _i385.CustomerRepository(gh<_i841.CustomerRemoteDataProvider>()),
); );
@@ -363,6 +371,9 @@ extension GetItInjectableX on _i174.GetIt {
gh.factory<_i822.VoidFormBloc>( gh.factory<_i822.VoidFormBloc>(
() => _i822.VoidFormBloc(gh<_i299.IOrderRepository>()), () => _i822.VoidFormBloc(gh<_i299.IOrderRepository>()),
); );
gh.lazySingleton<_i584.UnitRegistry>(
() => _i584.UnitRegistry(gh<_i320.IUnitRepository>()),
);
gh.factory<_i683.CustomerLoaderBloc>( gh.factory<_i683.CustomerLoaderBloc>(
() => _i683.CustomerLoaderBloc(gh<_i143.ICustomerRepository>()), () => _i683.CustomerLoaderBloc(gh<_i143.ICustomerRepository>()),
); );
@@ -381,19 +392,19 @@ extension GetItInjectableX on _i174.GetIt {
gh.factory<_i76.OutletLoaderBloc>( gh.factory<_i76.OutletLoaderBloc>(
() => _i76.OutletLoaderBloc(gh<_i552.IOutletRepository>()), () => _i76.OutletLoaderBloc(gh<_i552.IOutletRepository>()),
); );
gh.factory<_i13.ProductLoaderBloc>( gh.factory<_i46.LoginFormBloc>(
() => _i13.ProductLoaderBloc(gh<_i44.IProductRepository>()), () => _i46.LoginFormBloc(
); gh<_i776.IAuthRepository>(),
gh.factory<_i741.SyncBloc>( gh<_i312.FcmService>(),
() => _i741.SyncBloc( gh<_i288.DeviceInfoService>(),
gh<_i44.IProductRepository>(), gh<_i584.UnitRegistry>(),
gh<_i502.ICategoryRepository>(),
), ),
); );
gh.factory<_i729.SyncSettingBloc>( gh.factory<_i44.IProductRepository>(
() => _i729.SyncSettingBloc( () => _i763.ProductRepository(
gh<_i44.IProductRepository>(), gh<_i707.ProductRemoteDataProvider>(),
gh<_i502.ICategoryRepository>(), gh<_i464.ProductLocalDataProvider>(),
gh<_i584.UnitRegistry>(),
), ),
); );
gh.factory<_i911.CategoryAnalyticLoaderBloc>( gh.factory<_i911.CategoryAnalyticLoaderBloc>(
@@ -419,6 +430,22 @@ extension GetItInjectableX on _i174.GetIt {
gh.factory<_i413.SalesAnalyticLoaderBloc>( gh.factory<_i413.SalesAnalyticLoaderBloc>(
() => _i413.SalesAnalyticLoaderBloc(gh<_i346.IAnalyticRepository>()), () => _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; return this;
} }
} }
+5
View File
@@ -7,6 +7,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'common/service/fcm_service.dart'; import 'common/service/fcm_service.dart';
import 'common/service/unit_registry.dart';
import 'injection.dart'; import 'injection.dart';
import 'presentation/app_widget.dart'; import 'presentation/app_widget.dart';
@@ -49,6 +50,10 @@ void main() async {
// Inisialisasi FCM setelah DI siap // Inisialisasi FCM setelah DI siap
await getIt<FcmService>().initialize(); 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()); runApp(const AppWidget());
}, },
(error, stack) { (error, stack) {
+109 -63
View File
@@ -5,6 +5,7 @@ import '../../../application/checkout/checkout_form/checkout_form_bloc.dart';
import '../../../common/extension/extension.dart'; import '../../../common/extension/extension.dart';
import '../../../common/theme/theme.dart'; import '../../../common/theme/theme.dart';
import '../../../domain/product/product.dart'; import '../../../domain/product/product.dart';
import '../dialog/dialog.dart';
import '../image/image.dart'; import '../image/image.dart';
import '../spaces/space.dart'; import '../spaces/space.dart';
@@ -26,8 +27,8 @@ class _OrderMenuState extends State<OrderMenu> {
_controller.addListener(() { _controller.addListener(() {
context.read<CheckoutFormBloc>().add( context.read<CheckoutFormBloc>().add(
CheckoutFormEvent.updateItemNotes( CheckoutFormEvent.updateLineNotes(
widget.data.product, widget.data.lineId,
_controller.text, _controller.text,
), ),
); );
@@ -40,6 +41,29 @@ class _OrderMenuState extends State<OrderMenu> {
super.dispose(); 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Column( return Column(
@@ -77,9 +101,10 @@ class _OrderMenuState extends State<OrderMenu> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
(widget.data.product.price + widget.data.isWeighted &&
(widget.data.variant?.priceModifier ?? 0)) widget.data.product.unitLabel.isNotEmpty
.currencyFormatRp, ? '${widget.data.unitPrice.currencyFormatRp} / ${widget.data.product.unitLabel}'
: widget.data.unitPrice.currencyFormatRp,
), ),
if (widget.data.variant != null) if (widget.data.variant != null)
Text(widget.data.variant?.name ?? ""), Text(widget.data.variant?.name ?? ""),
@@ -87,60 +112,15 @@ class _OrderMenuState extends State<OrderMenu> {
), ),
), ),
), ),
Row( if (widget.data.isWeighted)
children: [ _buildWeightControl()
GestureDetector( else
onTap: () { _buildQuantityStepper(),
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,
),
),
),
],
),
const SpaceWidth(8), const SpaceWidth(8),
SizedBox( SizedBox(
width: 80.0, width: 80.0,
child: Text( child: Text(
((widget.data.product.price + widget.data.lineTotal.currencyFormatRp,
(widget.data.variant?.priceModifier ?? 0)) *
widget.data.quantity)
.currencyFormatRp,
textAlign: TextAlign.right, textAlign: TextAlign.right,
style: const TextStyle( style: const TextStyle(
color: AppColor.primary, color: AppColor.primary,
@@ -167,14 +147,7 @@ class _OrderMenuState extends State<OrderMenu> {
), ),
const SpaceWidth(16.0), const SpaceWidth(16.0),
GestureDetector( GestureDetector(
onTap: () { onTap: _removeLine,
context.read<CheckoutFormBloc>().add(
CheckoutFormEvent.removeItem(
widget.data.product,
widget.data.variant,
),
);
},
child: Container( child: Container(
height: 40, height: 40,
width: 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) { Widget build(BuildContext context) {
return GestureDetector( return GestureDetector(
onTap: () { onTap: () {
if (product.isActive == true) { if (product.isActive != true) return;
if (product.variants.isEmpty) {
context.read<CheckoutFormBloc>().add( if (product.variants.isNotEmpty) {
CheckoutFormEvent.addItem(product, null), showDialog(
); context: context,
} else { builder: (context) => VariantDialog(product: product),
showDialog( );
context: context, return;
builder: (context) => VariantDialog(product: product),
);
}
} }
// 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( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
@@ -75,7 +92,11 @@ class ProductCard extends StatelessWidget {
Align( Align(
alignment: Alignment.center, alignment: Alignment.center,
child: Text( child: Text(
product.price.currencyFormatRp, product.isSoldByWeight
? '${product.price.currencyFormatRp} ${product.priceUnitLabel}'
.trim()
: product.price.currencyFormatRp,
textAlign: TextAlign.center,
style: const TextStyle( style: const TextStyle(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
fontSize: 12, fontSize: 12,
@@ -89,31 +110,39 @@ class ProductCard extends StatelessWidget {
BlocBuilder<CheckoutFormBloc, CheckoutFormState>( BlocBuilder<CheckoutFormBloc, CheckoutFormState>(
builder: (context, state) { builder: (context, state) {
final totalQuantity = state.items final lines = state.items
.where((item) => item.product.id == product.id) .where((item) => item.product.id == product.id)
.map((item) => item.quantity) .toList();
.fold(0, (sum, qty) => sum + qty);
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( return Positioned(
top: 4, top: 4,
right: 4, right: 4,
child: Container( child: Container(
width: 40, constraints: const BoxConstraints(minWidth: 40),
height: 40, height: 40,
padding: const EdgeInsets.all(6), padding: const EdgeInsets.symmetric(horizontal: 6),
decoration: const BoxDecoration( decoration: const BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(9.0)), borderRadius: BorderRadius.all(Radius.circular(9.0)),
color: AppColor.primary, color: AppColor.primary,
), ),
child: Center( child: Center(
child: Text( child: Text(
totalQuantity.toString(), label,
style: const TextStyle( style: TextStyle(
color: Colors.white, color: Colors.white,
fontSize: 20, fontSize: product.isSoldByWeight ? 13 : 20,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
), ),
@@ -24,6 +24,7 @@ import '../spaces/space.dart';
part 'custom_modal_dialog.dart'; part 'custom_modal_dialog.dart';
part 'outlet_dialog.dart'; part 'outlet_dialog.dart';
part 'variant_dialog.dart'; part 'variant_dialog.dart';
part 'weight_input_dialog.dart';
part 'delivery_dialog.dart'; part 'delivery_dialog.dart';
part 'order_type_dialog.dart'; part 'order_type_dialog.dart';
part 'printer_bluetooth_dialog.dart'; part 'printer_bluetooth_dialog.dart';
@@ -163,7 +163,9 @@ class OrderSaveConfirmDialog extends StatelessWidget {
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
Text( Text(
'${item.quantity} qty', item.isWeighted
? item.quantityLabel
: '${item.quantity} qty',
style: AppStyle.sm.copyWith( style: AppStyle.sm.copyWith(
color: AppColor.textSecondary, color: AppColor.textSecondary,
), ),
@@ -185,8 +187,7 @@ class OrderSaveConfirmDialog extends StatelessWidget {
), ),
), ),
child: Text( child: Text(
((item.product.price) * item.quantity) item.lineTotal.currencyFormatRpV2,
.currencyFormatRpV2,
style: AppStyle.md.copyWith( style: AppStyle.md.copyWith(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: AppColor.primary, color: AppColor.primary,
@@ -21,10 +21,33 @@ class VariantDialog extends StatelessWidget {
return GestureDetector( return GestureDetector(
onTap: () { onTap: () {
// Aksi saat varian dipilih // Aksi saat varian dipilih
context.maybePop(); final bloc = context.read<CheckoutFormBloc>();
context.read<CheckoutFormBloc>().add( // Context dialog ini mati begitu di-pop, jadi dialog lanjutan
CheckoutFormEvent.addItem(product, variant), // 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( child: Container(
width: (context.deviceWidth * 0.4 - 12 - 32) / 2 - 6, // 2 per row 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),
),
),
);
}
}
@@ -77,6 +77,7 @@ class PrintUi {
bytes += builder.orderItem( bytes += builder.orderItem(
productName: item.productName, productName: item.productName,
quantity: item.quantity, quantity: item.quantity,
quantityLabel: item.isWeighted ? item.quantityLabel : null,
unitPrice: item.unitPrice.currencyFormatRpV2, unitPrice: item.unitPrice.currencyFormatRpV2,
totalPrice: item.totalPrice.currencyFormatRpV2, totalPrice: item.totalPrice.currencyFormatRpV2,
variantName: item.productVariantName, variantName: item.productVariantName,
@@ -140,7 +141,7 @@ class PrintUi {
// Items — qty NAMA (uppercase), variant indent // Items — qty NAMA (uppercase), variant indent
for (final item in order.orderItems.where((e) => e.printToChecker)) { for (final item in order.orderItems.where((e) => e.printToChecker)) {
final name = item.productName.toUpperCase(); final name = item.productName.toUpperCase();
bytes += builder.itemText('${item.quantity} $name'); bytes += builder.itemText('${item.quantityLabel} $name');
if (item.productVariantName.isNotEmpty) { if (item.productVariantName.isNotEmpty) {
bytes += builder.itemText(' ${item.productVariantName.toUpperCase()}', bold: false); bytes += builder.itemText(' ${item.productVariantName.toUpperCase()}', bold: false);
} }
@@ -196,7 +197,7 @@ class PrintUi {
// Single item // Single item
final name = item.productName.toUpperCase(); final name = item.productName.toUpperCase();
bytes += builder.itemText('${item.quantity} $name'); bytes += builder.itemText('${item.quantityLabel} $name');
if (item.productVariantName.isNotEmpty) { if (item.productVariantName.isNotEmpty) {
bytes += builder.itemText(' ${item.productVariantName.toUpperCase()}', bold: false); bytes += builder.itemText(' ${item.productVariantName.toUpperCase()}', bold: false);
} }
@@ -252,7 +253,7 @@ class PrintUi {
// Single item // Single item
final name = item.productName.toUpperCase(); final name = item.productName.toUpperCase();
bytes += builder.itemText('${item.quantity} $name'); bytes += builder.itemText('${item.quantityLabel} $name');
if (item.productVariantName.isNotEmpty) { if (item.productVariantName.isNotEmpty) {
bytes += builder.itemText(' ${item.productVariantName.toUpperCase()}', bold: false); bytes += builder.itemText(' ${item.productVariantName.toUpperCase()}', bold: false);
} }
@@ -359,6 +360,7 @@ class PrintUi {
bytes += builder.orderItem( bytes += builder.orderItem(
productName: item.productName, productName: item.productName,
quantity: item.quantity, quantity: item.quantity,
quantityLabel: item.isWeighted ? item.quantityLabel : null,
unitPrice: item.unitPrice.currencyFormatRpV2, unitPrice: item.unitPrice.currencyFormatRpV2,
totalPrice: item.totalPrice.currencyFormatRpV2, totalPrice: item.totalPrice.currencyFormatRpV2,
variantName: item.productVariantName, variantName: item.productVariantName,
@@ -439,6 +441,7 @@ class PrintUi {
bytes += builder.orderItem( bytes += builder.orderItem(
productName: item.productName, productName: item.productName,
quantity: item.quantity, quantity: item.quantity,
quantityLabel: item.isWeighted ? item.quantityLabel : null,
unitPrice: item.unitPrice.currencyFormatRpV2, unitPrice: item.unitPrice.currencyFormatRpV2,
totalPrice: item.totalPrice.currencyFormatRpV2, totalPrice: item.totalPrice.currencyFormatRpV2,
variantName: item.productVariantName, variantName: item.productVariantName,
@@ -530,6 +533,7 @@ class PrintUi {
bytes += builder.orderItem( bytes += builder.orderItem(
productName: item.productName, productName: item.productName,
quantity: item.quantity, quantity: item.quantity,
quantityLabel: item.isWeighted ? item.quantityLabel : null,
unitPrice: item.unitPrice.currencyFormatRpV2, unitPrice: item.unitPrice.currencyFormatRpV2,
totalPrice: item.totalPrice.currencyFormatRpV2, totalPrice: item.totalPrice.currencyFormatRpV2,
variantName: item.productVariantName, variantName: item.productVariantName,
@@ -301,6 +301,10 @@ class ReceiptComponentBuilder {
List<int> orderItem({ List<int> orderItem({
required String productName, required String productName,
required int quantity, 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 unitPrice,
required String totalPrice, required String totalPrice,
String? variantName, String? variantName,
@@ -314,7 +318,10 @@ class ReceiptComponentBuilder {
? '$productName ($variantName)' ? '$productName ($variantName)'
: productName; : productName;
bytes += textLeft(displayName, bold: true, fontType: fontType, height: height ?? _bodyHeight, width: width ?? _bodyWidth); bytes += textLeft(displayName, bold: true, fontType: fontType, height: height ?? _bodyHeight, width: width ?? _bodyWidth);
bytes += row2Columns('${quantity}x $unitPrice', totalPrice, 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) { if (notes != null && notes.isNotEmpty) {
bytes += row2Columns('Note', notes, fontType: fontType, height: height ?? _bodyHeight, width: width ?? _bodyWidth, leftWidth: 4, rightWidth: 8); bytes += row2Columns('Note', notes, fontType: fontType, height: height ?? _bodyHeight, width: width ?? _bodyWidth, leftWidth: 4, rightWidth: 8);
} }
@@ -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/payment_method/payment_method_loader/payment_method_loader_bloc.dart';
import '../../../application/table/table_loader/table_loader_bloc.dart'; import '../../../application/table/table_loader/table_loader_bloc.dart';
import '../../../common/theme/theme.dart'; import '../../../common/theme/theme.dart';
import '../../../domain/product/product.dart';
import '../../../injection.dart'; import '../../../injection.dart';
import '../../components/spaces/space.dart'; import '../../components/spaces/space.dart';
import '../../components/toast/flushbar.dart'; import '../../components/toast/flushbar.dart';
@@ -86,14 +87,7 @@ class CheckoutPage extends StatelessWidget implements AutoRouteWrapper {
tag: 'checkout_screen', tag: 'checkout_screen',
child: BlocBuilder<CheckoutFormBloc, CheckoutFormState>( child: BlocBuilder<CheckoutFormBloc, CheckoutFormState>(
builder: (context, state) { builder: (context, state) {
final price = state.items.fold( final price = state.items.cartTotal.toInt();
0,
(previousValue, element) =>
previousValue +
(element.product.price.toInt() +
(element.variant?.priceModifier.toInt() ?? 0)) *
element.quantity,
);
return Scaffold( return Scaffold(
backgroundColor: AppColor.white, backgroundColor: AppColor.white,
@@ -7,6 +7,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../../../application/checkout/checkout_form/checkout_form_bloc.dart'; import '../../../../../../application/checkout/checkout_form/checkout_form_bloc.dart';
import '../../../../../../common/extension/extension.dart'; import '../../../../../../common/extension/extension.dart';
import '../../../../../../common/theme/theme.dart'; import '../../../../../../common/theme/theme.dart';
import '../../../../../../domain/product/product.dart';
import '../../../../../components/button/button.dart'; import '../../../../../components/button/button.dart';
import '../../../../../components/card/order_menu.dart'; import '../../../../../components/card/order_menu.dart';
import '../../../../../components/spaces/space.dart'; import '../../../../../components/spaces/space.dart';
@@ -21,15 +22,7 @@ class HomeRightPanel extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocBuilder<CheckoutFormBloc, CheckoutFormState>( return BlocBuilder<CheckoutFormBloc, CheckoutFormState>(
builder: (context, state) { builder: (context, state) {
final price = state.items.isEmpty final price = state.items.cartTotal;
? 0.0
: state.items
.map(
(e) =>
(e.product.price + (e.variant?.priceModifier ?? 0)) *
e.quantity,
)
.reduce((value, element) => value + element);
log('🛒 Total Price: $price'); log('🛒 Total Price: $price');
return Align( return Align(
alignment: Alignment.topCenter, alignment: Alignment.topCenter,
@@ -87,8 +80,12 @@ class HomeRightPanel extends StatelessWidget {
: ListView.separated( : ListView.separated(
shrinkWrap: true, shrinkWrap: true,
padding: const EdgeInsets.symmetric(horizontal: 16), padding: const EdgeInsets.symmetric(horizontal: 16),
itemBuilder: (context, index) => itemBuilder: (context, index) => OrderMenu(
OrderMenu(data: state.items[index]), // 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) => separatorBuilder: (context, index) =>
const SpaceHeight(1.0), const SpaceHeight(1.0),
itemCount: state.items.length, itemCount: state.items.length,
@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import '../../../../../../application/checkout/checkout_form/checkout_form_bloc.dart'; import '../../../../../../application/checkout/checkout_form/checkout_form_bloc.dart';
import '../../../../../../common/extension/extension.dart'; import '../../../../../../common/extension/extension.dart';
import '../../../../../../common/theme/theme.dart'; import '../../../../../../common/theme/theme.dart';
import '../../../../../../domain/product/product.dart';
import '../../../../../components/spaces/space.dart'; import '../../../../../components/spaces/space.dart';
class SuccessAddItemOrderRightPanel extends StatelessWidget { class SuccessAddItemOrderRightPanel extends StatelessWidget {
@@ -86,14 +87,7 @@ class SuccessAddItemOrderRightPanel extends StatelessWidget {
], ],
), ),
Text( Text(
(checkoutState.items.fold( checkoutState.items.cartTotal.toInt().toString().currencyFormatRpV2,
0,
(previousValue, element) =>
previousValue +
(element.product.price.toInt() +
(element.variant?.priceModifier.toInt() ?? 0)) *
element.quantity,
)).toString().currencyFormatRpV2,
style: AppStyle.md.copyWith(fontWeight: FontWeight.w600), style: AppStyle.md.copyWith(fontWeight: FontWeight.w600),
), ),
], ],
@@ -165,15 +159,10 @@ class SuccessAddItemOrderRightPanel extends StatelessWidget {
], ],
), ),
child: Text( child: Text(
(checkoutState.items.fold( checkoutState.items.cartTotal
0, .toInt()
(previousValue, element) => .toString()
previousValue + .currencyFormatRpV2,
(element.product.price.toInt() +
(element.variant?.priceModifier.toInt() ??
0)) *
element.quantity,
)).toString().currencyFormatRpV2,
style: AppStyle.xl.copyWith( style: AppStyle.xl.copyWith(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Colors.white, color: Colors.white,
@@ -270,7 +259,7 @@ class SuccessAddItemOrderRightPanel extends StatelessWidget {
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
), ),
child: Text( child: Text(
'${item.quantity}x', (item.isWeighted ? item.quantityLabel : "${item.quantity}x"),
style: AppStyle.md.copyWith( style: AppStyle.md.copyWith(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Colors.white, color: Colors.white,
@@ -279,11 +268,7 @@ class SuccessAddItemOrderRightPanel extends StatelessWidget {
), ),
const SpaceHeight(8), const SpaceHeight(8),
Text( Text(
((item.product.price.toInt() + item.lineTotal.toInt().toString().currencyFormatRpV2,
(item.variant?.priceModifier.toInt() ?? 0)) *
item.quantity)
.toString()
.currencyFormatRpV2,
style: AppStyle.lg.copyWith( style: AppStyle.lg.copyWith(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: AppColor.primary, color: AppColor.primary,
@@ -255,7 +255,7 @@ class SuccessOrderRightPanel extends StatelessWidget {
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
), ),
child: Text( child: Text(
'${item.quantity}x', (item.isWeighted ? item.quantityLabel : "${item.quantity}x"),
style: AppStyle.md.copyWith( style: AppStyle.md.copyWith(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Colors.white, color: Colors.white,
@@ -211,7 +211,9 @@ class OrderList extends StatelessWidget {
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
), ),
child: Text( child: Text(
'x${product.quantity}', (product.isWeighted
? product.quantityLabel
: "x${product.quantity}"),
style: AppStyle.md.copyWith( style: AppStyle.md.copyWith(
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: AppColor.primary, color: AppColor.primary,
@@ -255,7 +255,7 @@ class PaymentSuccessRightPanel extends StatelessWidget {
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
), ),
child: Text( child: Text(
'${item.quantity}x', (item.isWeighted ? item.quantityLabel : "${item.quantity}x"),
style: AppStyle.md.copyWith( style: AppStyle.md.copyWith(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Colors.white, color: Colors.white,
@@ -118,7 +118,7 @@ class PaymentSuccessSplitBillRightPanel extends StatelessWidget {
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
), ),
child: Text( child: Text(
'${item.quantity}x', (item.isWeighted ? item.quantityLabel : "${item.quantity}x"),
style: AppStyle.md.copyWith( style: AppStyle.md.copyWith(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Colors.white, color: Colors.white,
@@ -65,7 +65,9 @@ class PaymentLeftPanel extends StatelessWidget {
), ),
), ),
subtitle: Text( subtitle: Text(
'Qty: ${item.quantity} | ${item.productVariantName}', item.isWeighted
? '${item.quantityLabel} | ${item.productVariantName}'
: 'Qty: ${item.quantity} | ${item.productVariantName}',
style: AppStyle.sm, style: AppStyle.sm,
), ),
trailing: Text( trailing: Text(
@@ -255,7 +255,7 @@ class RefundSuccessRightPanel extends StatelessWidget {
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
), ),
child: Text( child: Text(
'${item.quantity}x', (item.isWeighted ? item.quantityLabel : "${item.quantity}x"),
style: AppStyle.md.copyWith( style: AppStyle.md.copyWith(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Colors.white, color: Colors.white,
@@ -62,7 +62,9 @@ class RefundLeftPanel extends StatelessWidget {
), ),
), ),
Text( Text(
'Qty: ${item.quantity} x Rp ${item.unitPrice.currencyFormatRpV2} ', item.isWeighted
? item.quantityPriceLabel
: 'Qty: ${item.quantity} x ${item.unitPrice.currencyFormatRpV2}',
style: AppStyle.md.copyWith( style: AppStyle.md.copyWith(
color: AppColor.textSecondary, color: AppColor.textSecondary,
), ),
@@ -160,7 +160,9 @@ class SplitBillLeftPanel extends StatelessWidget {
), ),
), ),
Text( Text(
'Qty: ${item.quantity} x Rp ${item.unitPrice.currencyFormatRpV2} ', item.isWeighted
? item.quantityPriceLabel
: 'Qty: ${item.quantity} x ${item.unitPrice.currencyFormatRpV2}',
style: AppStyle.md.copyWith( style: AppStyle.md.copyWith(
color: AppColor.textSecondary, color: AppColor.textSecondary,
), ),
@@ -332,97 +332,148 @@ class _SplitBillRightPanelState extends State<SplitBillRightPanel> {
), ),
), ),
Text( Text(
'${item.unitPrice.currencyFormatRpV2} per item', item.isWeighted
? '${item.unitPrice.currencyFormatRpV2} per ${item.unitLabel}'
: '${item.unitPrice.currencyFormatRpV2} per item',
style: AppStyle.sm.copyWith(color: AppColor.textSecondary), style: AppStyle.sm.copyWith(color: AppColor.textSecondary),
), ),
], ],
), ),
), ),
Row( // Baris berbobot hanya punya dua keadaan: belum dibayar atau lunas.
children: [ // Tidak ada nilai di antaranya, jadi stepper diganti tombol pilih.
GestureDetector( if (item.isWeighted)
onTap: () { _buildWeightedLineSelector(item, selectedQty > 0, maxQty)
if (selectedQty > 0) { else
setState(() { _buildQuantityStepper(item, selectedQty, maxQty),
context.read<SplitBillFormBloc>().add(
SplitBillFormEvent.itemQuantityChanged(
itemId: item.id,
quantity: selectedQty - 1,
),
);
});
}
},
child: Container(
width: 32,
height: 32,
decoration: BoxDecoration(
color: selectedQty > 0 ? AppColor.primary : AppColor.border,
borderRadius: BorderRadius.circular(4),
),
child: Icon(
Icons.remove,
size: 16,
color: selectedQty > 0
? Colors.white
: AppColor.textSecondary,
),
),
),
Container(
width: 40,
height: 32,
margin: const EdgeInsets.symmetric(horizontal: 8),
decoration: BoxDecoration(
border: Border.all(color: AppColor.border),
borderRadius: BorderRadius.circular(4),
),
child: Center(
child: Text(
'$selectedQty',
style: AppStyle.md.copyWith(
fontWeight: FontWeight.bold,
color: AppColor.textPrimary,
),
),
),
),
GestureDetector(
onTap: () {
if (selectedQty < maxQty) {
context.read<SplitBillFormBloc>().add(
SplitBillFormEvent.itemQuantityChanged(
itemId: item.id,
quantity: selectedQty + 1,
),
);
}
},
child: Container(
width: 32,
height: 32,
decoration: BoxDecoration(
color: selectedQty < maxQty
? AppColor.primary
: AppColor.border,
borderRadius: BorderRadius.circular(4),
),
child: Icon(
Icons.add,
size: 16,
color: selectedQty < maxQty
? Colors.white
: AppColor.textSecondary,
),
),
),
],
),
], ],
), ),
); );
} }
Widget _buildWeightedLineSelector(OrderItem item, bool selected, int maxQty) {
final canSelect = selected || maxQty > 0;
return GestureDetector(
onTap: () {
if (!canSelect) return;
context.read<SplitBillFormBloc>().add(
SplitBillFormEvent.itemQuantityChanged(
itemId: item.id,
quantity: selected ? 0 : 1,
),
);
},
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: selected ? AppColor.primary : Colors.transparent,
border: Border.all(
color: canSelect ? AppColor.primary : AppColor.border,
),
borderRadius: BorderRadius.circular(4),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
selected ? Icons.check_circle : Icons.radio_button_unchecked,
size: 16,
color: selected ? Colors.white : AppColor.textSecondary,
),
const SizedBox(width: 6),
Text(
selected ? 'Dibayar' : 'Bayar baris',
style: AppStyle.sm.copyWith(
fontWeight: FontWeight.w600,
color: selected ? Colors.white : AppColor.textSecondary,
),
),
],
),
),
);
}
Widget _buildQuantityStepper(OrderItem item, int selectedQty, int maxQty) {
return Row(
children: [
GestureDetector(
onTap: () {
if (selectedQty > 0) {
setState(() {
context.read<SplitBillFormBloc>().add(
SplitBillFormEvent.itemQuantityChanged(
itemId: item.id,
quantity: selectedQty - 1,
),
);
});
}
},
child: Container(
width: 32,
height: 32,
decoration: BoxDecoration(
color: selectedQty > 0 ? AppColor.primary : AppColor.border,
borderRadius: BorderRadius.circular(4),
),
child: Icon(
Icons.remove,
size: 16,
color: selectedQty > 0 ? Colors.white : AppColor.textSecondary,
),
),
),
Container(
width: 40,
height: 32,
margin: const EdgeInsets.symmetric(horizontal: 8),
decoration: BoxDecoration(
border: Border.all(color: AppColor.border),
borderRadius: BorderRadius.circular(4),
),
child: Center(
child: Text(
'$selectedQty',
style: AppStyle.md.copyWith(
fontWeight: FontWeight.bold,
color: AppColor.textPrimary,
),
),
),
),
GestureDetector(
onTap: () {
if (selectedQty < maxQty) {
context.read<SplitBillFormBloc>().add(
SplitBillFormEvent.itemQuantityChanged(
itemId: item.id,
quantity: selectedQty + 1,
),
);
}
},
child: Container(
width: 32,
height: 32,
decoration: BoxDecoration(
color: selectedQty < maxQty ? AppColor.primary : AppColor.border,
borderRadius: BorderRadius.circular(4),
),
child: Icon(
Icons.add,
size: 16,
color: selectedQty < maxQty
? Colors.white
: AppColor.textSecondary,
),
),
),
],
);
}
Widget _buildRowButtonSplit() { Widget _buildRowButtonSplit() {
switch (widget.state.order.splitType) { switch (widget.state.order.splitType) {
case 'AMOUNT': case 'AMOUNT':
@@ -510,7 +561,10 @@ class _SplitBillRightPanelState extends State<SplitBillRightPanel> {
splitItems.add( splitItems.add(
originalItem.copyWith( originalItem.copyWith(
quantity: quantity, quantity: quantity,
totalPrice: (originalItem.unitPrice) * quantity, // Baris berbobot dibayar penuh; nilainya sudah weight x harga.
totalPrice: originalItem.isWeighted
? originalItem.totalPrice
: (originalItem.unitPrice) * quantity,
), ),
); );
} }
@@ -71,7 +71,9 @@ class VoidProductCard extends StatelessWidget {
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
), ),
child: Text( child: Text(
'Qty: ${item.quantity}', item.isWeighted
? item.quantityLabel
: 'Qty: ${item.quantity}',
style: AppStyle.sm.copyWith( style: AppStyle.sm.copyWith(
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
color: Colors.blue[700], color: Colors.blue[700],
@@ -112,65 +114,13 @@ class VoidProductCard extends StatelessWidget {
), ),
if (canSelect) ...[ if (canSelect) ...[
Container( // Baris berbobot bersifat utuh: dibatalkan seluruhnya atau
decoration: BoxDecoration( // tidak sama sekali, jadi yang ditawarkan tombol pilih baris
border: Border.all(color: AppColor.border), // — bukan stepper jumlah.
borderRadius: BorderRadius.circular(6), if (item.isWeighted)
), _buildLineSelector()
child: Row( else
mainAxisSize: MainAxisSize.min, _buildQuantityStepper(),
children: [
// Decrease Button
InkWell(
onTap: onTapDecrease,
child: Container(
padding: const EdgeInsets.all(8),
child: Icon(
Icons.remove,
size: 16,
color: voidQty > 0
? AppColor.primary
: Colors.grey[400],
),
),
),
// Quantity Display
Container(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
decoration: const BoxDecoration(
border: Border.symmetric(
vertical: BorderSide(color: AppColor.border),
),
),
child: Text(
'$voidQty',
style: AppStyle.sm.copyWith(
fontWeight: FontWeight.w600,
),
),
),
// Increase Button
InkWell(
onTap: onTapIncrease,
child: Container(
padding: const EdgeInsets.all(8),
child: Icon(
Icons.add,
size: 16,
color: voidQty < (item.quantity)
? AppColor.primary
: Colors.grey[400],
),
),
),
],
),
),
const SpaceWidth(20), const SpaceWidth(20),
], ],
// Price // Price
@@ -195,7 +145,7 @@ class VoidProductCard extends StatelessWidget {
borderRadius: BorderRadius.circular(4), borderRadius: BorderRadius.circular(4),
), ),
child: Text( child: Text(
'Void: ${((item.unitPrice) * voidQty).currencyFormatRpV2}', 'Void: ${(item.isWeighted ? item.totalPrice : (item.unitPrice) * voidQty).currencyFormatRpV2}',
style: AppStyle.sm.copyWith( style: AppStyle.sm.copyWith(
fontSize: 12, fontSize: 12,
color: AppColor.error, color: AppColor.error,
@@ -214,4 +164,97 @@ class VoidProductCard extends StatelessWidget {
), ),
); );
} }
/// Pilih atau lepas satu baris berbobot secara utuh.
Widget _buildLineSelector() {
final selected = voidQty > 0;
return InkWell(
onTap: selected ? onTapDecrease : onTapIncrease,
borderRadius: BorderRadius.circular(6),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: selected ? AppColor.primary : Colors.transparent,
border: Border.all(
color: selected ? AppColor.primary : AppColor.border,
),
borderRadius: BorderRadius.circular(6),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
selected ? Icons.check_circle : Icons.radio_button_unchecked,
size: 16,
color: selected ? AppColor.white : AppColor.textSecondary,
),
const SpaceWidth(6),
Text(
selected ? 'Dipilih' : 'Pilih baris',
style: AppStyle.sm.copyWith(
fontWeight: FontWeight.w600,
color: selected ? AppColor.white : AppColor.textSecondary,
),
),
],
),
),
);
}
Widget _buildQuantityStepper() {
return Container(
decoration: BoxDecoration(
border: Border.all(color: AppColor.border),
borderRadius: BorderRadius.circular(6),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
// Decrease Button
InkWell(
onTap: onTapDecrease,
child: Container(
padding: const EdgeInsets.all(8),
child: Icon(
Icons.remove,
size: 16,
color: voidQty > 0 ? AppColor.primary : Colors.grey[400],
),
),
),
// Quantity Display
Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: const BoxDecoration(
border: Border.symmetric(
vertical: BorderSide(color: AppColor.border),
),
),
child: Text(
'$voidQty',
style: AppStyle.sm.copyWith(fontWeight: FontWeight.w600),
),
),
// Increase Button
InkWell(
onTap: onTapIncrease,
child: Container(
padding: const EdgeInsets.all(8),
child: Icon(
Icons.add,
size: 16,
color: voidQty < (item.quantity)
? AppColor.primary
: Colors.grey[400],
),
),
),
],
),
);
}
} }
+139
View File
@@ -0,0 +1,139 @@
import 'package:apskel_pos_flutter_v2/application/checkout/checkout_form/checkout_form_bloc.dart';
import 'package:apskel_pos_flutter_v2/common/types/sell_by.dart';
import 'package:apskel_pos_flutter_v2/domain/order/order.dart';
import 'package:apskel_pos_flutter_v2/domain/product/product.dart';
import 'package:flutter_test/flutter_test.dart';
/// Ikan Tude — Rp 4.500 per ons.
final ikanTude = Product.empty().copyWith(
id: 'ikan-tude',
name: 'Ikan Tude',
price: 4500,
isActive: true,
sellBy: SellBy.weight,
unitId: 'unit-ons',
unitName: 'Ons',
unitAbbreviation: 'ons',
);
final nasiGoreng = Product.empty().copyWith(
id: 'nasi-goreng',
name: 'Nasi Goreng',
price: 25000,
isActive: true,
);
void main() {
group('keranjang produk timbangan', () {
test('dua penimbangan produk yang sama menjadi dua baris', () async {
final bloc = CheckoutFormBloc();
bloc.add(CheckoutFormEvent.addWeightedItem(ikanTude, null, 4.2));
bloc.add(CheckoutFormEvent.addWeightedItem(ikanTude, null, 5.6));
await bloc.stream.firstWhere((s) => s.items.length == 2);
final items = bloc.state.items;
expect(items.length, 2);
expect(items.map((e) => e.weight), [4.2, 5.6]);
expect(items.every((e) => e.quantity == 1), isTrue);
expect(items[0].lineId, isNot(items[1].lineId));
});
test('produk cacah tetap digabung jadi satu baris', () async {
final bloc = CheckoutFormBloc();
bloc.add(CheckoutFormEvent.addItem(nasiGoreng, null));
bloc.add(CheckoutFormEvent.addItem(nasiGoreng, null));
await bloc.stream.firstWhere((s) => s.items.isNotEmpty && s.items.first.quantity == 2);
expect(bloc.state.items.length, 1);
expect(bloc.state.items.first.quantity, 2);
});
test('produk timbangan tidak bisa masuk tanpa berat', () async {
final bloc = CheckoutFormBloc();
bloc.add(CheckoutFormEvent.addItem(ikanTude, null));
await Future<void>.delayed(Duration.zero);
expect(bloc.state.items, isEmpty);
});
test('total baris dihitung berat x harga satuan', () {
final line = ProductQuantity(
product: ikanTude,
quantity: 1,
notes: '',
weight: 4.2,
);
expect(line.lineTotal, closeTo(18900, 0.001));
expect(line.quantityLabel, '4,2 ons');
});
test('total keranjang mencampur baris berbobot dan baris cacah', () {
final items = [
ProductQuantity(
product: ikanTude,
quantity: 1,
notes: '',
weight: 4.2,
),
ProductQuantity(product: nasiGoreng, quantity: 2, notes: ''),
];
expect(items.cartTotal, closeTo(18900 + 50000, 0.001));
expect(items.cartItemCount, 3);
});
});
group('permintaan ke backend', () {
test('baris berbobot dikirim quantity 1 dan membawa berat', () {
final request = OrderItemRequest.fromProductQuantity(
ProductQuantity(
product: ikanTude,
quantity: 1,
notes: '',
weight: 4.2,
),
);
expect(request.quantity, 1);
expect(request.weight, 4.2);
expect(request.unitPrice, 4500);
});
test('produk cacah tidak membawa berat sama sekali', () {
final request = OrderItemRequest.fromProductQuantity(
ProductQuantity(product: nasiGoreng, quantity: 2, notes: ''),
);
expect(request.quantity, 2);
expect(request.weight, isNull);
});
});
group('tampilan baris pesanan', () {
test('baris berbobot memakai berat, bukan quantity', () {
final item = OrderItem.empty().copyWith(
quantity: 1,
unitPrice: 4500,
totalPrice: 18900,
weight: 4.2,
unitAbbreviation: 'ons',
);
expect(item.isWeighted, isTrue);
expect(item.quantityLabel, '4,2 ons');
expect(item.quantityPriceLabel, '4,2 ons x Rp 4.500');
});
test('baris biasa tetap memakai quantity', () {
final item = OrderItem.empty().copyWith(quantity: 2, unitPrice: 25000);
expect(item.isWeighted, isFalse);
expect(item.quantityLabel, '2');
expect(item.quantityPriceLabel, '2 x Rp 25.000');
});
});
}