feat: update profit shareing
Build & Deploy iOS to TestFlight / build-and-deploy (push) Canceled after 0s

This commit is contained in:
efrilm
2026-10-04 22:31:01 +07:00
parent 288d0d75a0
commit 243b7e02aa
23 changed files with 1349 additions and 451 deletions
+289
View File
@@ -0,0 +1,289 @@
# Migrasi profit sharing
4 Oktober 2026
## Ringkasan
Ada tiga perubahan di backend:
1. **Parent category bisa ditandai bukan team.** Kategori punya field baru `is_team`. Parent category dengan `is_team: false` tidak bisa dipilih sebagai team di purchase order dan cash advance, dan tidak ikut laporan profit sharing.
2. **Endpoint laporan pindah path.** `/api/v1/analytics/parent-categories` menjadi `/api/v1/analytics/profit-sharing`.
3. **Pembagian revenue tinggal dua porsi.** Porsi purchase (60%) dihapus. Revenue sekarang dibagi ke owner (SDL) dan team, dengan porsi team = 100% − fee owner.
Nomor 2 dan 3 adalah breaking change. Setelah backend baru dirilis, client yang masih memanggil path lama mendapat 404, dan `limit_purchase` serta `percentages.purchase` tidak ada lagi di response. Karena itu client harus diupdate lebih dulu, lihat [Urutan rilis](#database-dan-urutan-rilis).
Yang perlu bertindak, di dashboard maupun app mobile, mana pun yang punya layarnya:
- **Form kategori**: tambah toggle team untuk parent category.
- **Laporan profit sharing**: ganti path, hapus porsi purchase, tampilkan dua porsi.
- **Form purchase order dan cash advance**: picker team tidak perlu diubah, tapi form edit perlu menyesuaikan, lihat [Purchase order dan cash advance](#purchase-order-dan-cash-advance).
## Kategori: field `is_team`
### Response
Semua response kategori membawa `is_team` (boolean, tidak pernah `null`): `POST /api/v1/categories`, `PUT /api/v1/categories/:id`, `GET /api/v1/categories`, dan `GET /api/v1/categories/:id`.
```json
{
"id": "<uuid>",
"name": "Merchandise",
"parent_id": null,
"owner_fee_percent": null,
"is_team": false
}
```
### Request
`POST /api/v1/categories` dan `PUT /api/v1/categories/:id` menerima `is_team`.
```json
{
"name": "Merchandise",
"is_team": false
}
```
| Request | `is_team` yang dikirim | Hasil |
| --- | --- | --- |
| Create | tidak dikirim | `true` |
| Create | `false` | `false` |
| Update | tidak dikirim atau `null` | tidak berubah |
| Update | `true` atau `false` | diganti |
Update yang hanya berisi `is_team` diterima.
Aturan nilainya:
- `is_team` hanya berpengaruh di parent category, yaitu kategori dengan `parent_id: null`. Di sub-category nilainya disimpan tapi tidak dipakai, jadi tampilkan toggle hanya untuk parent category.
- Semua kategori yang sudah ada sebelum rilis bernilai `true`. Tidak ada yang berubah sampai admin mematikannya.
- Flag dibaca saat request, bukan saat transaksi. Kalau parent category dimatikan, penjualannya di periode lampau juga hilang dari laporan profit sharing. Kalau dinyalakan lagi, semuanya muncul kembali.
### Efek `is_team: false`
| Endpoint | Efek |
| --- | --- |
| `GET /api/v1/purchase-orders/teams`, `GET /api/v1/cash-advances/teams` | Kategori tidak muncul. Pusat tetap ada. |
| Create dan update purchase order dan cash advance | `team_scope: "category"` dengan `team_category_id` kategori ini ditolak. |
| `GET /api/v1/analytics/profit-sharing` | Kategori tidak muncul di `data`, dan revenue-nya tidak dihitung di `budget`. |
| `GET /api/v1/analytics/profit-sharing/:parent_category_id` | Ditolak. |
Data yang sudah ada tidak diubah. Purchase order dan cash advance yang sudah tercatat ke kategori itu tetap menyimpan team-nya. Laporan purchasing (`GET /api/v1/analytics/purchasing`) masih menampilkannya di `team_data`, dan filter `team=<category_id>` tetap bisa dipakai.
Laporan lain yang tidak menyaring `is_team`, misalnya `GET /api/v1/analytics/categories`, tetap menampilkan penjualan kategori itu.
## Purchase order dan cash advance
Picker team sudah mengambil dari `GET /api/v1/purchase-orders/teams` dan `GET /api/v1/cash-advances/teams`, jadi kategori non-team otomatis tidak muncul tanpa perubahan di client.
Yang perlu diubah ada di form edit. Purchase order atau cash advance lama bisa tercatat ke kategori yang sekarang sudah non-team. Kalau form edit mengirim ulang `team_scope` dan `team_category_id` yang sama, request ditolak, walaupun user tidak mengubah team-nya.
1. Kirim `team_scope` dan `team_category_id` hanya kalau user mengganti team. Kalau `team_scope` tidak dikirim, team yang tersimpan tidak berubah.
2. Kalau team yang tersimpan tidak ada di daftar `/teams`, tampilkan namanya dari field `team` di response apa adanya, dan jangan memilihkan team lain secara otomatis.
Contoh error saat team ditolak, dengan HTTP status 500:
```json
{
"success": false,
"data": null,
"errors": [
{
"code": "900",
"entity": "purchase_order_service",
"cause": "category Merchandise is not a team"
}
]
}
```
Untuk cash advance, `entity` bernilai `cash_advance_service`. Cocokkan dengan teks `is not a team` di `cause` kalau perlu menampilkan pesan khusus.
## Laporan profit sharing
### Path baru
| Lama | Baru |
| --- | --- |
| `GET /api/v1/analytics/parent-categories` | `GET /api/v1/analytics/profit-sharing` |
| `GET /api/v1/analytics/parent-categories/:parent_category_id` | `GET /api/v1/analytics/profit-sharing/:parent_category_id` |
Query parameter dan role tidak berubah:
- `date_from` dan `date_to` wajib, dengan format `DD-MM-YYYY`, misalnya `28-09-2026`.
- `outlet_id` opsional.
- Hanya bisa diakses superadmin, admin, manager, owner, dan purchasing.
Path lama sudah tidak ada dan mengembalikan 404.
### Pembagian revenue
Revenue tiap parent category yang team dibagi dua:
- **SDL (fee owner)** = revenue × `owner_fee_percent` / 100. Default-nya 20%, dan bisa diganti per parent category lewat `owner_fee_percent` di kategori.
- **Team** = revenue − SDL.
Contoh dengan tiga parent category dalam satu minggu:
| Parent category | `is_team` | Fee owner | Revenue | SDL | Team |
| --- | --- | --- | --- | --- | --- |
| Food | `true` | 20% (default) | 1.000.000 | 200.000 | 800.000 |
| Drink | `true` | 35% | 2.000.000 | 700.000 | 1.300.000 |
| Merchandise | `false` | - | 500.000 | tidak dihitung | tidak dihitung |
| **Budget minggu ini** | | | **3.000.000** | **900.000** | **2.100.000** |
### Perubahan field
| Field | Sebelum | Sesudah |
| --- | --- | --- |
| `data[]` | semua parent category | hanya parent category team |
| `budget.percentages.purchase` | `60` | dihapus |
| `budget.percentages.owner` | `20`, atau fee parent itu di endpoint detail | tidak berubah |
| `budget.percentages.team` | `20` | `100 − owner`: `80` di list, `100 − fee parent` di detail |
| `limit_purchase` di `budget.total`, `budget.weekly[]`, `budget.monthly[]` | 60% revenue | dihapus |
| `limit_team` di tempat yang sama | 20% revenue | `revenue − sdl` |
| `revenue` dan `sdl` di `budget` | semua parent category | hanya parent category team |
Di endpoint list, `budget.percentages` selalu berisi default `20` dan `80`, walaupun ada parent dengan fee berbeda. Angka `sdl` dan `limit_team` dihitung per parent dengan fee masing-masing, jadi `limit_team / revenue` bisa tidak persis 80%. Tampilkan angka rupiah dari response, jangan dihitung ulang dari persentase.
Baris di `data[]` membawa `sdl` tapi tidak membawa porsi team. Kalau porsi team per parent perlu ditampilkan, hitung dari `total_revenue − sdl`.
Contoh response list, dipotong:
```json
{
"success": true,
"data": {
"date_from": "2026-09-28T00:00:00+07:00",
"date_to": "2026-10-04T23:59:59.999999999+07:00",
"data": [
{
"parent_category_id": "<uuid>",
"parent_category_name": "Drink",
"owner_fee_percent": 35,
"sdl": 700000,
"total_revenue": 2000000
},
{
"parent_category_id": "<uuid>",
"parent_category_name": "Food",
"owner_fee_percent": 20,
"sdl": 200000,
"total_revenue": 1000000
}
],
"budget": {
"percentages": { "owner": 20, "team": 80 },
"cut_off_from": "2026-09-28T00:00:00+07:00",
"cut_off_to": "2026-10-04T23:59:59.999999999+07:00",
"total": {
"period_start": "2026-09-28T00:00:00+07:00",
"period_end": "2026-10-04T23:59:59.999999999+07:00",
"revenue": 3000000,
"order_count": 4,
"sdl": 900000,
"limit_team": 2100000
},
"weekly": [
{
"period_start": "2026-09-28T00:00:00+07:00",
"period_end": "2026-10-04T23:59:59.999999999+07:00",
"revenue": 3000000,
"order_count": 4,
"sdl": 900000,
"limit_team": 2100000
}
],
"monthly": [
{
"month": "2026-09",
"week_count": 1,
"period_start": "2026-09-28T00:00:00+07:00",
"period_end": "2026-10-04T23:59:59.999999999+07:00",
"revenue": 3000000,
"order_count": 4,
"sdl": 900000,
"limit_team": 2100000
}
]
}
},
"errors": null
}
```
Di endpoint detail, `budget` bentuknya sama, tapi `percentages` memakai fee parent itu, misalnya `{ "owner": 35, "team": 65 }` untuk Drink.
### Detail kategori non-team
Detail untuk parent category non-team ditolak dengan HTTP status 500:
```json
{
"success": false,
"data": null,
"errors": [
{
"code": "internal_error",
"entity": "AnalyticsHandler::GetParentCategoryAnalyticsDetail",
"cause": "failed to get parent category analytics detail: failed to get parent category analytics detail: category Merchandise is not a team"
}
]
}
```
Ini bisa terjadi kalau user membuka link lama atau bookmark ke parent yang baru dimatikan. Cocokkan dengan teks `is not a team` di `cause`, lalu arahkan user kembali ke list.
## Migrasi client
### Laporan profit sharing
1. Ganti path ke `/api/v1/analytics/profit-sharing`. Selama backend lama masih jalan, path baru mengembalikan 404. Kalau dapat 404, panggil path lama `/api/v1/analytics/parent-categories`, supaya client baru bisa dirilis sebelum backend.
2. Hapus tampilan limit purchase dan persentase purchase. Jangan menganggap `limit_purchase` atau `percentages.purchase` selalu ada.
3. Tampilkan dua porsi dengan label "SDL / Fee owner" dan "Team", ambil angkanya dari `sdl` dan `limit_team`.
4. Kalau detail ditolak dengan `is not a team`, arahkan kembali ke list.
Selama fallback ke backend lama, `limit_team` masih berisi 20% revenue. Angkanya baru jadi `revenue − sdl` setelah backend baru dirilis.
### Form kategori
1. Tambah toggle `is_team` untuk parent category, misalnya berlabel "Ikut profit sharing (team)". Untuk kategori baru, toggle menyala secara default.
2. Saat membuka form edit, isi toggle dari `is_team`. Selama backend lama masih jalan, field ini tidak ada di response, jadi anggap `true`.
3. Saat admin mematikan toggle, tampilkan konfirmasi bahwa kategori itu tidak akan muncul di pilihan team dan di laporan profit sharing, termasuk untuk periode lampau.
4. Di daftar kategori, beri penanda untuk parent category dengan `is_team: false`.
Backend lama mengabaikan `is_team` di request, jadi toggle bisa dirilis lebih dulu, tapi belum berpengaruh sampai backend baru jalan. Pengecualiannya update yang hanya berisi `is_team`: backend lama menolaknya dengan error code `303` dan pesan `at least one field must be provided for update`. Selama masa transisi, kirim `is_team` bersama field form lainnya.
## Database dan urutan rilis
Migration `000101_add_is_team_to_categories` menambah kolom `categories.is_team` (`BOOLEAN NOT NULL DEFAULT TRUE`). Semua kategori yang ada otomatis bernilai `true`. Migration ini hanya menambah kolom, jadi backend lama tetap jalan normal setelahnya.
Urutan rilis:
1. Rilis client baru: path profit sharing dengan fallback ke path lama, tanpa porsi purchase, dan dengan toggle `is_team`. Untuk app mobile, pastikan versi baru sudah dipakai sebagian besar user sebelum langkah 3, karena versi lama yang memanggil `/parent-categories` mendapat 404 setelah itu.
2. Jalankan migration `000101`.
3. Deploy backend baru.
4. Hapus fallback path lama di client.
5. Admin mematikan `is_team` di parent category yang bukan team.
Rollback: deploy backend lama, lalu jalankan down migration yang menghapus kolom `is_team`. Client dengan fallback tetap jalan di backend lama. Nilai `is_team` yang sudah diatur admin hilang saat kolom dihapus.
## Checklist
- [ ] Client baru (fallback path, tanpa porsi purchase, toggle `is_team`) dirilis
- [ ] Migration `000101` dan backend baru dirilis di staging
- [ ] Uji: parent category yang dimatikan hilang dari `/purchase-orders/teams` dan `/cash-advances/teams`
- [ ] Uji: purchase order dan cash advance baru dengan kategori itu ditolak, dan edit purchase order lama tanpa mengganti team tetap berhasil
- [ ] Uji: kategori itu hilang dari `/analytics/profit-sharing`, dan detailnya ditolak
- [ ] Uji: `limit_team = revenue − sdl`, dan `sdl` mengikuti fee masing-masing parent
- [ ] Migration `000101` dan backend baru dirilis di production
- [ ] Fallback path lama di client dihapus
## FAQ
**Kenapa porsi team jadi 100% − fee owner, bukan tetap 20%?** Porsi purchase sudah tidak ada, jadi seluruh revenue dibagi dua. Owner mengambil fee-nya, dan sisanya untuk team. Kalau fee owner sebuah parent dinaikkan, porsi team parent itu turun sebesar yang sama.
**Apakah sub-category bisa dijadikan non-team sendiri?** Tidak. Team dan profit sharing dihitung per parent category, jadi semua sub-category ikut status parent-nya.
**Bagaimana dengan kategori top-level yang tidak punya sub-category?** Kategori itu tetap parent category, jadi `is_team` berlaku untuknya.
+6 -1
View File
@@ -14,7 +14,12 @@ class ApiPath {
static const String purchasingAnalytic = '/api/v1/analytics/purchasing';
static const String exclusiveSummaryAnalytic =
'/api/v1/analytics/exclusive-summary/period';
static const String parentCategoryAnalytic =
static const String profitSharingAnalytic =
'/api/v1/analytics/profit-sharing';
/// Path lama profit sharing, hanya untuk fallback selama backend lama masih
/// jalan. Hapus setelah backend baru rilis (docs/migrasi-profit-sharing.md).
static const String profitSharingAnalyticLegacy =
'/api/v1/analytics/parent-categories';
// Inventory
+162 -54
View File
@@ -13342,7 +13342,6 @@ abstract class _ProfitSharingBudget implements ProfitSharingBudget {
/// @nodoc
mixin _$ProfitSharingPercentage {
double get purchase => throw _privateConstructorUsedError;
double get owner => throw _privateConstructorUsedError;
double get team => throw _privateConstructorUsedError;
@@ -13360,7 +13359,7 @@ abstract class $ProfitSharingPercentageCopyWith<$Res> {
$Res Function(ProfitSharingPercentage) then,
) = _$ProfitSharingPercentageCopyWithImpl<$Res, ProfitSharingPercentage>;
@useResult
$Res call({double purchase, double owner, double team});
$Res call({double owner, double team});
}
/// @nodoc
@@ -13380,17 +13379,9 @@ class _$ProfitSharingPercentageCopyWithImpl<
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? purchase = null,
Object? owner = null,
Object? team = null,
}) {
$Res call({Object? owner = null, Object? team = null}) {
return _then(
_value.copyWith(
purchase: null == purchase
? _value.purchase
: purchase // ignore: cast_nullable_to_non_nullable
as double,
owner: null == owner
? _value.owner
: owner // ignore: cast_nullable_to_non_nullable
@@ -13414,7 +13405,7 @@ abstract class _$$ProfitSharingPercentageImplCopyWith<$Res>
) = __$$ProfitSharingPercentageImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({double purchase, double owner, double team});
$Res call({double owner, double team});
}
/// @nodoc
@@ -13434,17 +13425,9 @@ class __$$ProfitSharingPercentageImplCopyWithImpl<$Res>
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? purchase = null,
Object? owner = null,
Object? team = null,
}) {
$Res call({Object? owner = null, Object? team = null}) {
return _then(
_$ProfitSharingPercentageImpl(
purchase: null == purchase
? _value.purchase
: purchase // ignore: cast_nullable_to_non_nullable
as double,
owner: null == owner
? _value.owner
: owner // ignore: cast_nullable_to_non_nullable
@@ -13462,13 +13445,10 @@ class __$$ProfitSharingPercentageImplCopyWithImpl<$Res>
class _$ProfitSharingPercentageImpl implements _ProfitSharingPercentage {
const _$ProfitSharingPercentageImpl({
required this.purchase,
required this.owner,
required this.team,
});
@override
final double purchase;
@override
final double owner;
@override
@@ -13476,7 +13456,7 @@ class _$ProfitSharingPercentageImpl implements _ProfitSharingPercentage {
@override
String toString() {
return 'ProfitSharingPercentage(purchase: $purchase, owner: $owner, team: $team)';
return 'ProfitSharingPercentage(owner: $owner, team: $team)';
}
@override
@@ -13484,14 +13464,12 @@ class _$ProfitSharingPercentageImpl implements _ProfitSharingPercentage {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$ProfitSharingPercentageImpl &&
(identical(other.purchase, purchase) ||
other.purchase == purchase) &&
(identical(other.owner, owner) || other.owner == owner) &&
(identical(other.team, team) || other.team == team));
}
@override
int get hashCode => Object.hash(runtimeType, purchase, owner, team);
int get hashCode => Object.hash(runtimeType, owner, team);
/// Create a copy of ProfitSharingPercentage
/// with the given fields replaced by the non-null parameter values.
@@ -13507,13 +13485,10 @@ class _$ProfitSharingPercentageImpl implements _ProfitSharingPercentage {
abstract class _ProfitSharingPercentage implements ProfitSharingPercentage {
const factory _ProfitSharingPercentage({
required final double purchase,
required final double owner,
required final double team,
}) = _$ProfitSharingPercentageImpl;
@override
double get purchase;
@override
double get owner;
@override
@@ -13533,7 +13508,6 @@ mixin _$ProfitSharingPeriod {
DateTime get periodEnd => throw _privateConstructorUsedError;
int get revenue => throw _privateConstructorUsedError;
int get orderCount => throw _privateConstructorUsedError;
int get limitPurchase => throw _privateConstructorUsedError;
int get sdl => throw _privateConstructorUsedError;
int get limitTeam => throw _privateConstructorUsedError;
String get month => throw _privateConstructorUsedError;
@@ -13558,7 +13532,6 @@ abstract class $ProfitSharingPeriodCopyWith<$Res> {
DateTime periodEnd,
int revenue,
int orderCount,
int limitPurchase,
int sdl,
int limitTeam,
String month,
@@ -13585,7 +13558,6 @@ class _$ProfitSharingPeriodCopyWithImpl<$Res, $Val extends ProfitSharingPeriod>
Object? periodEnd = null,
Object? revenue = null,
Object? orderCount = null,
Object? limitPurchase = null,
Object? sdl = null,
Object? limitTeam = null,
Object? month = null,
@@ -13609,10 +13581,6 @@ class _$ProfitSharingPeriodCopyWithImpl<$Res, $Val extends ProfitSharingPeriod>
? _value.orderCount
: orderCount // ignore: cast_nullable_to_non_nullable
as int,
limitPurchase: null == limitPurchase
? _value.limitPurchase
: limitPurchase // ignore: cast_nullable_to_non_nullable
as int,
sdl: null == sdl
? _value.sdl
: sdl // ignore: cast_nullable_to_non_nullable
@@ -13649,7 +13617,6 @@ abstract class _$$ProfitSharingPeriodImplCopyWith<$Res>
DateTime periodEnd,
int revenue,
int orderCount,
int limitPurchase,
int sdl,
int limitTeam,
String month,
@@ -13675,7 +13642,6 @@ class __$$ProfitSharingPeriodImplCopyWithImpl<$Res>
Object? periodEnd = null,
Object? revenue = null,
Object? orderCount = null,
Object? limitPurchase = null,
Object? sdl = null,
Object? limitTeam = null,
Object? month = null,
@@ -13699,10 +13665,6 @@ class __$$ProfitSharingPeriodImplCopyWithImpl<$Res>
? _value.orderCount
: orderCount // ignore: cast_nullable_to_non_nullable
as int,
limitPurchase: null == limitPurchase
? _value.limitPurchase
: limitPurchase // ignore: cast_nullable_to_non_nullable
as int,
sdl: null == sdl
? _value.sdl
: sdl // ignore: cast_nullable_to_non_nullable
@@ -13732,7 +13694,6 @@ class _$ProfitSharingPeriodImpl extends _ProfitSharingPeriod {
required this.periodEnd,
required this.revenue,
required this.orderCount,
required this.limitPurchase,
required this.sdl,
required this.limitTeam,
this.month = '',
@@ -13748,8 +13709,6 @@ class _$ProfitSharingPeriodImpl extends _ProfitSharingPeriod {
@override
final int orderCount;
@override
final int limitPurchase;
@override
final int sdl;
@override
final int limitTeam;
@@ -13762,7 +13721,7 @@ class _$ProfitSharingPeriodImpl extends _ProfitSharingPeriod {
@override
String toString() {
return 'ProfitSharingPeriod(periodStart: $periodStart, periodEnd: $periodEnd, revenue: $revenue, orderCount: $orderCount, limitPurchase: $limitPurchase, sdl: $sdl, limitTeam: $limitTeam, month: $month, weekCount: $weekCount)';
return 'ProfitSharingPeriod(periodStart: $periodStart, periodEnd: $periodEnd, revenue: $revenue, orderCount: $orderCount, sdl: $sdl, limitTeam: $limitTeam, month: $month, weekCount: $weekCount)';
}
@override
@@ -13777,8 +13736,6 @@ class _$ProfitSharingPeriodImpl extends _ProfitSharingPeriod {
(identical(other.revenue, revenue) || other.revenue == revenue) &&
(identical(other.orderCount, orderCount) ||
other.orderCount == orderCount) &&
(identical(other.limitPurchase, limitPurchase) ||
other.limitPurchase == limitPurchase) &&
(identical(other.sdl, sdl) || other.sdl == sdl) &&
(identical(other.limitTeam, limitTeam) ||
other.limitTeam == limitTeam) &&
@@ -13794,7 +13751,6 @@ class _$ProfitSharingPeriodImpl extends _ProfitSharingPeriod {
periodEnd,
revenue,
orderCount,
limitPurchase,
sdl,
limitTeam,
month,
@@ -13819,7 +13775,6 @@ abstract class _ProfitSharingPeriod extends ProfitSharingPeriod {
required final DateTime periodEnd,
required final int revenue,
required final int orderCount,
required final int limitPurchase,
required final int sdl,
required final int limitTeam,
final String month,
@@ -13836,8 +13791,6 @@ abstract class _ProfitSharingPeriod extends ProfitSharingPeriod {
@override
int get orderCount;
@override
int get limitPurchase;
@override
int get sdl;
@override
int get limitTeam;
@@ -15050,6 +15003,7 @@ mixin _$AnalyticFailure {
required TResult Function(ApiFailure failure) serverError,
required TResult Function() unexpectedError,
required TResult Function() empty,
required TResult Function() notTeam,
required TResult Function(String erroMessage) dynamicErrorMessage,
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
@@ -15057,6 +15011,7 @@ mixin _$AnalyticFailure {
TResult? Function(ApiFailure failure)? serverError,
TResult? Function()? unexpectedError,
TResult? Function()? empty,
TResult? Function()? notTeam,
TResult? Function(String erroMessage)? dynamicErrorMessage,
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
@@ -15064,6 +15019,7 @@ mixin _$AnalyticFailure {
TResult Function(ApiFailure failure)? serverError,
TResult Function()? unexpectedError,
TResult Function()? empty,
TResult Function()? notTeam,
TResult Function(String erroMessage)? dynamicErrorMessage,
required TResult orElse(),
}) => throw _privateConstructorUsedError;
@@ -15072,6 +15028,7 @@ mixin _$AnalyticFailure {
required TResult Function(_ServerError value) serverError,
required TResult Function(_UnexpectedError value) unexpectedError,
required TResult Function(_Empty value) empty,
required TResult Function(_NotTeam value) notTeam,
required TResult Function(_DynamicErrorMessage value) dynamicErrorMessage,
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
@@ -15079,6 +15036,7 @@ mixin _$AnalyticFailure {
TResult? Function(_ServerError value)? serverError,
TResult? Function(_UnexpectedError value)? unexpectedError,
TResult? Function(_Empty value)? empty,
TResult? Function(_NotTeam value)? notTeam,
TResult? Function(_DynamicErrorMessage value)? dynamicErrorMessage,
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
@@ -15086,6 +15044,7 @@ mixin _$AnalyticFailure {
TResult Function(_ServerError value)? serverError,
TResult Function(_UnexpectedError value)? unexpectedError,
TResult Function(_Empty value)? empty,
TResult Function(_NotTeam value)? notTeam,
TResult Function(_DynamicErrorMessage value)? dynamicErrorMessage,
required TResult orElse(),
}) => throw _privateConstructorUsedError;
@@ -15198,6 +15157,7 @@ class _$ServerErrorImpl implements _ServerError {
required TResult Function(ApiFailure failure) serverError,
required TResult Function() unexpectedError,
required TResult Function() empty,
required TResult Function() notTeam,
required TResult Function(String erroMessage) dynamicErrorMessage,
}) {
return serverError(failure);
@@ -15209,6 +15169,7 @@ class _$ServerErrorImpl implements _ServerError {
TResult? Function(ApiFailure failure)? serverError,
TResult? Function()? unexpectedError,
TResult? Function()? empty,
TResult? Function()? notTeam,
TResult? Function(String erroMessage)? dynamicErrorMessage,
}) {
return serverError?.call(failure);
@@ -15220,6 +15181,7 @@ class _$ServerErrorImpl implements _ServerError {
TResult Function(ApiFailure failure)? serverError,
TResult Function()? unexpectedError,
TResult Function()? empty,
TResult Function()? notTeam,
TResult Function(String erroMessage)? dynamicErrorMessage,
required TResult orElse(),
}) {
@@ -15235,6 +15197,7 @@ class _$ServerErrorImpl implements _ServerError {
required TResult Function(_ServerError value) serverError,
required TResult Function(_UnexpectedError value) unexpectedError,
required TResult Function(_Empty value) empty,
required TResult Function(_NotTeam value) notTeam,
required TResult Function(_DynamicErrorMessage value) dynamicErrorMessage,
}) {
return serverError(this);
@@ -15246,6 +15209,7 @@ class _$ServerErrorImpl implements _ServerError {
TResult? Function(_ServerError value)? serverError,
TResult? Function(_UnexpectedError value)? unexpectedError,
TResult? Function(_Empty value)? empty,
TResult? Function(_NotTeam value)? notTeam,
TResult? Function(_DynamicErrorMessage value)? dynamicErrorMessage,
}) {
return serverError?.call(this);
@@ -15257,6 +15221,7 @@ class _$ServerErrorImpl implements _ServerError {
TResult Function(_ServerError value)? serverError,
TResult Function(_UnexpectedError value)? unexpectedError,
TResult Function(_Empty value)? empty,
TResult Function(_NotTeam value)? notTeam,
TResult Function(_DynamicErrorMessage value)? dynamicErrorMessage,
required TResult orElse(),
}) {
@@ -15325,6 +15290,7 @@ class _$UnexpectedErrorImpl implements _UnexpectedError {
required TResult Function(ApiFailure failure) serverError,
required TResult Function() unexpectedError,
required TResult Function() empty,
required TResult Function() notTeam,
required TResult Function(String erroMessage) dynamicErrorMessage,
}) {
return unexpectedError();
@@ -15336,6 +15302,7 @@ class _$UnexpectedErrorImpl implements _UnexpectedError {
TResult? Function(ApiFailure failure)? serverError,
TResult? Function()? unexpectedError,
TResult? Function()? empty,
TResult? Function()? notTeam,
TResult? Function(String erroMessage)? dynamicErrorMessage,
}) {
return unexpectedError?.call();
@@ -15347,6 +15314,7 @@ class _$UnexpectedErrorImpl implements _UnexpectedError {
TResult Function(ApiFailure failure)? serverError,
TResult Function()? unexpectedError,
TResult Function()? empty,
TResult Function()? notTeam,
TResult Function(String erroMessage)? dynamicErrorMessage,
required TResult orElse(),
}) {
@@ -15362,6 +15330,7 @@ class _$UnexpectedErrorImpl implements _UnexpectedError {
required TResult Function(_ServerError value) serverError,
required TResult Function(_UnexpectedError value) unexpectedError,
required TResult Function(_Empty value) empty,
required TResult Function(_NotTeam value) notTeam,
required TResult Function(_DynamicErrorMessage value) dynamicErrorMessage,
}) {
return unexpectedError(this);
@@ -15373,6 +15342,7 @@ class _$UnexpectedErrorImpl implements _UnexpectedError {
TResult? Function(_ServerError value)? serverError,
TResult? Function(_UnexpectedError value)? unexpectedError,
TResult? Function(_Empty value)? empty,
TResult? Function(_NotTeam value)? notTeam,
TResult? Function(_DynamicErrorMessage value)? dynamicErrorMessage,
}) {
return unexpectedError?.call(this);
@@ -15384,6 +15354,7 @@ class _$UnexpectedErrorImpl implements _UnexpectedError {
TResult Function(_ServerError value)? serverError,
TResult Function(_UnexpectedError value)? unexpectedError,
TResult Function(_Empty value)? empty,
TResult Function(_NotTeam value)? notTeam,
TResult Function(_DynamicErrorMessage value)? dynamicErrorMessage,
required TResult orElse(),
}) {
@@ -15444,6 +15415,7 @@ class _$EmptyImpl implements _Empty {
required TResult Function(ApiFailure failure) serverError,
required TResult Function() unexpectedError,
required TResult Function() empty,
required TResult Function() notTeam,
required TResult Function(String erroMessage) dynamicErrorMessage,
}) {
return empty();
@@ -15455,6 +15427,7 @@ class _$EmptyImpl implements _Empty {
TResult? Function(ApiFailure failure)? serverError,
TResult? Function()? unexpectedError,
TResult? Function()? empty,
TResult? Function()? notTeam,
TResult? Function(String erroMessage)? dynamicErrorMessage,
}) {
return empty?.call();
@@ -15466,6 +15439,7 @@ class _$EmptyImpl implements _Empty {
TResult Function(ApiFailure failure)? serverError,
TResult Function()? unexpectedError,
TResult Function()? empty,
TResult Function()? notTeam,
TResult Function(String erroMessage)? dynamicErrorMessage,
required TResult orElse(),
}) {
@@ -15481,6 +15455,7 @@ class _$EmptyImpl implements _Empty {
required TResult Function(_ServerError value) serverError,
required TResult Function(_UnexpectedError value) unexpectedError,
required TResult Function(_Empty value) empty,
required TResult Function(_NotTeam value) notTeam,
required TResult Function(_DynamicErrorMessage value) dynamicErrorMessage,
}) {
return empty(this);
@@ -15492,6 +15467,7 @@ class _$EmptyImpl implements _Empty {
TResult? Function(_ServerError value)? serverError,
TResult? Function(_UnexpectedError value)? unexpectedError,
TResult? Function(_Empty value)? empty,
TResult? Function(_NotTeam value)? notTeam,
TResult? Function(_DynamicErrorMessage value)? dynamicErrorMessage,
}) {
return empty?.call(this);
@@ -15503,6 +15479,7 @@ class _$EmptyImpl implements _Empty {
TResult Function(_ServerError value)? serverError,
TResult Function(_UnexpectedError value)? unexpectedError,
TResult Function(_Empty value)? empty,
TResult Function(_NotTeam value)? notTeam,
TResult Function(_DynamicErrorMessage value)? dynamicErrorMessage,
required TResult orElse(),
}) {
@@ -15517,6 +15494,131 @@ abstract class _Empty implements AnalyticFailure {
const factory _Empty() = _$EmptyImpl;
}
/// @nodoc
abstract class _$$NotTeamImplCopyWith<$Res> {
factory _$$NotTeamImplCopyWith(
_$NotTeamImpl value,
$Res Function(_$NotTeamImpl) then,
) = __$$NotTeamImplCopyWithImpl<$Res>;
}
/// @nodoc
class __$$NotTeamImplCopyWithImpl<$Res>
extends _$AnalyticFailureCopyWithImpl<$Res, _$NotTeamImpl>
implements _$$NotTeamImplCopyWith<$Res> {
__$$NotTeamImplCopyWithImpl(
_$NotTeamImpl _value,
$Res Function(_$NotTeamImpl) _then,
) : super(_value, _then);
/// Create a copy of AnalyticFailure
/// with the given fields replaced by the non-null parameter values.
}
/// @nodoc
class _$NotTeamImpl implements _NotTeam {
const _$NotTeamImpl();
@override
String toString() {
return 'AnalyticFailure.notTeam()';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType && other is _$NotTeamImpl);
}
@override
int get hashCode => runtimeType.hashCode;
@override
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function(ApiFailure failure) serverError,
required TResult Function() unexpectedError,
required TResult Function() empty,
required TResult Function() notTeam,
required TResult Function(String erroMessage) dynamicErrorMessage,
}) {
return notTeam();
}
@override
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function(ApiFailure failure)? serverError,
TResult? Function()? unexpectedError,
TResult? Function()? empty,
TResult? Function()? notTeam,
TResult? Function(String erroMessage)? dynamicErrorMessage,
}) {
return notTeam?.call();
}
@override
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function(ApiFailure failure)? serverError,
TResult Function()? unexpectedError,
TResult Function()? empty,
TResult Function()? notTeam,
TResult Function(String erroMessage)? dynamicErrorMessage,
required TResult orElse(),
}) {
if (notTeam != null) {
return notTeam();
}
return orElse();
}
@override
@optionalTypeArgs
TResult map<TResult extends Object?>({
required TResult Function(_ServerError value) serverError,
required TResult Function(_UnexpectedError value) unexpectedError,
required TResult Function(_Empty value) empty,
required TResult Function(_NotTeam value) notTeam,
required TResult Function(_DynamicErrorMessage value) dynamicErrorMessage,
}) {
return notTeam(this);
}
@override
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult? Function(_ServerError value)? serverError,
TResult? Function(_UnexpectedError value)? unexpectedError,
TResult? Function(_Empty value)? empty,
TResult? Function(_NotTeam value)? notTeam,
TResult? Function(_DynamicErrorMessage value)? dynamicErrorMessage,
}) {
return notTeam?.call(this);
}
@override
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
TResult Function(_ServerError value)? serverError,
TResult Function(_UnexpectedError value)? unexpectedError,
TResult Function(_Empty value)? empty,
TResult Function(_NotTeam value)? notTeam,
TResult Function(_DynamicErrorMessage value)? dynamicErrorMessage,
required TResult orElse(),
}) {
if (notTeam != null) {
return notTeam(this);
}
return orElse();
}
}
abstract class _NotTeam implements AnalyticFailure {
const factory _NotTeam() = _$NotTeamImpl;
}
/// @nodoc
abstract class _$$DynamicErrorMessageImplCopyWith<$Res> {
factory _$$DynamicErrorMessageImplCopyWith(
@@ -15594,6 +15696,7 @@ class _$DynamicErrorMessageImpl implements _DynamicErrorMessage {
required TResult Function(ApiFailure failure) serverError,
required TResult Function() unexpectedError,
required TResult Function() empty,
required TResult Function() notTeam,
required TResult Function(String erroMessage) dynamicErrorMessage,
}) {
return dynamicErrorMessage(erroMessage);
@@ -15605,6 +15708,7 @@ class _$DynamicErrorMessageImpl implements _DynamicErrorMessage {
TResult? Function(ApiFailure failure)? serverError,
TResult? Function()? unexpectedError,
TResult? Function()? empty,
TResult? Function()? notTeam,
TResult? Function(String erroMessage)? dynamicErrorMessage,
}) {
return dynamicErrorMessage?.call(erroMessage);
@@ -15616,6 +15720,7 @@ class _$DynamicErrorMessageImpl implements _DynamicErrorMessage {
TResult Function(ApiFailure failure)? serverError,
TResult Function()? unexpectedError,
TResult Function()? empty,
TResult Function()? notTeam,
TResult Function(String erroMessage)? dynamicErrorMessage,
required TResult orElse(),
}) {
@@ -15631,6 +15736,7 @@ class _$DynamicErrorMessageImpl implements _DynamicErrorMessage {
required TResult Function(_ServerError value) serverError,
required TResult Function(_UnexpectedError value) unexpectedError,
required TResult Function(_Empty value) empty,
required TResult Function(_NotTeam value) notTeam,
required TResult Function(_DynamicErrorMessage value) dynamicErrorMessage,
}) {
return dynamicErrorMessage(this);
@@ -15642,6 +15748,7 @@ class _$DynamicErrorMessageImpl implements _DynamicErrorMessage {
TResult? Function(_ServerError value)? serverError,
TResult? Function(_UnexpectedError value)? unexpectedError,
TResult? Function(_Empty value)? empty,
TResult? Function(_NotTeam value)? notTeam,
TResult? Function(_DynamicErrorMessage value)? dynamicErrorMessage,
}) {
return dynamicErrorMessage?.call(this);
@@ -15653,6 +15760,7 @@ class _$DynamicErrorMessageImpl implements _DynamicErrorMessage {
TResult Function(_ServerError value)? serverError,
TResult Function(_UnexpectedError value)? unexpectedError,
TResult Function(_Empty value)? empty,
TResult Function(_NotTeam value)? notTeam,
TResult Function(_DynamicErrorMessage value)? dynamicErrorMessage,
required TResult orElse(),
}) {
@@ -59,6 +59,13 @@ class ProfitSharingCategory with _$ProfitSharingCategory {
bool get hasTopProduct => topProduct.productId.isNotEmpty;
/// Porsi tim = omzet − SDL. Response tidak membawa porsi tim per kategori,
/// jadi dihitung dari nominalnya (docs/migrasi-profit-sharing.md).
int get teamShare => totalRevenue - sdl;
/// % porsi tim terhadap omzet, yaitu 100 − fee owner.
double get teamPercent => 100 - ownerFeePercent;
/// Laba kotor memakai HPP standar (acuan yang dipakai di laporan laba rugi).
int get grossProfit => totalRevenue - totalStandardHpp;
@@ -106,16 +113,16 @@ class ProfitSharingBudget with _$ProfitSharingBudget {
);
}
/// Porsi bawaan bagi hasil: owner (SDL) dan tim, dengan tim = 100 − owner.
@freezed
class ProfitSharingPercentage with _$ProfitSharingPercentage {
const factory ProfitSharingPercentage({
required double purchase,
required double owner,
required double team,
}) = _ProfitSharingPercentage;
factory ProfitSharingPercentage.empty() =>
const ProfitSharingPercentage(purchase: 0, owner: 0, team: 0);
const ProfitSharingPercentage(owner: 0, team: 0);
}
/// Satu baris periode bagi hasil. Dipakai untuk total, mingguan, dan bulanan.
@@ -129,7 +136,6 @@ class ProfitSharingPeriod with _$ProfitSharingPeriod {
required DateTime periodEnd,
required int revenue,
required int orderCount,
required int limitPurchase,
required int sdl,
required int limitTeam,
@Default('') String month,
@@ -141,7 +147,6 @@ class ProfitSharingPeriod with _$ProfitSharingPeriod {
periodEnd: DateTime.fromMillisecondsSinceEpoch(0),
revenue: 0,
orderCount: 0,
limitPurchase: 0,
sdl: 0,
limitTeam: 0,
);
@@ -152,6 +157,9 @@ class ProfitSharingPeriod with _$ProfitSharingPeriod {
/// induk, jadi porsinya dihitung dari nominal, bukan dari persentase tetap.
double get sdlPercentage => revenue == 0 ? 0 : (sdl / revenue) * 100;
/// % porsi tim terhadap omzet, dihitung dari nominal dengan alasan yang sama.
double get teamPercentage => revenue == 0 ? 0 : (limitTeam / revenue) * 100;
int get averageOrderValue => orderCount == 0 ? 0 : revenue ~/ orderCount;
}
@@ -5,6 +5,9 @@ sealed class AnalyticFailure with _$AnalyticFailure {
const factory AnalyticFailure.serverError(ApiFailure failure) = _ServerError;
const factory AnalyticFailure.unexpectedError() = _UnexpectedError;
const factory AnalyticFailure.empty() = _Empty;
/// Detail profit sharing ditolak karena kategori induknya sudah non-team.
const factory AnalyticFailure.notTeam() = _NotTeam;
const factory AnalyticFailure.dynamicErrorMessage(String erroMessage) =
_DynamicErrorMessage;
}
@@ -15656,8 +15656,6 @@ ProfitSharingPercentageDto _$ProfitSharingPercentageDtoFromJson(
/// @nodoc
mixin _$ProfitSharingPercentageDto {
@JsonKey(name: 'purchase')
num? get purchase => throw _privateConstructorUsedError;
@JsonKey(name: 'owner')
num? get owner => throw _privateConstructorUsedError;
@JsonKey(name: 'team')
@@ -15685,7 +15683,6 @@ abstract class $ProfitSharingPercentageDtoCopyWith<$Res> {
>;
@useResult
$Res call({
@JsonKey(name: 'purchase') num? purchase,
@JsonKey(name: 'owner') num? owner,
@JsonKey(name: 'team') num? team,
});
@@ -15708,17 +15705,9 @@ class _$ProfitSharingPercentageDtoCopyWithImpl<
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? purchase = freezed,
Object? owner = freezed,
Object? team = freezed,
}) {
$Res call({Object? owner = freezed, Object? team = freezed}) {
return _then(
_value.copyWith(
purchase: freezed == purchase
? _value.purchase
: purchase // ignore: cast_nullable_to_non_nullable
as num?,
owner: freezed == owner
? _value.owner
: owner // ignore: cast_nullable_to_non_nullable
@@ -15743,7 +15732,6 @@ abstract class _$$ProfitSharingPercentageDtoImplCopyWith<$Res>
@override
@useResult
$Res call({
@JsonKey(name: 'purchase') num? purchase,
@JsonKey(name: 'owner') num? owner,
@JsonKey(name: 'team') num? team,
});
@@ -15766,17 +15754,9 @@ class __$$ProfitSharingPercentageDtoImplCopyWithImpl<$Res>
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? purchase = freezed,
Object? owner = freezed,
Object? team = freezed,
}) {
$Res call({Object? owner = freezed, Object? team = freezed}) {
return _then(
_$ProfitSharingPercentageDtoImpl(
purchase: freezed == purchase
? _value.purchase
: purchase // ignore: cast_nullable_to_non_nullable
as num?,
owner: freezed == owner
? _value.owner
: owner // ignore: cast_nullable_to_non_nullable
@@ -15794,7 +15774,6 @@ class __$$ProfitSharingPercentageDtoImplCopyWithImpl<$Res>
@JsonSerializable()
class _$ProfitSharingPercentageDtoImpl extends _ProfitSharingPercentageDto {
const _$ProfitSharingPercentageDtoImpl({
@JsonKey(name: 'purchase') this.purchase,
@JsonKey(name: 'owner') this.owner,
@JsonKey(name: 'team') this.team,
}) : super._();
@@ -15803,9 +15782,6 @@ class _$ProfitSharingPercentageDtoImpl extends _ProfitSharingPercentageDto {
Map<String, dynamic> json,
) => _$$ProfitSharingPercentageDtoImplFromJson(json);
@override
@JsonKey(name: 'purchase')
final num? purchase;
@override
@JsonKey(name: 'owner')
final num? owner;
@@ -15815,7 +15791,7 @@ class _$ProfitSharingPercentageDtoImpl extends _ProfitSharingPercentageDto {
@override
String toString() {
return 'ProfitSharingPercentageDto(purchase: $purchase, owner: $owner, team: $team)';
return 'ProfitSharingPercentageDto(owner: $owner, team: $team)';
}
@override
@@ -15823,15 +15799,13 @@ class _$ProfitSharingPercentageDtoImpl extends _ProfitSharingPercentageDto {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$ProfitSharingPercentageDtoImpl &&
(identical(other.purchase, purchase) ||
other.purchase == purchase) &&
(identical(other.owner, owner) || other.owner == owner) &&
(identical(other.team, team) || other.team == team));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType, purchase, owner, team);
int get hashCode => Object.hash(runtimeType, owner, team);
/// Create a copy of ProfitSharingPercentageDto
/// with the given fields replaced by the non-null parameter values.
@@ -15852,7 +15826,6 @@ class _$ProfitSharingPercentageDtoImpl extends _ProfitSharingPercentageDto {
abstract class _ProfitSharingPercentageDto extends ProfitSharingPercentageDto {
const factory _ProfitSharingPercentageDto({
@JsonKey(name: 'purchase') final num? purchase,
@JsonKey(name: 'owner') final num? owner,
@JsonKey(name: 'team') final num? team,
}) = _$ProfitSharingPercentageDtoImpl;
@@ -15861,9 +15834,6 @@ abstract class _ProfitSharingPercentageDto extends ProfitSharingPercentageDto {
factory _ProfitSharingPercentageDto.fromJson(Map<String, dynamic> json) =
_$ProfitSharingPercentageDtoImpl.fromJson;
@override
@JsonKey(name: 'purchase')
num? get purchase;
@override
@JsonKey(name: 'owner')
num? get owner;
@@ -15895,8 +15865,6 @@ mixin _$ProfitSharingPeriodDto {
num? get revenue => throw _privateConstructorUsedError;
@JsonKey(name: 'order_count')
num? get orderCount => throw _privateConstructorUsedError;
@JsonKey(name: 'limit_purchase')
num? get limitPurchase => throw _privateConstructorUsedError;
@JsonKey(name: 'sdl')
num? get sdl => throw _privateConstructorUsedError;
@JsonKey(name: 'limit_team')
@@ -15928,7 +15896,6 @@ abstract class $ProfitSharingPeriodDtoCopyWith<$Res> {
@JsonKey(name: 'period_end') DateTime? periodEnd,
@JsonKey(name: 'revenue') num? revenue,
@JsonKey(name: 'order_count') num? orderCount,
@JsonKey(name: 'limit_purchase') num? limitPurchase,
@JsonKey(name: 'sdl') num? sdl,
@JsonKey(name: 'limit_team') num? limitTeam,
@JsonKey(name: 'month') String? month,
@@ -15958,7 +15925,6 @@ class _$ProfitSharingPeriodDtoCopyWithImpl<
Object? periodEnd = freezed,
Object? revenue = freezed,
Object? orderCount = freezed,
Object? limitPurchase = freezed,
Object? sdl = freezed,
Object? limitTeam = freezed,
Object? month = freezed,
@@ -15982,10 +15948,6 @@ class _$ProfitSharingPeriodDtoCopyWithImpl<
? _value.orderCount
: orderCount // ignore: cast_nullable_to_non_nullable
as num?,
limitPurchase: freezed == limitPurchase
? _value.limitPurchase
: limitPurchase // ignore: cast_nullable_to_non_nullable
as num?,
sdl: freezed == sdl
? _value.sdl
: sdl // ignore: cast_nullable_to_non_nullable
@@ -16022,7 +15984,6 @@ abstract class _$$ProfitSharingPeriodDtoImplCopyWith<$Res>
@JsonKey(name: 'period_end') DateTime? periodEnd,
@JsonKey(name: 'revenue') num? revenue,
@JsonKey(name: 'order_count') num? orderCount,
@JsonKey(name: 'limit_purchase') num? limitPurchase,
@JsonKey(name: 'sdl') num? sdl,
@JsonKey(name: 'limit_team') num? limitTeam,
@JsonKey(name: 'month') String? month,
@@ -16049,7 +16010,6 @@ class __$$ProfitSharingPeriodDtoImplCopyWithImpl<$Res>
Object? periodEnd = freezed,
Object? revenue = freezed,
Object? orderCount = freezed,
Object? limitPurchase = freezed,
Object? sdl = freezed,
Object? limitTeam = freezed,
Object? month = freezed,
@@ -16073,10 +16033,6 @@ class __$$ProfitSharingPeriodDtoImplCopyWithImpl<$Res>
? _value.orderCount
: orderCount // ignore: cast_nullable_to_non_nullable
as num?,
limitPurchase: freezed == limitPurchase
? _value.limitPurchase
: limitPurchase // ignore: cast_nullable_to_non_nullable
as num?,
sdl: freezed == sdl
? _value.sdl
: sdl // ignore: cast_nullable_to_non_nullable
@@ -16106,7 +16062,6 @@ class _$ProfitSharingPeriodDtoImpl extends _ProfitSharingPeriodDto {
@JsonKey(name: 'period_end') this.periodEnd,
@JsonKey(name: 'revenue') this.revenue,
@JsonKey(name: 'order_count') this.orderCount,
@JsonKey(name: 'limit_purchase') this.limitPurchase,
@JsonKey(name: 'sdl') this.sdl,
@JsonKey(name: 'limit_team') this.limitTeam,
@JsonKey(name: 'month') this.month,
@@ -16129,9 +16084,6 @@ class _$ProfitSharingPeriodDtoImpl extends _ProfitSharingPeriodDto {
@JsonKey(name: 'order_count')
final num? orderCount;
@override
@JsonKey(name: 'limit_purchase')
final num? limitPurchase;
@override
@JsonKey(name: 'sdl')
final num? sdl;
@override
@@ -16146,7 +16098,7 @@ class _$ProfitSharingPeriodDtoImpl extends _ProfitSharingPeriodDto {
@override
String toString() {
return 'ProfitSharingPeriodDto(periodStart: $periodStart, periodEnd: $periodEnd, revenue: $revenue, orderCount: $orderCount, limitPurchase: $limitPurchase, sdl: $sdl, limitTeam: $limitTeam, month: $month, weekCount: $weekCount)';
return 'ProfitSharingPeriodDto(periodStart: $periodStart, periodEnd: $periodEnd, revenue: $revenue, orderCount: $orderCount, sdl: $sdl, limitTeam: $limitTeam, month: $month, weekCount: $weekCount)';
}
@override
@@ -16161,8 +16113,6 @@ class _$ProfitSharingPeriodDtoImpl extends _ProfitSharingPeriodDto {
(identical(other.revenue, revenue) || other.revenue == revenue) &&
(identical(other.orderCount, orderCount) ||
other.orderCount == orderCount) &&
(identical(other.limitPurchase, limitPurchase) ||
other.limitPurchase == limitPurchase) &&
(identical(other.sdl, sdl) || other.sdl == sdl) &&
(identical(other.limitTeam, limitTeam) ||
other.limitTeam == limitTeam) &&
@@ -16179,7 +16129,6 @@ class _$ProfitSharingPeriodDtoImpl extends _ProfitSharingPeriodDto {
periodEnd,
revenue,
orderCount,
limitPurchase,
sdl,
limitTeam,
month,
@@ -16210,7 +16159,6 @@ abstract class _ProfitSharingPeriodDto extends ProfitSharingPeriodDto {
@JsonKey(name: 'period_end') final DateTime? periodEnd,
@JsonKey(name: 'revenue') final num? revenue,
@JsonKey(name: 'order_count') final num? orderCount,
@JsonKey(name: 'limit_purchase') final num? limitPurchase,
@JsonKey(name: 'sdl') final num? sdl,
@JsonKey(name: 'limit_team') final num? limitTeam,
@JsonKey(name: 'month') final String? month,
@@ -16234,9 +16182,6 @@ abstract class _ProfitSharingPeriodDto extends ProfitSharingPeriodDto {
@JsonKey(name: 'order_count')
num? get orderCount;
@override
@JsonKey(name: 'limit_purchase')
num? get limitPurchase;
@override
@JsonKey(name: 'sdl')
num? get sdl;
@override
@@ -1251,18 +1251,13 @@ Map<String, dynamic> _$$ProfitSharingBudgetDtoImplToJson(
_$ProfitSharingPercentageDtoImpl _$$ProfitSharingPercentageDtoImplFromJson(
Map<String, dynamic> json,
) => _$ProfitSharingPercentageDtoImpl(
purchase: json['purchase'] as num?,
owner: json['owner'] as num?,
team: json['team'] as num?,
);
Map<String, dynamic> _$$ProfitSharingPercentageDtoImplToJson(
_$ProfitSharingPercentageDtoImpl instance,
) => <String, dynamic>{
'purchase': instance.purchase,
'owner': instance.owner,
'team': instance.team,
};
) => <String, dynamic>{'owner': instance.owner, 'team': instance.team};
_$ProfitSharingPeriodDtoImpl _$$ProfitSharingPeriodDtoImplFromJson(
Map<String, dynamic> json,
@@ -1275,7 +1270,6 @@ _$ProfitSharingPeriodDtoImpl _$$ProfitSharingPeriodDtoImplFromJson(
: DateTime.parse(json['period_end'] as String),
revenue: json['revenue'] as num?,
orderCount: json['order_count'] as num?,
limitPurchase: json['limit_purchase'] as num?,
sdl: json['sdl'] as num?,
limitTeam: json['limit_team'] as num?,
month: json['month'] as String?,
@@ -1289,7 +1283,6 @@ Map<String, dynamic> _$$ProfitSharingPeriodDtoImplToJson(
'period_end': instance.periodEnd?.toIso8601String(),
'revenue': instance.revenue,
'order_count': instance.orderCount,
'limit_purchase': instance.limitPurchase,
'sdl': instance.sdl,
'limit_team': instance.limitTeam,
'month': instance.month,
@@ -1,6 +1,7 @@
import 'dart:developer';
import 'package:data_channel/data_channel.dart';
import 'package:dio/dio.dart' show Response;
import 'package:injectable/injectable.dart';
import '../../../common/api/api_client.dart';
@@ -291,7 +292,12 @@ class AnalyticRemoteDataProvider {
return DC.data(dto);
} on ApiFailure catch (e, s) {
log('fetchExclusiveSummaryError', name: _logName, error: e, stackTrace: s);
log(
'fetchExclusiveSummaryError',
name: _logName,
error: e,
stackTrace: s,
);
return DC.error(AnalyticFailure.serverError(e));
}
}
@@ -310,17 +316,18 @@ class AnalyticRemoteDataProvider {
};
if (outletId != null) params['outlet_id'] = outletId;
final response = await _apiClient.get(
ApiPath.parentCategoryAnalytic,
params: params,
headers: getAuthorizationHeader(),
);
final response = await _getProfitSharing('', params);
if (response.data['data'] == null) {
final failure = _profitSharingFailure(response);
if (failure != null) return DC.error(failure);
final body = response.data;
final data = body is Map ? body['data'] : null;
if (data == null) {
return DC.error(AnalyticFailure.empty());
}
final dto = ProfitSharingDto.fromJson(response.data['data']);
final dto = ProfitSharingDto.fromJson(data);
return DC.data(dto);
} on ApiFailure catch (e, s) {
@@ -342,17 +349,18 @@ class AnalyticRemoteDataProvider {
};
if (outletId != null) params['outlet_id'] = outletId;
final response = await _apiClient.get(
'${ApiPath.parentCategoryAnalytic}/$parentCategoryId',
params: params,
headers: getAuthorizationHeader(),
);
final response = await _getProfitSharing('/$parentCategoryId', params);
if (response.data['data'] == null) {
final failure = _profitSharingFailure(response);
if (failure != null) return DC.error(failure);
final body = response.data;
final data = body is Map ? body['data'] : null;
if (data == null) {
return DC.error(AnalyticFailure.empty());
}
final dto = ProfitSharingDetailDto.fromJson(response.data['data']);
final dto = ProfitSharingDetailDto.fromJson(data);
return DC.data(dto);
} on ApiFailure catch (e, s) {
@@ -365,4 +373,43 @@ class AnalyticRemoteDataProvider {
return DC.error(AnalyticFailure.serverError(e));
}
}
/// GET ke endpoint profit sharing. Selama backend lama masih jalan path baru
/// mengembalikan 404, jadi dicoba sekali lagi ke path lama.
Future<Response> _getProfitSharing(
String subPath,
Map<String, dynamic> params,
) async {
Future<Response> request(String basePath) => _apiClient.get(
'$basePath$subPath',
params: params,
headers: getAuthorizationHeader(),
// Status 5xx tidak dilempar supaya `cause` di body masih bisa dibaca.
validateStatus: (status) => status != null && status < 600,
);
final response = await request(ApiPath.profitSharingAnalytic);
if (response.statusCode != 404) return response;
return request(ApiPath.profitSharingAnalyticLegacy);
}
/// Failure dari status error endpoint profit sharing, null kalau berhasil.
/// Detail kategori non-team ditolak 500 dengan `cause` "... is not a team".
AnalyticFailure? _profitSharingFailure(Response response) {
final status = response.statusCode ?? 0;
if (status == 404) {
return const AnalyticFailure.serverError(ApiFailure.notFound(null));
}
if (status < 500) return null;
final body = response.data;
final errors = body is Map ? body['errors'] : null;
final isNotTeam =
errors is List &&
errors.any(
(e) => e is Map && '${e['cause']}'.contains('is not a team'),
);
if (isNotTeam) return const AnalyticFailure.notTeam();
return const AnalyticFailure.serverError(ApiFailure.internalServerError());
}
}
@@ -121,7 +121,6 @@ class ProfitSharingPercentageDto with _$ProfitSharingPercentageDto {
const ProfitSharingPercentageDto._();
const factory ProfitSharingPercentageDto({
@JsonKey(name: 'purchase') num? purchase,
@JsonKey(name: 'owner') num? owner,
@JsonKey(name: 'team') num? team,
}) = _ProfitSharingPercentageDto;
@@ -130,7 +129,6 @@ class ProfitSharingPercentageDto with _$ProfitSharingPercentageDto {
_$ProfitSharingPercentageDtoFromJson(json);
ProfitSharingPercentage toDomain() => ProfitSharingPercentage(
purchase: purchase?.toDouble() ?? 0,
owner: owner?.toDouble() ?? 0,
team: team?.toDouble() ?? 0,
);
@@ -145,7 +143,6 @@ class ProfitSharingPeriodDto with _$ProfitSharingPeriodDto {
@JsonKey(name: 'period_end') DateTime? periodEnd,
@JsonKey(name: 'revenue') num? revenue,
@JsonKey(name: 'order_count') num? orderCount,
@JsonKey(name: 'limit_purchase') num? limitPurchase,
@JsonKey(name: 'sdl') num? sdl,
@JsonKey(name: 'limit_team') num? limitTeam,
@JsonKey(name: 'month') String? month,
@@ -161,7 +158,6 @@ class ProfitSharingPeriodDto with _$ProfitSharingPeriodDto {
periodEnd: periodEnd?.toLocal() ?? DateTime.fromMillisecondsSinceEpoch(0),
revenue: revenue?.toInt() ?? 0,
orderCount: orderCount?.toInt() ?? 0,
limitPurchase: limitPurchase?.toInt() ?? 0,
sdl: sdl?.toInt() ?? 0,
limitTeam: limitTeam?.toInt() ?? 0,
month: month ?? '',
+20 -3
View File
@@ -541,18 +541,35 @@
"@total_inventory_value": {},
"profit_sharing": "Profit Sharing",
"@profit_sharing": {},
"profit_sharing_desc": "Revenue allocation for purchasing, owner, and team",
"profit_sharing_desc": "Revenue allocation for owner (SDL) and team",
"@profit_sharing_desc": {},
"profit_sharing_allocation": "Profit Sharing Allocation",
"@profit_sharing_allocation": {},
"share_purchase": "Purchasing",
"@share_purchase": {},
"profit_sharing_not_team": "This category is no longer part of profit sharing",
"@profit_sharing_not_team": {},
"share_owner": "Owner",
"@share_owner": {},
"share_team": "Team",
"@share_team": {},
"sdl": "SDL",
"@sdl": {},
"sdl_summary": "SDL Summary",
"@sdl_summary": {},
"sdl_desc": "Owner's share of revenue",
"@sdl_desc": {},
"sdl_per_parent_category": "SDL per Parent Category",
"@sdl_per_parent_category": {},
"sdl_per_week": "SDL per Week",
"@sdl_per_week": {},
"from_revenue": "from {revenue} revenue",
"@from_revenue": {
"placeholders": {
"revenue": {
"type": "String",
"example": "Rp 52,500,000"
}
}
},
"top_product": "Top Product",
"@top_product": {},
"weekly": "Weekly",
+20 -3
View File
@@ -541,18 +541,35 @@
"@total_inventory_value": {},
"profit_sharing": "Bagi Hasil",
"@profit_sharing": {},
"profit_sharing_desc": "Alokasi omzet untuk belanja, owner, dan tim",
"profit_sharing_desc": "Alokasi omzet untuk owner (SDL) dan tim",
"@profit_sharing_desc": {},
"profit_sharing_allocation": "Alokasi Bagi Hasil",
"@profit_sharing_allocation": {},
"share_purchase": "Belanja",
"@share_purchase": {},
"profit_sharing_not_team": "Kategori ini sudah tidak ikut bagi hasil",
"@profit_sharing_not_team": {},
"share_owner": "Owner",
"@share_owner": {},
"share_team": "Tim",
"@share_team": {},
"sdl": "SDL",
"@sdl": {},
"sdl_summary": "Ringkasan SDL",
"@sdl_summary": {},
"sdl_desc": "Bagian owner dari omzet",
"@sdl_desc": {},
"sdl_per_parent_category": "SDL per Kategori Induk",
"@sdl_per_parent_category": {},
"sdl_per_week": "SDL per Minggu",
"@sdl_per_week": {},
"from_revenue": "dari omzet {revenue}",
"@from_revenue": {
"placeholders": {
"revenue": {
"type": "String",
"example": "Rp 52.500.000"
}
}
},
"top_product": "Produk Terlaris",
"@top_product": {},
"weekly": "Mingguan",
+34 -4
View File
@@ -1574,7 +1574,7 @@ abstract class AppLocalizations {
/// No description provided for @profit_sharing_desc.
///
/// In en, this message translates to:
/// **'Revenue allocation for purchasing, owner, and team'**
/// **'Revenue allocation for owner (SDL) and team'**
String get profit_sharing_desc;
/// No description provided for @profit_sharing_allocation.
@@ -1583,11 +1583,11 @@ abstract class AppLocalizations {
/// **'Profit Sharing Allocation'**
String get profit_sharing_allocation;
/// No description provided for @share_purchase.
/// No description provided for @profit_sharing_not_team.
///
/// In en, this message translates to:
/// **'Purchasing'**
String get share_purchase;
/// **'This category is no longer part of profit sharing'**
String get profit_sharing_not_team;
/// No description provided for @share_owner.
///
@@ -1607,6 +1607,36 @@ abstract class AppLocalizations {
/// **'SDL'**
String get sdl;
/// No description provided for @sdl_summary.
///
/// In en, this message translates to:
/// **'SDL Summary'**
String get sdl_summary;
/// No description provided for @sdl_desc.
///
/// In en, this message translates to:
/// **'Owner\'s share of revenue'**
String get sdl_desc;
/// No description provided for @sdl_per_parent_category.
///
/// In en, this message translates to:
/// **'SDL per Parent Category'**
String get sdl_per_parent_category;
/// No description provided for @sdl_per_week.
///
/// In en, this message translates to:
/// **'SDL per Week'**
String get sdl_per_week;
/// No description provided for @from_revenue.
///
/// In en, this message translates to:
/// **'from {revenue} revenue'**
String from_revenue(String revenue);
/// No description provided for @top_product.
///
/// In en, this message translates to:
+19 -2
View File
@@ -761,13 +761,13 @@ class AppLocalizationsEn extends AppLocalizations {
String get profit_sharing => 'Profit Sharing';
@override
String get profit_sharing_desc => 'Revenue allocation for purchasing, owner, and team';
String get profit_sharing_desc => 'Revenue allocation for owner (SDL) and team';
@override
String get profit_sharing_allocation => 'Profit Sharing Allocation';
@override
String get share_purchase => 'Purchasing';
String get profit_sharing_not_team => 'This category is no longer part of profit sharing';
@override
String get share_owner => 'Owner';
@@ -778,6 +778,23 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get sdl => 'SDL';
@override
String get sdl_summary => 'SDL Summary';
@override
String get sdl_desc => 'Owner\'s share of revenue';
@override
String get sdl_per_parent_category => 'SDL per Parent Category';
@override
String get sdl_per_week => 'SDL per Week';
@override
String from_revenue(String revenue) {
return 'from $revenue revenue';
}
@override
String get top_product => 'Top Product';
+19 -2
View File
@@ -761,13 +761,13 @@ class AppLocalizationsId extends AppLocalizations {
String get profit_sharing => 'Bagi Hasil';
@override
String get profit_sharing_desc => 'Alokasi omzet untuk belanja, owner, dan tim';
String get profit_sharing_desc => 'Alokasi omzet untuk owner (SDL) dan tim';
@override
String get profit_sharing_allocation => 'Alokasi Bagi Hasil';
@override
String get share_purchase => 'Belanja';
String get profit_sharing_not_team => 'Kategori ini sudah tidak ikut bagi hasil';
@override
String get share_owner => 'Owner';
@@ -778,6 +778,23 @@ class AppLocalizationsId extends AppLocalizations {
@override
String get sdl => 'SDL';
@override
String get sdl_summary => 'Ringkasan SDL';
@override
String get sdl_desc => 'Bagian owner dari omzet';
@override
String get sdl_per_parent_category => 'SDL per Kategori Induk';
@override
String get sdl_per_week => 'SDL per Minggu';
@override
String from_revenue(String revenue) {
return 'dari omzet $revenue';
}
@override
String get top_product => 'Produk Terlaris';
@@ -9,8 +9,10 @@ import '../../../common/theme/theme.dart';
import '../../../domain/analytic/analytic.dart';
import '../../../injection.dart';
import '../../components/spacer/spacer.dart';
import 'widgets/period_label.dart';
import 'widgets/profit_sharing_allocation.dart';
import 'widgets/profit_sharing_detail_header.dart';
import 'widgets/profit_sharing_sdl.dart';
import 'widgets/profit_sharing_status.dart';
import 'widgets/profit_sharing_subcategories.dart';
@@ -48,75 +50,116 @@ class ProfitSharingDetailPage extends StatelessWidget
return Scaffold(
backgroundColor: AppColor.background,
body:
BlocBuilder<
BlocListener<
ProfitSharingDetailLoaderBloc,
ProfitSharingDetailLoaderState
>(
builder: (context, state) {
return RefreshIndicator(
backgroundColor: AppColor.white,
color: AppColor.primary,
onRefresh: () async {
context.read<ProfitSharingDetailLoaderBloc>().add(
_fetchEvent(),
);
await context
.read<ProfitSharingDetailLoaderBloc>()
.stream
.firstWhere((s) => !s.isFetching);
},
child: CustomScrollView(
physics: const AlwaysScrollableScrollPhysics(),
slivers: [
SliverToBoxAdapter(
child: ProfitSharingDetailHeader(
state: state,
fallbackTitle: parentCategoryName,
),
),
const SliverToBoxAdapter(child: SpaceHeight(16)),
if (state.isFetching)
SliverToBoxAdapter(child: _buildShimmer())
else ...[
SliverToBoxAdapter(
child: _SummaryCard(summary: state.detail.summary),
),
const SliverToBoxAdapter(child: SpaceHeight(16)),
SliverToBoxAdapter(
child: ProfitSharingAllocation(
budget: state.detail.budget,
),
),
const SliverToBoxAdapter(child: SpaceHeight(16)),
SliverToBoxAdapter(
child: ProfitSharingSubCategories(
categories: state.detail.categories,
expandedCategoryId: state.expandedCategoryId,
onToggle: (categoryId) =>
context.read<ProfitSharingDetailLoaderBloc>().add(
ProfitSharingDetailLoaderEvent.expandedCategoryChanged(
categoryId,
),
),
),
),
],
const SliverToBoxAdapter(child: SizedBox(height: 100)),
],
),
listenWhen: (previous, current) =>
previous.failureOption != current.failureOption,
listener: (context, state) {
final isNotTeam = state.failureOption.fold(
() => false,
(f) => f.maybeMap(notTeam: (_) => true, orElse: () => false),
);
// Kategori induk dimatikan dari team setelah list dimuat:
// kembali ke list dengan hasil `true` supaya list memuat ulang.
if (isNotTeam) context.router.maybePop(true);
},
child: _buildContent(),
),
);
}
Widget _buildContent() {
return BlocBuilder<
ProfitSharingDetailLoaderBloc,
ProfitSharingDetailLoaderState
>(
builder: (context, state) {
return RefreshIndicator(
backgroundColor: AppColor.white,
color: AppColor.primary,
onRefresh: () async {
context.read<ProfitSharingDetailLoaderBloc>().add(_fetchEvent());
await context
.read<ProfitSharingDetailLoaderBloc>()
.stream
.firstWhere((s) => !s.isFetching);
},
child: CustomScrollView(
physics: const AlwaysScrollableScrollPhysics(),
slivers: [
SliverToBoxAdapter(
child: ProfitSharingDetailHeader(
state: state,
fallbackTitle: parentCategoryName,
),
),
const SliverToBoxAdapter(child: SpaceHeight(16)),
if (state.isFetching)
SliverToBoxAdapter(child: _buildShimmer())
else ...[
SliverToBoxAdapter(
child: _SummaryCard(summary: state.detail.summary),
),
const SliverToBoxAdapter(child: SpaceHeight(16)),
SliverToBoxAdapter(
child: _buildSdlSection(context, state.detail),
),
const SliverToBoxAdapter(child: SpaceHeight(16)),
SliverToBoxAdapter(
child: ProfitSharingAllocation(budget: state.detail.budget),
),
const SliverToBoxAdapter(child: SpaceHeight(16)),
SliverToBoxAdapter(
child: ProfitSharingSubCategories(
categories: state.detail.categories,
expandedCategoryId: state.expandedCategoryId,
onToggle: (categoryId) =>
context.read<ProfitSharingDetailLoaderBloc>().add(
ProfitSharingDetailLoaderEvent.expandedCategoryChanged(
categoryId,
),
),
),
),
],
const SliverToBoxAdapter(child: SizedBox(height: 100)),
],
),
);
},
);
}
Widget _buildSdlSection(BuildContext context, ProfitSharingDetail detail) {
final summary = detail.summary;
return ProfitSharingSdlSection(
sdl: summary.sdl,
revenue: summary.totalRevenue,
percent: summary.ownerFeePercent,
breakdownTitle: context.lang.sdl_per_week,
items: [
for (final period in detail.budget.weekly.where((e) => e.hasRevenue))
ProfitSharingSdlItem(
label: formatPeriodLabel(period.periodStart, period.periodEnd),
sdl: period.sdl,
revenue: period.revenue,
),
],
);
}
Widget _buildShimmer() {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
@@ -187,6 +230,7 @@ class _SummaryCard extends StatelessWidget {
icon: Icons.category_rounded,
label: context.lang.sub_category,
value: '${summary.categoryCount}',
color: ProfitSharingColor.subCategory,
),
),
const SpaceWidth(10),
@@ -195,7 +239,7 @@ class _SummaryCard extends StatelessWidget {
icon: Icons.inventory_2_rounded,
label: context.lang.products,
value: '${summary.productCount}',
color: AppColor.info,
color: ProfitSharingColor.product,
),
),
const SpaceWidth(10),
@@ -204,20 +248,13 @@ class _SummaryCard extends StatelessWidget {
icon: Icons.receipt_long_rounded,
label: context.lang.orders,
value: summary.orderCount.thousandFormat,
color: AppColor.warning,
color: ProfitSharingColor.order,
),
),
],
),
const SpaceHeight(12),
ProfitSharingSdlStrip(
amount: summary.sdl,
feePercent: summary.ownerFeePercent,
),
const SpaceHeight(12),
const SpaceHeight(16),
Row(
children: [
@@ -4,15 +4,19 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:shimmer/shimmer.dart';
import '../../../application/analytic/profit_sharing_loader/profit_sharing_loader_bloc.dart';
import '../../../common/extension/extension.dart';
import '../../../common/theme/theme.dart';
import '../../../domain/analytic/analytic.dart';
import '../../../injection.dart';
import '../../components/field/date_range_picker_field.dart';
import '../../components/spacer/spacer.dart';
import '../../components/toast/flushbar.dart';
import '../../router/app_router.gr.dart';
import 'widgets/profit_sharing_allocation.dart';
import 'widgets/profit_sharing_categories.dart';
import 'widgets/profit_sharing_header.dart';
import 'widgets/profit_sharing_periods.dart';
import 'widgets/profit_sharing_sdl.dart';
@RoutePage()
class ProfitSharingPage extends StatefulWidget implements AutoRouteWrapper {
@@ -124,20 +128,23 @@ class _ProfitSharingPageState extends State<ProfitSharingPage>
opacity: _fadeAnimation,
child: ProfitSharingCategories(
categories: state.profitSharing.categories,
onCategoryTap: (category) => context.router.push(
ProfitSharingDetailRoute(
parentCategoryId: category.parentCategoryId,
parentCategoryName: category.parentCategoryName,
dateFrom: state.dateFrom,
dateTo: state.dateTo,
),
),
onCategoryTap: (category) =>
_openDetail(context, state, category),
),
),
),
const SliverToBoxAdapter(child: SpaceHeight(16)),
SliverToBoxAdapter(
child: FadeTransition(
opacity: _fadeAnimation,
child: _buildSdlSection(context, state),
),
),
const SliverToBoxAdapter(child: SpaceHeight(16)),
SliverToBoxAdapter(
child: FadeTransition(
opacity: _fadeAnimation,
@@ -174,6 +181,58 @@ class _ProfitSharingPageState extends State<ProfitSharingPage>
);
}
/// Detail kembali dengan `true` kalau kategori induknya sudah non-team;
/// list dimuat ulang supaya kategori itu hilang.
Future<void> _openDetail(
BuildContext context,
ProfitSharingLoaderState state,
ProfitSharingCategory category,
) async {
final isNotTeam = await context.router.push<bool>(
ProfitSharingDetailRoute(
parentCategoryId: category.parentCategoryId,
parentCategoryName: category.parentCategoryName,
dateFrom: state.dateFrom,
dateTo: state.dateTo,
),
);
if (isNotTeam != true || !context.mounted) return;
AppFlushbar.showError(context, context.lang.profit_sharing_not_team);
context.read<ProfitSharingLoaderBloc>().add(
const ProfitSharingLoaderEvent.fetched(),
);
}
Widget _buildSdlSection(
BuildContext context,
ProfitSharingLoaderState state,
) {
final total = state.profitSharing.budget.total;
final categories = [...state.profitSharing.categories]
..sort((a, b) => b.sdl.compareTo(a.sdl));
return ProfitSharingSdlSection(
sdl: total.sdl,
revenue: total.revenue,
// Fee owner beda tiap kategori induk, jadi yang ditampilkan persen
// efektifnya; persentase bawaan hanya dipakai saat belum ada omzet.
percent: total.hasRevenue
? total.sdlPercentage
: state.profitSharing.budget.percentages.owner,
breakdownTitle: context.lang.sdl_per_parent_category,
items: [
for (final category in categories)
ProfitSharingSdlItem(
label: category.parentCategoryName,
sdl: category.sdl,
revenue: category.totalRevenue,
feePercent: category.ownerFeePercent,
),
],
);
}
Widget _buildShimmer() {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
@@ -6,10 +6,20 @@ import '../../../../domain/analytic/analytic.dart';
import '../../../components/spacer/spacer.dart';
/// Warna tetap untuk tiap pos bagi hasil supaya konsisten di semua bagian.
/// Sengaja pakai warna jenuh, bukan default Material yang pucat.
class ProfitSharingColor {
static const Color purchase = Color(0xFF2196F3);
static const Color owner = Color(0xFF4CAF50);
static const Color team = Color(0xFFFF9800);
static const Color owner = Color(0xFF00B341);
static const Color team = Color(0xFFFF6A00);
// Status margin (Sehat / Waspada / Tidak Sehat).
static const Color healthy = Color(0xFF00B341);
static const Color watch = Color(0xFFFF9500);
static const Color critical = Color(0xFFFF1E3C);
// Tile ringkasan.
static const Color subCategory = AppColor.primary;
static const Color product = Color(0xFF7B2FF7);
static const Color order = Color(0xFF00A3D9);
}
class ProfitSharingAllocation extends StatelessWidget {
@@ -21,11 +31,14 @@ class ProfitSharingAllocation extends StatelessWidget {
Widget build(BuildContext context) {
final total = budget.total;
final percentages = budget.percentages;
// Fee owner berbeda tiap kategori induk, jadi porsi SDL dihitung dari
// Fee owner berbeda tiap kategori induk, jadi porsi dihitung dari
// nominalnya; persentase bawaan hanya dipakai saat belum ada omzet.
final sdlPercentage = total.hasRevenue
? total.sdlPercentage
: percentages.owner;
final teamPercentage = total.hasRevenue
? total.teamPercentage
: percentages.team;
return Container(
margin: const EdgeInsets.symmetric(horizontal: 16),
@@ -55,22 +68,10 @@ class ProfitSharingAllocation extends StatelessWidget {
const SpaceHeight(16),
ProfitSharingBar(
purchase: percentages.purchase,
owner: sdlPercentage,
team: percentages.team,
),
ProfitSharingBar(owner: sdlPercentage, team: teamPercentage),
const SpaceHeight(20),
_row(
context: context,
color: ProfitSharingColor.purchase,
label: context.lang.share_purchase,
percentage: percentages.purchase,
amount: total.limitPurchase,
),
const Divider(height: 24, color: AppColor.borderLight),
_row(
context: context,
color: ProfitSharingColor.owner,
@@ -83,7 +84,7 @@ class ProfitSharingAllocation extends StatelessWidget {
context: context,
color: ProfitSharingColor.team,
label: context.lang.share_team,
percentage: percentages.team,
percentage: teamPercentage,
amount: total.limitTeam,
),
],
@@ -117,14 +118,14 @@ class ProfitSharingAllocation extends StatelessWidget {
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: color.withOpacity(0.12),
color: color,
borderRadius: BorderRadius.circular(6),
),
child: Text(
'${percentage.toStringAsFixed(percentage % 1 == 0 ? 0 : 1)}%',
style: AppStyle.xs.copyWith(
color: color,
fontWeight: FontWeight.w700,
color: AppColor.textWhite,
fontWeight: FontWeight.w800,
),
),
),
@@ -145,16 +146,15 @@ class ProfitSharingAllocation extends StatelessWidget {
}
}
/// Bar proporsi belanja / SDL (owner) / tim.
/// Bar proporsi SDL (owner) / tim dalam persen omzet. Sisa yang belum terbagi
/// (backend lama masih menyisihkan porsi belanja) tampil sebagai track kosong.
class ProfitSharingBar extends StatelessWidget {
final double purchase;
final double owner;
final double team;
final double height;
const ProfitSharingBar({
super.key,
required this.purchase,
required this.owner,
required this.team,
this.height = 12,
@@ -162,7 +162,7 @@ class ProfitSharingBar extends StatelessWidget {
@override
Widget build(BuildContext context) {
final total = purchase + owner + team;
final total = owner + team;
if (total <= 0) {
return Container(
height: height,
@@ -179,9 +179,9 @@ class ProfitSharingBar extends StatelessWidget {
height: height,
child: Row(
children: [
_segment(purchase, ProfitSharingColor.purchase),
_segment(owner, ProfitSharingColor.owner),
_segment(team, ProfitSharingColor.team),
_segment(100 - total, AppColor.borderLight),
].whereType<Widget>().toList(),
),
),
@@ -4,9 +4,10 @@ import '../../../../common/extension/extension.dart';
import '../../../../common/theme/theme.dart';
import '../../../../domain/analytic/analytic.dart';
import '../../../components/spacer/spacer.dart';
import 'profit_sharing_allocation.dart';
import 'profit_sharing_status.dart';
/// Ringkasan omzet & SDL per kategori induk (parent category).
/// Ringkasan porsi tim (omzet − SDL) per kategori induk (parent category).
/// Tiap kartu bisa diketuk untuk melihat rincian sub kategori & produknya.
class ProfitSharingCategories extends StatelessWidget {
final List<ProfitSharingCategory> categories;
@@ -21,9 +22,9 @@ class ProfitSharingCategories extends StatelessWidget {
@override
Widget build(BuildContext context) {
final sorted = [...categories]
..sort((a, b) => b.totalRevenue.compareTo(a.totalRevenue));
..sort((a, b) => b.teamShare.compareTo(a.teamShare));
final totalRevenue = sorted.fold<int>(0, (sum, e) => sum + e.totalRevenue);
final totalTeam = sorted.fold<int>(0, (sum, e) => sum + e.teamShare);
return Container(
margin: const EdgeInsets.symmetric(horizontal: 16),
@@ -75,9 +76,7 @@ class ProfitSharingCategories extends StatelessWidget {
child: _CategoryCard(
rank: i + 1,
category: sorted[i],
share: totalRevenue == 0
? 0
: sorted[i].totalRevenue / totalRevenue,
share: totalTeam == 0 ? 0 : sorted[i].teamShare / totalTeam,
onTap: () => onCategoryTap(sorted[i]),
),
),
@@ -106,18 +105,11 @@ class _CategoryCard extends StatelessWidget {
Widget build(BuildContext context) {
final isTop = rank == 1;
return Material(
color: AppColor.background,
borderRadius: BorderRadius.circular(14),
return ProfitSharingItemCard(
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(14),
child: Container(
child: Padding(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(14),
border: Border.all(color: AppColor.borderLight),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -188,7 +180,7 @@ class _CategoryCard extends StatelessWidget {
children: [
Expanded(
child: Text(
category.totalRevenue.currencyFormatRp,
category.teamShare.currencyFormatRp,
style: AppStyle.xl.copyWith(
color: AppColor.textPrimary,
fontWeight: FontWeight.w800,
@@ -204,21 +196,53 @@ class _CategoryCard extends StatelessWidget {
vertical: 3,
),
decoration: BoxDecoration(
color: AppColor.primary.withOpacity(0.08),
color: AppColor.primary,
borderRadius: BorderRadius.circular(20),
),
child: Text(
formatPercent(share * 100),
style: AppStyle.xs.copyWith(
color: AppColor.primary,
fontWeight: FontWeight.w700,
color: AppColor.textWhite,
fontWeight: FontWeight.w800,
),
),
),
],
),
const SpaceHeight(8),
const SpaceHeight(4),
Row(
children: [
Text(
context.lang.share_team,
style: AppStyle.xs.copyWith(
color: ProfitSharingColor.team,
fontWeight: FontWeight.w700,
),
),
const SpaceWidth(4),
ProfitSharingFeeBadge(
feePercent: category.teamPercent,
color: ProfitSharingColor.team,
),
const SpaceWidth(6),
Expanded(
child: Text(
context.lang.from_revenue(
category.totalRevenue.currencyFormatRp,
),
style: AppStyle.xs.copyWith(
color: AppColor.textSecondary,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
),
const SpaceHeight(10),
ClipRRect(
borderRadius: BorderRadius.circular(6),
@@ -232,15 +256,8 @@ class _CategoryCard extends StatelessWidget {
),
),
const SpaceHeight(12),
ProfitSharingSdlStrip(
amount: category.sdl,
feePercent: category.ownerFeePercent,
),
if (category.hasTopProduct) ...[
const SpaceHeight(10),
const SpaceHeight(12),
_TopProduct(product: category.topProduct),
],
],
@@ -333,7 +350,7 @@ class _TotalCard extends StatelessWidget {
0,
(sum, e) => sum + e.totalRevenue,
);
final totalSdl = categories.fold<int>(0, (sum, e) => sum + e.sdl);
final totalTeam = categories.fold<int>(0, (sum, e) => sum + e.teamShare);
return Container(
padding: const EdgeInsets.all(16),
@@ -349,7 +366,8 @@ class _TotalCard extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
context.lang.grand_total,
'${context.lang.grand_total} '
'${context.lang.share_team.toUpperCase()}',
style: AppStyle.xs.copyWith(
color: AppColor.textWhite.withOpacity(0.8),
fontWeight: FontWeight.w700,
@@ -358,7 +376,7 @@ class _TotalCard extends StatelessWidget {
),
const SizedBox(height: 4),
Text(
totalRevenue.currencyFormatRp,
totalTeam.currencyFormatRp,
style: AppStyle.xl.copyWith(
color: AppColor.textWhite,
fontWeight: FontWeight.w900,
@@ -369,8 +387,8 @@ class _TotalCard extends StatelessWidget {
ProfitSharingHeaderStats(
stats: [
(context.lang.revenue, totalRevenue.currencyFormatRp),
(context.lang.qty, totalQuantity.thousandFormat),
(context.lang.sdl, totalSdl.currencyFormatRp),
],
),
],
@@ -7,6 +7,7 @@ import '../../../../domain/analytic/analytic.dart';
import '../../../components/spacer/spacer.dart';
import 'period_label.dart';
import 'profit_sharing_allocation.dart';
import 'profit_sharing_status.dart';
class ProfitSharingPeriods extends StatelessWidget {
final ProfitSharingLoaderState state;
@@ -151,78 +152,73 @@ class _PeriodCard extends StatelessWidget {
? '${formatPeriodLabel(period.periodStart, period.periodEnd)} - ${context.lang.week_count(period.weekCount)}'
: context.lang.order_count_label(period.orderCount);
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColor.background,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppColor.borderLight),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: AppStyle.md.copyWith(
fontWeight: FontWeight.w700,
color: AppColor.textPrimary,
return ProfitSharingItemCard(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: AppStyle.md.copyWith(
fontWeight: FontWeight.w700,
color: AppColor.textPrimary,
),
),
),
const SizedBox(height: 2),
Text(
subtitle,
style: AppStyle.xs.copyWith(
color: AppColor.textSecondary,
const SizedBox(height: 2),
Text(
subtitle,
style: AppStyle.xs.copyWith(
color: AppColor.textSecondary,
),
),
),
],
],
),
),
),
const SpaceWidth(8),
Text(
period.revenue.currencyFormatRp,
style: AppStyle.md.copyWith(
fontWeight: FontWeight.w800,
color: AppColor.primary,
const SpaceWidth(8),
Text(
period.revenue.currencyFormatRp,
style: AppStyle.md.copyWith(
fontWeight: FontWeight.w800,
color: AppColor.primary,
),
),
),
],
),
],
),
const SpaceHeight(12),
const SpaceHeight(12),
ProfitSharingBar(
purchase: percentages.purchase,
owner: period.hasRevenue ? period.sdlPercentage : percentages.owner,
team: percentages.team,
height: 6,
),
ProfitSharingBar(
owner: period.hasRevenue
? period.sdlPercentage
: percentages.owner,
team: period.hasRevenue
? period.teamPercentage
: percentages.team,
height: 6,
),
const SpaceHeight(12),
const SpaceHeight(12),
Row(
children: [
_share(
context.lang.share_purchase,
period.limitPurchase,
ProfitSharingColor.purchase,
),
_share(context.lang.sdl, period.sdl, ProfitSharingColor.owner),
_share(
context.lang.share_team,
period.limitTeam,
ProfitSharingColor.team,
),
],
),
],
Row(
children: [
_share(context.lang.sdl, period.sdl, ProfitSharingColor.owner),
_share(
context.lang.share_team,
period.limitTeam,
ProfitSharingColor.team,
),
],
),
],
),
),
);
}
@@ -0,0 +1,283 @@
import 'package:flutter/material.dart';
import '../../../../common/extension/extension.dart';
import '../../../../common/theme/theme.dart';
import '../../../components/spacer/spacer.dart';
import 'profit_sharing_allocation.dart';
import 'profit_sharing_status.dart';
/// Satu baris rincian SDL, bisa per kategori induk atau per periode.
class ProfitSharingSdlItem {
final String label;
final int sdl;
final int revenue;
final double? feePercent;
const ProfitSharingSdlItem({
required this.label,
required this.sdl,
required this.revenue,
this.feePercent,
});
}
/// Section khusus SDL (bagian owner): total di blok hijau, lalu rinciannya.
/// Urutan [items] ditampilkan apa adanya.
class ProfitSharingSdlSection extends StatelessWidget {
final int sdl;
final int revenue;
/// Persen di blok total: fee owner kalau satu kategori, atau persen
/// efektif terhadap omzet kalau gabungan beberapa kategori.
final double percent;
final String breakdownTitle;
final List<ProfitSharingSdlItem> items;
const ProfitSharingSdlSection({
super.key,
required this.sdl,
required this.revenue,
required this.percent,
required this.breakdownTitle,
this.items = const [],
});
@override
Widget build(BuildContext context) {
final totalSdl = items.fold<int>(0, (sum, e) => sum + e.sdl);
return Container(
margin: const EdgeInsets.symmetric(horizontal: 16),
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: AppColor.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: AppColor.textLight.withOpacity(0.08),
spreadRadius: 1,
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
context.lang.sdl_summary,
style: AppStyle.lg.copyWith(
fontWeight: FontWeight.w700,
color: AppColor.textPrimary,
),
),
const SizedBox(height: 2),
Text(
context.lang.sdl_desc,
style: AppStyle.xs.copyWith(color: AppColor.textSecondary),
),
const SpaceHeight(16),
_TotalBlock(sdl: sdl, revenue: revenue, percent: percent),
if (items.isNotEmpty) ...[
const SpaceHeight(20),
Text(
breakdownTitle,
style: AppStyle.md.copyWith(
fontWeight: FontWeight.w700,
color: AppColor.textPrimary,
),
),
const SpaceHeight(12),
for (var i = 0; i < items.length; i++) ...[
if (i > 0) const Divider(height: 24, color: AppColor.borderLight),
_SdlRow(
item: items[i],
share: totalSdl == 0 ? 0 : items[i].sdl / totalSdl,
),
],
],
],
),
);
}
}
class _TotalBlock extends StatelessWidget {
final int sdl;
final int revenue;
final double percent;
const _TotalBlock({
required this.sdl,
required this.revenue,
required this.percent,
});
@override
Widget build(BuildContext context) {
const color = ProfitSharingColor.owner;
return Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [color, darken(color)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(14),
boxShadow: [
BoxShadow(
color: color.withOpacity(0.3),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: AppColor.textWhite.withOpacity(0.2),
borderRadius: BorderRadius.circular(10),
),
child: const Icon(
Icons.account_balance_wallet_rounded,
color: AppColor.textWhite,
size: 20,
),
),
const SpaceWidth(10),
Expanded(
child: Text(
context.lang.sdl,
style: AppStyle.md.copyWith(
color: AppColor.textWhite,
fontWeight: FontWeight.w700,
),
),
),
ProfitSharingFeeBadge(
feePercent: percent,
onDarkBackground: true,
),
],
),
const SpaceHeight(14),
FittedBox(
fit: BoxFit.scaleDown,
alignment: Alignment.centerLeft,
child: Text(
sdl.currencyFormatRp,
style: AppStyle.h3.copyWith(
color: AppColor.textWhite,
fontWeight: FontWeight.w900,
),
),
),
const SizedBox(height: 4),
Text(
context.lang.from_revenue(revenue.currencyFormatRp),
style: AppStyle.sm.copyWith(
color: AppColor.textWhite.withOpacity(0.85),
),
),
],
),
);
}
}
class _SdlRow extends StatelessWidget {
final ProfitSharingSdlItem item;
/// Porsi baris ini terhadap total SDL di daftar (0..1).
final double share;
const _SdlRow({required this.item, required this.share});
@override
Widget build(BuildContext context) {
final amountColor = darken(ProfitSharingColor.owner);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Row(
children: [
Flexible(
child: Text(
item.label,
style: AppStyle.md.copyWith(
color: AppColor.textPrimary,
fontWeight: FontWeight.w700,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
if (item.feePercent != null) ...[
const SpaceWidth(6),
ProfitSharingFeeBadge(feePercent: item.feePercent!),
],
],
),
),
const SpaceWidth(8),
Text(
item.sdl.currencyFormatRp,
style: AppStyle.md.copyWith(
color: amountColor,
fontWeight: FontWeight.w800,
),
),
],
),
const SpaceHeight(8),
ClipRRect(
borderRadius: BorderRadius.circular(6),
child: LinearProgressIndicator(
value: share.clamp(0.0, 1.0),
minHeight: 6,
backgroundColor: AppColor.border,
valueColor: const AlwaysStoppedAnimation<Color>(
ProfitSharingColor.owner,
),
),
),
const SpaceHeight(6),
Row(
children: [
Expanded(
child: Text(
context.lang.from_revenue(item.revenue.currencyFormatRp),
style: AppStyle.xs.copyWith(color: AppColor.textSecondary),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
Text(
formatPercent(share * 100),
style: AppStyle.xs.copyWith(
color: amountColor,
fontWeight: FontWeight.w700,
),
),
],
),
],
);
}
}
@@ -18,10 +18,16 @@ String formatPercent(double value) => '${value.toStringAsFixed(1)}%';
String formatFeePercent(double value) =>
'${value.toStringAsFixed(value % 1 == 0 ? 0 : 1)}%';
/// Versi lebih gelap dari [color], dipakai sebagai ujung gradien.
Color darken(Color color, [double amount = 0.2]) =>
Color.lerp(color, AppColor.black, amount)!;
Color statusColor(double realHppPercentage) {
if (realHppPercentage <= HppThreshold.healthy) return AppColor.success;
if (realHppPercentage <= HppThreshold.watch) return AppColor.warning;
return AppColor.error;
if (realHppPercentage <= HppThreshold.healthy) {
return ProfitSharingColor.healthy;
}
if (realHppPercentage <= HppThreshold.watch) return ProfitSharingColor.watch;
return ProfitSharingColor.critical;
}
String statusLabel(BuildContext context, double realHppPercentage) {
@@ -47,97 +53,95 @@ class ProfitSharingStatusBadge extends StatelessWidget {
Widget build(BuildContext context) {
final color = statusColor(realHppPercentage);
// Di atas header merah, pill putih supaya status merah tidak tenggelam.
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: onDarkBackground
? AppColor.textWhite.withOpacity(0.18)
: color.withOpacity(0.12),
color: onDarkBackground ? AppColor.white : color,
borderRadius: BorderRadius.circular(20),
border: onDarkBackground
? Border.all(color: AppColor.textWhite.withOpacity(0.3))
: null,
),
child: Text(
statusLabel(context, realHppPercentage),
style: AppStyle.xs.copyWith(
color: onDarkBackground ? AppColor.textWhite : color,
fontWeight: FontWeight.w700,
color: onDarkBackground ? color : AppColor.textWhite,
fontWeight: FontWeight.w800,
),
),
);
}
}
/// Strip sorotan SDL: label + persen fee owner di kiri, nominal di kanan.
class ProfitSharingSdlStrip extends StatelessWidget {
final int amount;
final double? feePercent;
/// Kartu item di dalam section: latar putih, garis aksen berwarna di kiri.
/// Child boleh berisi InkWell; ripple-nya ikut terpotong sudut kartu.
class ProfitSharingItemCard extends StatelessWidget {
final Widget child;
final Color accentColor;
const ProfitSharingSdlStrip({
const ProfitSharingItemCard({
super.key,
required this.amount,
this.feePercent,
required this.child,
this.accentColor = AppColor.primary,
});
@override
Widget build(BuildContext context) {
const color = ProfitSharingColor.owner;
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
clipBehavior: Clip.antiAlias,
decoration: BoxDecoration(
color: color.withOpacity(0.08),
borderRadius: BorderRadius.circular(10),
border: Border.all(color: color.withOpacity(0.2)),
),
child: Row(
children: [
const Icon(
Icons.account_balance_wallet_rounded,
color: color,
size: 18,
),
const SpaceWidth(8),
Text(
context.lang.sdl,
style: AppStyle.sm.copyWith(
color: AppColor.textPrimary,
fontWeight: FontWeight.w600,
),
),
if (feePercent != null) ...[
const SpaceWidth(6),
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadius.circular(6),
),
child: Text(
formatFeePercent(feePercent!),
style: AppStyle.xs.copyWith(
color: AppColor.textWhite,
fontWeight: FontWeight.w700,
),
),
),
],
const SpaceWidth(8),
Expanded(
child: Text(
amount.currencyFormatRp,
textAlign: TextAlign.right,
style: AppStyle.md.copyWith(
color: AppColor.secondaryDark,
fontWeight: FontWeight.w800,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
color: AppColor.white,
borderRadius: BorderRadius.circular(14),
border: Border.all(color: AppColor.border),
boxShadow: [
BoxShadow(
color: AppColor.black.withOpacity(0.05),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Stack(
children: [
Material(type: MaterialType.transparency, child: child),
Positioned(
left: 0,
top: 0,
bottom: 0,
child: Container(width: 4, color: accentColor),
),
],
),
);
}
}
/// Pill persen fee owner. Di atas latar berwarna jadi putih supaya terbaca.
class ProfitSharingFeeBadge extends StatelessWidget {
final double feePercent;
final Color color;
final bool onDarkBackground;
const ProfitSharingFeeBadge({
super.key,
required this.feePercent,
this.color = ProfitSharingColor.owner,
this.onDarkBackground = false,
});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: onDarkBackground ? AppColor.white : color,
borderRadius: BorderRadius.circular(6),
),
child: Text(
formatFeePercent(feePercent),
style: AppStyle.xs.copyWith(
color: onDarkBackground ? color : AppColor.textWhite,
fontWeight: FontWeight.w800,
),
),
);
}
}
@@ -205,7 +209,7 @@ class ProfitSharingHeaderStats extends StatelessWidget {
}
}
/// Kotak statistik kecil: ikon, nilai, lalu label.
/// Kartu statistik kecil berwarna solid: ikon, nilai, lalu label.
class ProfitSharingStatTile extends StatelessWidget {
final IconData icon;
final String label;
@@ -225,9 +229,19 @@ class ProfitSharingStatTile extends StatelessWidget {
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: AppColor.background,
gradient: LinearGradient(
colors: [color, darken(color)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppColor.borderLight),
boxShadow: [
BoxShadow(
color: color.withOpacity(0.3),
blurRadius: 8,
offset: const Offset(0, 3),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -236,25 +250,30 @@ class ProfitSharingStatTile extends StatelessWidget {
width: 30,
height: 30,
decoration: BoxDecoration(
color: color.withOpacity(0.1),
color: AppColor.textWhite.withOpacity(0.2),
borderRadius: BorderRadius.circular(8),
),
child: Icon(icon, color: color, size: 16),
child: Icon(icon, color: AppColor.textWhite, size: 16),
),
const SpaceHeight(10),
Text(
value,
style: AppStyle.lg.copyWith(
color: AppColor.textPrimary,
fontWeight: FontWeight.w800,
FittedBox(
fit: BoxFit.scaleDown,
alignment: Alignment.centerLeft,
child: Text(
value,
style: AppStyle.lg.copyWith(
color: AppColor.textWhite,
fontWeight: FontWeight.w800,
),
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 2),
Text(
label,
style: AppStyle.xs.copyWith(color: AppColor.textSecondary),
style: AppStyle.xs.copyWith(
color: AppColor.textWhite.withOpacity(0.85),
fontWeight: FontWeight.w600,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
@@ -99,17 +99,11 @@ class _SubCategoryCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: AppColor.background,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppColor.borderLight),
),
return ProfitSharingItemCard(
child: Column(
children: [
InkWell(
onTap: onToggle,
borderRadius: BorderRadius.circular(12),
child: Padding(
padding: const EdgeInsets.all(14),
child: Column(
@@ -155,7 +149,7 @@ class _SubCategoryCard extends StatelessWidget {
child: LinearProgressIndicator(
value: share.clamp(0.0, 1.0),
minHeight: 5,
backgroundColor: AppColor.borderLight,
backgroundColor: AppColor.border,
valueColor: const AlwaysStoppedAnimation<Color>(
AppColor.primary,
),
+1 -1
View File
@@ -3,7 +3,7 @@ description: "A new Flutter project."
publish_to: "none"
version: 1.0.4+13
version: 1.0.4+14
environment:
sdk: ^3.8.1