Compare commits

...
13 Commits
Author SHA1 Message Date
efrilm 36bc9e0f9e feat: update
Build & Deploy iOS to TestFlight / build-and-deploy (push) Canceled after 0s
2026-08-21 23:47:19 +07:00
efrilm ac991431c8 feat: add order
Build & Deploy iOS to TestFlight / build-and-deploy (push) Canceled after 0s
2026-08-21 23:24:46 +07:00
efrilm 587caa44c5 feat: update ci
Build & Deploy iOS to TestFlight / build-and-deploy (push) Canceled after 0s
2026-08-21 22:52:19 +07:00
efrilm 2e4c77888e feat: profit sharing
Build & Deploy iOS to TestFlight / build-and-deploy (push) Canceled after 0s
2026-08-21 22:31:54 +07:00
Efril f9bfb69254 feat: home ui update and update api url
Build & Deploy iOS to TestFlight / build-and-deploy (push) Has been cancelled
2026-07-03 16:00:57 +07:00
Efril d02d3b5fbd config: update version
Build & Deploy iOS to TestFlight / build-and-deploy (push) Has been cancelled
2026-06-24 11:05:38 +07:00
Efril 7661319b4f feat: update profile
Build & Deploy iOS to TestFlight / build-and-deploy (push) Waiting to run
2026-06-24 11:05:01 +07:00
Efril 75415cc6ff feat: update main, exclusive summary and stock
Build & Deploy iOS to TestFlight / build-and-deploy (push) Waiting to run
2026-06-24 11:03:28 +07:00
Efril 843c11b200 feat: update stock ui
Build & Deploy iOS to TestFlight / build-and-deploy (push) Waiting to run
2026-06-24 10:28:13 +07:00
Efril 0917c5132b feat: update profit loss ui
Build & Deploy iOS to TestFlight / build-and-deploy (push) Waiting to run
2026-06-24 10:14:37 +07:00
Efril b07af60778 feat: update home ui
Build & Deploy iOS to TestFlight / build-and-deploy (push) Waiting to run
2026-06-23 23:33:10 +07:00
Efril 8d801e52d9 feat: update sales ui
Build & Deploy iOS to TestFlight / build-and-deploy (push) Waiting to run
2026-06-23 23:18:22 +07:00
Efril 7137cd2335 feat: update ui splash
Build & Deploy iOS to TestFlight / build-and-deploy (push) Waiting to run
2026-06-23 21:40:08 +07:00
120 changed files with 22698 additions and 4004 deletions
+4 -1
View File
@@ -18,6 +18,9 @@ jobs:
uses: subosito/flutter-action@v2
with:
channel: "stable"
# Dipin biar CI nggak ikut naik versi diam-diam.
# Flutter 3.47 mensyaratkan iOS deployment target minimal 15.0.
flutter-version: "3.47.0"
cache: true
- name: Install Dependencies
@@ -69,7 +72,7 @@ jobs:
- name: Install CocoaPods Dependencies
run: |
cd ios
rm -f Podfile.lock
rm -rf Pods Podfile.lock
pod install --repo-update
# ── Build & Archive ─────────────────────────────────────────────────────
+72 -18
View File
@@ -12,7 +12,7 @@ A POS (Point of Sale) application for business owners, built with Flutter. The p
- Architecture Overview
- Project Structure
- Key Dependencies & Purpose
- Code Generation
- Code Generation (see docs/codegen.md)
- Internationalization (i18n)
- Theming & Assets
- Environment Configuration (`env.dart`)
@@ -40,6 +40,7 @@ A POS (Point of Sale) application for business owners, built with Flutter. The p
- Flutter SDK installed as per the official guide (`https://flutter.dev/docs/get-started/install`)
- Dart version per constraint: ^3.8.1
- For code generation only: FVM + Flutter 3.41.9 (`dart pub global activate fvm && fvm install 3.41.9`) — see [docs/codegen.md](docs/codegen.md)
### Installation
@@ -51,8 +52,16 @@ flutter pub get
### Code generation (required after clone or when annotations change)
```bash
flutter pub run build_runner build --delete-conflicting-outputs
> **Do not run codegen with Flutter 3.47 / Dart 3.13** — every builder crashes with
> `Missing implementation of visitDotShorthandPropertyAccess`. Codegen must run on Flutter 3.41.9
> (Dart 3.11.5) via FVM. Full setup, commands, and troubleshooting: **[docs/codegen.md](docs/codegen.md)**.
```powershell
$FVM = "$env:USERPROFILE\fvm\versions\3.41.9\bin"
& "$FVM\flutter.bat" pub get
& "$FVM\dart.bat" run build_runner build --delete-conflicting-outputs
flutter pub get # restore lockfile resolution for the global SDK
```
> Re-run whenever you change files using `@RoutePage()`, `@injectable`, `@freezed`, or `@JsonSerializable`.
@@ -179,14 +188,27 @@ Refer to `pubspec.yaml` for full version constraints.
## Code Generation
**Full guide: [docs/codegen.md](docs/codegen.md)** — read it before your first codegen run.
Codegen runs on a pinned SDK (Flutter 3.41.9 / Dart 3.11.5 via FVM), not on the global Flutter 3.47:
`freezed 2.5.8` caps `analyzer` below 8.0.0, and analyzer 7.7.1 crashes on the dot-shorthand syntax
used by Dart 3.13 sources. The app itself still runs and builds with the global SDK.
Common commands:
```bash
flutter pub run build_runner build --delete-conflicting-outputs
```powershell
$FVM = "$env:USERPROFILE\fvm\versions\3.41.9\bin"
& "$FVM\flutter.bat" pub get
& "$FVM\dart.bat" run build_runner build --delete-conflicting-outputs
# or watch
flutter pub run build_runner watch --delete-conflicting-outputs
& "$FVM\dart.bat" run build_runner watch --delete-conflicting-outputs
flutter pub get # restore lockfile resolution for the global SDK
```
> `--delete-conflicting-outputs` wipes every generated file first. If you cancel the build midway,
> restore them with `git checkout` — see the troubleshooting table in [docs/codegen.md](docs/codegen.md).
### Asset generation (flutter_gen_runner)
`flutter_gen_runner` is integrated with `build_runner`. Running the commands above will also generate typed accessors for assets based on the `flutter_gen` section in `pubspec.yaml`:
@@ -255,15 +277,20 @@ flutter pub get
flutter format .
flutter analyze
# Code generation (single run / watch)
flutter pub run build_runner build --delete-conflicting-outputs
flutter pub run build_runner watch --delete-conflicting-outputs
# Localization (safe on the global SDK)
flutter gen-l10n
# Run by device
flutter devices
flutter run -d <device-id>
```
Code generation uses the pinned FVM SDK — see [docs/codegen.md](docs/codegen.md):
```powershell
& "$env:USERPROFILE\fvm\versions\3.41.9\bin\dart.bat" run build_runner build --delete-conflicting-outputs
```
---
## Additional Notes
@@ -316,7 +343,7 @@ Aplikasi Point of Sale (POS) untuk pemilik usaha, dibangun dengan Flutter. Proye
- Arsitektur & Alur
- Struktur Proyek
- Dependensi Utama & Fungsinya
- Code Generation
- Code Generation (lihat docs/codegen.md)
- Internationalization (i18n)
- Theming & Assets
- Konfigurasi Lingkungan (`env.dart`)
@@ -344,6 +371,7 @@ Aplikasi Point of Sale (POS) untuk pemilik usaha, dibangun dengan Flutter. Proye
- Flutter SDK terpasang sesuai panduan resmi (`https://flutter.dev/docs/get-started/install`)
- Versi Dart sesuai constraint: ^3.8.1
- Khusus untuk code generation: FVM + Flutter 3.41.9 (`dart pub global activate fvm && fvm install 3.41.9`) — lihat [docs/codegen.md](docs/codegen.md)
### Instalasi
@@ -355,8 +383,16 @@ flutter pub get
### Generate kode (wajib setelah clone atau mengubah anotasi)
```bash
flutter pub run build_runner build --delete-conflicting-outputs
> **Jangan jalankan codegen dengan Flutter 3.47 / Dart 3.13** — semua builder crash dengan
> `Missing implementation of visitDotShorthandPropertyAccess`. Codegen harus memakai Flutter 3.41.9
> (Dart 3.11.5) via FVM. Setup, perintah, dan troubleshooting lengkap: **[docs/codegen.md](docs/codegen.md)**.
```powershell
$FVM = "$env:USERPROFILE\fvm\versions\3.41.9\bin"
& "$FVM\flutter.bat" pub get
& "$FVM\dart.bat" run build_runner build --delete-conflicting-outputs
flutter pub get # kembalikan resolusi lockfile ke SDK global
```
> Jalankan ulang perintah di atas setiap kali Anda mengubah file yang menggunakan anotasi `@RoutePage()`, `@injectable`, `@freezed`, atau `@JsonSerializable`.
@@ -472,14 +508,27 @@ Catatan versi lengkap tersedia di `pubspec.yaml`.
## Code Generation
**Panduan lengkap: [docs/codegen.md](docs/codegen.md)** — baca dulu sebelum codegen pertama kali.
Codegen dijalankan dengan SDK terpisah (Flutter 3.41.9 / Dart 3.11.5 via FVM), bukan Flutter global 3.47:
`freezed 2.5.8` mengunci `analyzer` di bawah 8.0.0, dan analyzer 7.7.1 crash pada sintaks *dot-shorthand*
milik Dart 3.13. Aplikasinya sendiri tetap di-`run`/`build` dengan SDK global.
Perintah umum:
```bash
flutter pub run build_runner build --delete-conflicting-outputs
```powershell
$FVM = "$env:USERPROFILE\fvm\versions\3.41.9\bin"
& "$FVM\flutter.bat" pub get
& "$FVM\dart.bat" run build_runner build --delete-conflicting-outputs
# atau untuk watch
flutter pub run build_runner watch --delete-conflicting-outputs
& "$FVM\dart.bat" run build_runner watch --delete-conflicting-outputs
flutter pub get # kembalikan resolusi lockfile ke SDK global
```
> `--delete-conflicting-outputs` menghapus semua file generated lebih dulu. Kalau build dibatalkan di
> tengah jalan, pulihkan dengan `git checkout` — lihat tabel troubleshooting di [docs/codegen.md](docs/codegen.md).
Generator yang digunakan:
- AutoRoute: menghasilkan deklarasi router & route.
@@ -533,15 +582,20 @@ flutter pub get
flutter format .
flutter analyze
# Code generation (sekali jalan / watch)
flutter pub run build_runner build --delete-conflicting-outputs
flutter pub run build_runner watch --delete-conflicting-outputs
# Localization (aman dengan SDK global)
flutter gen-l10n
# Run by device
flutter devices
flutter run -d <device-id>
```
Code generation memakai SDK FVM yang di-pin — lihat [docs/codegen.md](docs/codegen.md):
```powershell
& "$env:USERPROFILE\fvm\versions\3.41.9\bin\dart.bat" run build_runner build --delete-conflicting-outputs
```
---
## Catatan Tambahan
+7
View File
@@ -19,3 +19,10 @@ analyzer:
- test/generated/**
- "**/**.g.dart"
- "**/**.freezed.dart"
- build/**
- android/**
- ios/**
- web/**
- windows/**
- macos/**
- linux/**
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-all.zip
+2 -2
View File
@@ -18,8 +18,8 @@ pluginManagement {
plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "8.7.3" apply false
id("org.jetbrains.kotlin.android") version "2.1.0" apply false
id("com.android.application") version "8.11.1" apply false
id("org.jetbrains.kotlin.android") version "2.2.20" apply false
id("com.google.gms.google-services") version "4.4.2" apply false
}
+163
View File
@@ -0,0 +1,163 @@
# Code Generation (build_runner)
> **PENTING:** `build_runner` di proyek ini **tidak bisa** dijalankan dengan Flutter 3.47 / Dart 3.13.
> Gunakan SDK Flutter 3.41.9 (Dart 3.11.5) lewat FVM. Aplikasinya sendiri tetap di-`run`/`build`
> dengan Flutter versi global (3.47).
---
## TL;DR
PowerShell (Windows):
```powershell
$FVM = "$env:USERPROFILE\fvm\versions\3.41.9\bin"
& "$FVM\flutter.bat" pub get # 1. resolve paket dgn Dart 3.11.5
& "$FVM\dart.bat" run build_runner build --delete-conflicting-outputs # 2. generate
flutter pub get # 3. balik ke SDK global 3.47
```
Git Bash:
```bash
FVM=~/fvm/versions/3.41.9/bin
"$FVM/flutter.bat" pub get
"$FVM/dart.bat" run build_runner build --delete-conflicting-outputs
flutter pub get
```
Durasi normal: **±90–120 detik** untuk full build (146 output).
Setelah itu jalankan `flutter analyze` untuk memastikan hasil generate bersih.
---
## Kenapa harus SDK terpisah?
Kalau codegen dijalankan dengan Flutter global (3.47 / Dart 3.13), semua builder gagal dengan:
```
W SDK language version 3.13.0 is newer than `analyzer` language version 3.9.0.
E freezed on lib/application/.../category_analytic_loader_bloc.dart:
Exception: Missing implementation of visitDotShorthandPropertyAccess
```
Rantai penyebabnya:
1. `freezed 2.5.8` membatasi `analyzer <8.0.0`, jadi pub me-resolve ke **analyzer 7.7.1** (language version 3.9).
2. Dart 3.13 memakai sintaks *dot-shorthand* (`.value`) di sumber SDK/framework.
3. Analyzer 7.7.1 bisa mem-*parse*-nya tapi belum bisa menuliskannya ke summary → crash saat resolve.
4. Semua generator (`freezed`, `json_serializable`, `injectable_generator`, `auto_route_generator`) berbagi analyzer yang sama, jadi semuanya ikut mati — termasuk saat men-generate file lama yang tidak diubah.
Dart 3.11.5 (Flutter 3.41.9) adalah versi tertinggi yang **masih cocok dengan analyzer 7.7.1** sekaligus memenuhi batas `pubspec.lock` (`dart >=3.11.0-0`).
---
## Setup sekali di mesin baru
```powershell
dart pub global activate fvm
```
Tambahkan folder binari pub ke PATH agar perintah `fvm` dikenali:
`%LOCALAPPDATA%\Pub\Cache\bin`
Lalu unduh SDK-nya (±1 GB, sekali saja):
```powershell
fvm install 3.41.9
fvm list # verifikasi
```
SDK tersimpan di `%USERPROFILE%\fvm\versions\3.41.9` dan **tidak** mengubah Flutter global.
> Kita sengaja **tidak** menjalankan `fvm use`, supaya repo tidak punya `.fvmrc`/`.fvm/`
> dan IDE tetap memakai Flutter 3.47 untuk menjalankan aplikasi.
> Kalau lebih suka pin per-project, jalankan `fvm use 3.41.9` lalu pakai `fvm dart run build_runner ...`,
> dan tambahkan `.fvm/` ke `.gitignore`.
---
## Perintah harian
Semua contoh memakai variabel `$FVM` dari bagian TL;DR.
```powershell
# build sekali (paling sering dipakai)
& "$FVM\dart.bat" run build_runner build --delete-conflicting-outputs
# mode watch saat banyak mengubah anotasi
& "$FVM\dart.bat" run build_runner watch --delete-conflicting-outputs
# hanya membersihkan output generated
& "$FVM\dart.bat" run build_runner clean
```
Wajib generate ulang setiap kali mengubah/menambah file dengan anotasi:
| Anotasi | Generator | Output |
| --- | --- | --- |
| `@freezed` | freezed | `*.freezed.dart` |
| `@JsonSerializable` / `fromJson` di DTO | json_serializable | `*.g.dart` |
| `@injectable`, `@LazySingleton` | injectable_generator | `lib/injection.config.dart` |
| `@RoutePage()` | auto_route_generator | `lib/presentation/router/app_router.gr.dart` |
| asset baru di `assets/` | flutter_gen_runner | `lib/presentation/components/assets/assets.gen.dart` |
### Localization (i18n) — tidak lewat build_runner
`flutter gen-l10n` memakai tooling Flutter, bukan analyzer, jadi **aman dijalankan dengan Flutter 3.47**:
```powershell
flutter gen-l10n
```
Jalankan setiap kali `lib/l10n/app_id.arb` / `app_en.arb` berubah. Hasilnya: `lib/l10n/app_localizations*.dart`.
---
## Aturan penting soal `--delete-conflicting-outputs`
Flag ini **menghapus dulu** semua file generated (±55 file `*.freezed.dart` / `*.g.dart`) sebelum membuat ulang.
Kalau proses dibatalkan di tengah jalan, project akan penuh error karena file-nya sudah hilang.
Pemulihan (semua file generated ikut ter-commit di git):
```bash
git status --porcelain | grep -E "^ D" | sed 's/^ D //' | while read -r f; do git checkout -- "$f"; done
```
Tips: jangan pipe output build ke `tail`/`head` — outputnya jadi ter-buffer sampai proses selesai,
sehingga terlihat seperti *hang* padahal sedang jalan.
---
## Troubleshooting
| Gejala | Penyebab | Solusi |
| --- | --- | --- |
| `Missing implementation of visitDotShorthandPropertyAccess` | codegen dijalankan dengan Flutter global 3.47 | pakai SDK FVM 3.41.9 |
| `Failed to update packages` + dump stack VM | `flutter pub run build_runner` / `fvm spawn ... pub run` | pakai `dart.bat run build_runner`, jangan `flutter pub run` |
| Ratusan error “isn't defined” di IDE, file `*.freezed.dart` hilang | build dibatalkan setelah `--delete-conflicting-outputs` | restore lewat `git checkout` (lihat di atas) |
| `pub get` gagal / versi paket bergeser | `pubspec.lock` di-resolve oleh SDK berbeda | jalankan `flutter pub get` dengan SDK global setelah codegen selesai |
| Build terasa lama di run pertama | asset graph baru dibuat dari nol | wajar, ±90–120 detik; run berikutnya inkremental |
---
## Rencana jangka panjang
Perbaikan permanen = upgrade toolchain codegen supaya jalan native di Dart 3.13:
- `freezed` 2.5.8 → 3.x, `freezed_annotation` 2.4.x → 3.x
- `injectable` 2.x → 3.x, `injectable_generator` 2.7 → 3.x
- `auto_route` 9.x → 11.x, `auto_route_generator` 9.x → 10.x
- `build_runner` → 2.16.x, `json_serializable` → 6.14.x (analyzer ikut naik ke 13.x)
Konsekuensinya breaking dan cukup besar, jadi perlu dijadwalkan sendiri:
- freezed 3 **menghapus** `.map()` / `.when()` / `.maybeWhen()` / `.maybeMap()` → ±49 pemakaian (mayoritas di handler event BLoC) harus diganti pattern matching Dart 3 (`switch`).
- ±98 file dengan `@freezed` harus dideklarasikan `abstract class` atau `sealed class`.
- Migrasi konfigurasi router (auto_route 11) dan DI (injectable 3).
Selama migrasi itu belum dikerjakan, gunakan alur FVM di dokumen ini.
+1 -1
View File
@@ -21,6 +21,6 @@
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>12.0</string>
<string>15.0</string>
</dict>
</plist>
+2 -2
View File
@@ -1,5 +1,5 @@
# Uncomment this line to define a global platform for your project
platform :ios, '14.0'
platform :ios, '15.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
@@ -40,7 +40,7 @@ post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)
target.build_configurations.each do |config|
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '14.0'
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '15.0'
config.build_settings['CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES'] = 'YES'
# Disable code signing for all Pod targets — only Runner needs signing
config.build_settings['CODE_SIGNING_ALLOWED'] = 'NO'
+6 -6
View File
@@ -476,7 +476,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
@@ -495,7 +495,7 @@
DEVELOPMENT_TEAM = 5TRC3M8UZG;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@@ -607,7 +607,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
@@ -658,7 +658,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
@@ -679,7 +679,7 @@
DEVELOPMENT_TEAM = 5TRC3M8UZG;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@@ -703,7 +703,7 @@
DEVELOPMENT_TEAM = 5TRC3M8UZG;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@@ -25,6 +25,9 @@ class CategoryAnalyticLoaderBloc
Emitter<CategoryAnalyticLoaderState> emit,
) {
return event.map(
rangeDateChanged: (e) async {
emit(state.copyWith(dateFrom: e.dateFrom, dateTo: e.dateTo));
},
fetched: (e) async {
emit(
state.copyWith(
@@ -34,8 +37,8 @@ class CategoryAnalyticLoaderBloc
);
final result = await _repository.getCategory(
dateFrom: DateTime.now().subtract(const Duration(days: 30)),
dateTo: DateTime.now(),
dateFrom: state.dateFrom,
dateTo: state.dateTo,
);
var data = result.fold(
@@ -19,27 +19,34 @@ final _privateConstructorUsedError = UnsupportedError(
mixin _$CategoryAnalyticLoaderEvent {
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function(DateTime dateFrom, DateTime dateTo)
rangeDateChanged,
required TResult Function() fetched,
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function(DateTime dateFrom, DateTime dateTo)? rangeDateChanged,
TResult? Function()? fetched,
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function(DateTime dateFrom, DateTime dateTo)? rangeDateChanged,
TResult Function()? fetched,
required TResult orElse(),
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult map<TResult extends Object?>({
required TResult Function(_RangeDateChanged value) rangeDateChanged,
required TResult Function(_Fetched value) fetched,
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult? Function(_RangeDateChanged value)? rangeDateChanged,
TResult? Function(_Fetched value)? fetched,
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
TResult Function(_RangeDateChanged value)? rangeDateChanged,
TResult Function(_Fetched value)? fetched,
required TResult orElse(),
}) => throw _privateConstructorUsedError;
@@ -74,6 +81,165 @@ class _$CategoryAnalyticLoaderEventCopyWithImpl<
/// with the given fields replaced by the non-null parameter values.
}
/// @nodoc
abstract class _$$RangeDateChangedImplCopyWith<$Res> {
factory _$$RangeDateChangedImplCopyWith(
_$RangeDateChangedImpl value,
$Res Function(_$RangeDateChangedImpl) then,
) = __$$RangeDateChangedImplCopyWithImpl<$Res>;
@useResult
$Res call({DateTime dateFrom, DateTime dateTo});
}
/// @nodoc
class __$$RangeDateChangedImplCopyWithImpl<$Res>
extends
_$CategoryAnalyticLoaderEventCopyWithImpl<$Res, _$RangeDateChangedImpl>
implements _$$RangeDateChangedImplCopyWith<$Res> {
__$$RangeDateChangedImplCopyWithImpl(
_$RangeDateChangedImpl _value,
$Res Function(_$RangeDateChangedImpl) _then,
) : super(_value, _then);
/// Create a copy of CategoryAnalyticLoaderEvent
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({Object? dateFrom = null, Object? dateTo = null}) {
return _then(
_$RangeDateChangedImpl(
null == dateFrom
? _value.dateFrom
: dateFrom // ignore: cast_nullable_to_non_nullable
as DateTime,
null == dateTo
? _value.dateTo
: dateTo // ignore: cast_nullable_to_non_nullable
as DateTime,
),
);
}
}
/// @nodoc
class _$RangeDateChangedImpl implements _RangeDateChanged {
const _$RangeDateChangedImpl(this.dateFrom, this.dateTo);
@override
final DateTime dateFrom;
@override
final DateTime dateTo;
@override
String toString() {
return 'CategoryAnalyticLoaderEvent.rangeDateChanged(dateFrom: $dateFrom, dateTo: $dateTo)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$RangeDateChangedImpl &&
(identical(other.dateFrom, dateFrom) ||
other.dateFrom == dateFrom) &&
(identical(other.dateTo, dateTo) || other.dateTo == dateTo));
}
@override
int get hashCode => Object.hash(runtimeType, dateFrom, dateTo);
/// Create a copy of CategoryAnalyticLoaderEvent
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$RangeDateChangedImplCopyWith<_$RangeDateChangedImpl> get copyWith =>
__$$RangeDateChangedImplCopyWithImpl<_$RangeDateChangedImpl>(
this,
_$identity,
);
@override
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function(DateTime dateFrom, DateTime dateTo)
rangeDateChanged,
required TResult Function() fetched,
}) {
return rangeDateChanged(dateFrom, dateTo);
}
@override
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function(DateTime dateFrom, DateTime dateTo)? rangeDateChanged,
TResult? Function()? fetched,
}) {
return rangeDateChanged?.call(dateFrom, dateTo);
}
@override
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function(DateTime dateFrom, DateTime dateTo)? rangeDateChanged,
TResult Function()? fetched,
required TResult orElse(),
}) {
if (rangeDateChanged != null) {
return rangeDateChanged(dateFrom, dateTo);
}
return orElse();
}
@override
@optionalTypeArgs
TResult map<TResult extends Object?>({
required TResult Function(_RangeDateChanged value) rangeDateChanged,
required TResult Function(_Fetched value) fetched,
}) {
return rangeDateChanged(this);
}
@override
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult? Function(_RangeDateChanged value)? rangeDateChanged,
TResult? Function(_Fetched value)? fetched,
}) {
return rangeDateChanged?.call(this);
}
@override
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
TResult Function(_RangeDateChanged value)? rangeDateChanged,
TResult Function(_Fetched value)? fetched,
required TResult orElse(),
}) {
if (rangeDateChanged != null) {
return rangeDateChanged(this);
}
return orElse();
}
}
abstract class _RangeDateChanged implements CategoryAnalyticLoaderEvent {
const factory _RangeDateChanged(
final DateTime dateFrom,
final DateTime dateTo,
) = _$RangeDateChangedImpl;
DateTime get dateFrom;
DateTime get dateTo;
/// Create a copy of CategoryAnalyticLoaderEvent
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
_$$RangeDateChangedImplCopyWith<_$RangeDateChangedImpl> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class _$$FetchedImplCopyWith<$Res> {
factory _$$FetchedImplCopyWith(
@@ -116,19 +282,27 @@ class _$FetchedImpl implements _Fetched {
@override
@optionalTypeArgs
TResult when<TResult extends Object?>({required TResult Function() fetched}) {
TResult when<TResult extends Object?>({
required TResult Function(DateTime dateFrom, DateTime dateTo)
rangeDateChanged,
required TResult Function() fetched,
}) {
return fetched();
}
@override
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({TResult? Function()? fetched}) {
TResult? whenOrNull<TResult extends Object?>({
TResult? Function(DateTime dateFrom, DateTime dateTo)? rangeDateChanged,
TResult? Function()? fetched,
}) {
return fetched?.call();
}
@override
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function(DateTime dateFrom, DateTime dateTo)? rangeDateChanged,
TResult Function()? fetched,
required TResult orElse(),
}) {
@@ -141,6 +315,7 @@ class _$FetchedImpl implements _Fetched {
@override
@optionalTypeArgs
TResult map<TResult extends Object?>({
required TResult Function(_RangeDateChanged value) rangeDateChanged,
required TResult Function(_Fetched value) fetched,
}) {
return fetched(this);
@@ -149,6 +324,7 @@ class _$FetchedImpl implements _Fetched {
@override
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult? Function(_RangeDateChanged value)? rangeDateChanged,
TResult? Function(_Fetched value)? fetched,
}) {
return fetched?.call(this);
@@ -157,6 +333,7 @@ class _$FetchedImpl implements _Fetched {
@override
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
TResult Function(_RangeDateChanged value)? rangeDateChanged,
TResult Function(_Fetched value)? fetched,
required TResult orElse(),
}) {
@@ -177,6 +354,8 @@ mixin _$CategoryAnalyticLoaderState {
Option<AnalyticFailure> get failureOptionCategoryAnalytic =>
throw _privateConstructorUsedError;
bool get isFetching => throw _privateConstructorUsedError;
DateTime get dateFrom => throw _privateConstructorUsedError;
DateTime get dateTo => throw _privateConstructorUsedError;
/// Create a copy of CategoryAnalyticLoaderState
/// with the given fields replaced by the non-null parameter values.
@@ -200,6 +379,8 @@ abstract class $CategoryAnalyticLoaderStateCopyWith<$Res> {
CategoryAnalytic categoryAnalytic,
Option<AnalyticFailure> failureOptionCategoryAnalytic,
bool isFetching,
DateTime dateFrom,
DateTime dateTo,
});
$CategoryAnalyticCopyWith<$Res> get categoryAnalytic;
@@ -226,6 +407,8 @@ class _$CategoryAnalyticLoaderStateCopyWithImpl<
Object? categoryAnalytic = null,
Object? failureOptionCategoryAnalytic = null,
Object? isFetching = null,
Object? dateFrom = null,
Object? dateTo = null,
}) {
return _then(
_value.copyWith(
@@ -241,6 +424,14 @@ class _$CategoryAnalyticLoaderStateCopyWithImpl<
? _value.isFetching
: isFetching // ignore: cast_nullable_to_non_nullable
as bool,
dateFrom: null == dateFrom
? _value.dateFrom
: dateFrom // ignore: cast_nullable_to_non_nullable
as DateTime,
dateTo: null == dateTo
? _value.dateTo
: dateTo // ignore: cast_nullable_to_non_nullable
as DateTime,
)
as $Val,
);
@@ -270,6 +461,8 @@ abstract class _$$CategoryAnalyticLoaderStateImplCopyWith<$Res>
CategoryAnalytic categoryAnalytic,
Option<AnalyticFailure> failureOptionCategoryAnalytic,
bool isFetching,
DateTime dateFrom,
DateTime dateTo,
});
@override
@@ -297,6 +490,8 @@ class __$$CategoryAnalyticLoaderStateImplCopyWithImpl<$Res>
Object? categoryAnalytic = null,
Object? failureOptionCategoryAnalytic = null,
Object? isFetching = null,
Object? dateFrom = null,
Object? dateTo = null,
}) {
return _then(
_$CategoryAnalyticLoaderStateImpl(
@@ -312,6 +507,14 @@ class __$$CategoryAnalyticLoaderStateImplCopyWithImpl<$Res>
? _value.isFetching
: isFetching // ignore: cast_nullable_to_non_nullable
as bool,
dateFrom: null == dateFrom
? _value.dateFrom
: dateFrom // ignore: cast_nullable_to_non_nullable
as DateTime,
dateTo: null == dateTo
? _value.dateTo
: dateTo // ignore: cast_nullable_to_non_nullable
as DateTime,
),
);
}
@@ -325,6 +528,8 @@ class _$CategoryAnalyticLoaderStateImpl
required this.categoryAnalytic,
required this.failureOptionCategoryAnalytic,
this.isFetching = false,
required this.dateFrom,
required this.dateTo,
});
@override
@@ -334,10 +539,14 @@ class _$CategoryAnalyticLoaderStateImpl
@override
@JsonKey()
final bool isFetching;
@override
final DateTime dateFrom;
@override
final DateTime dateTo;
@override
String toString() {
return 'CategoryAnalyticLoaderState(categoryAnalytic: $categoryAnalytic, failureOptionCategoryAnalytic: $failureOptionCategoryAnalytic, isFetching: $isFetching)';
return 'CategoryAnalyticLoaderState(categoryAnalytic: $categoryAnalytic, failureOptionCategoryAnalytic: $failureOptionCategoryAnalytic, isFetching: $isFetching, dateFrom: $dateFrom, dateTo: $dateTo)';
}
@override
@@ -354,7 +563,10 @@ class _$CategoryAnalyticLoaderStateImpl
other.failureOptionCategoryAnalytic ==
failureOptionCategoryAnalytic) &&
(identical(other.isFetching, isFetching) ||
other.isFetching == isFetching));
other.isFetching == isFetching) &&
(identical(other.dateFrom, dateFrom) ||
other.dateFrom == dateFrom) &&
(identical(other.dateTo, dateTo) || other.dateTo == dateTo));
}
@override
@@ -363,6 +575,8 @@ class _$CategoryAnalyticLoaderStateImpl
categoryAnalytic,
failureOptionCategoryAnalytic,
isFetching,
dateFrom,
dateTo,
);
/// Create a copy of CategoryAnalyticLoaderState
@@ -383,6 +597,8 @@ abstract class _CategoryAnalyticLoaderState
required final CategoryAnalytic categoryAnalytic,
required final Option<AnalyticFailure> failureOptionCategoryAnalytic,
final bool isFetching,
required final DateTime dateFrom,
required final DateTime dateTo,
}) = _$CategoryAnalyticLoaderStateImpl;
@override
@@ -391,6 +607,10 @@ abstract class _CategoryAnalyticLoaderState
Option<AnalyticFailure> get failureOptionCategoryAnalytic;
@override
bool get isFetching;
@override
DateTime get dateFrom;
@override
DateTime get dateTo;
/// Create a copy of CategoryAnalyticLoaderState
/// with the given fields replaced by the non-null parameter values.
@@ -2,5 +2,9 @@ part of 'category_analytic_loader_bloc.dart';
@freezed
class CategoryAnalyticLoaderEvent with _$CategoryAnalyticLoaderEvent {
const factory CategoryAnalyticLoaderEvent.rangeDateChanged(
DateTime dateFrom,
DateTime dateTo,
) = _RangeDateChanged;
const factory CategoryAnalyticLoaderEvent.fetched() = _Fetched;
}
@@ -6,10 +6,14 @@ class CategoryAnalyticLoaderState with _$CategoryAnalyticLoaderState {
required CategoryAnalytic categoryAnalytic,
required Option<AnalyticFailure> failureOptionCategoryAnalytic,
@Default(false) bool isFetching,
required DateTime dateFrom,
required DateTime dateTo,
}) = _CategoryAnalyticLoaderState;
factory CategoryAnalyticLoaderState.initial() => CategoryAnalyticLoaderState(
categoryAnalytic: CategoryAnalytic.empty(),
failureOptionCategoryAnalytic: none(),
dateFrom: DateTime.now(),
dateTo: DateTime.now(),
);
}
@@ -28,6 +28,9 @@ class PaymentMethodAnalyticLoaderBloc
Emitter<PaymentMethodAnalyticLoaderState> emit,
) {
return event.map(
rangeDateChanged: (e) async {
emit(state.copyWith(dateFrom: e.dateFrom, dateTo: e.dateTo));
},
fetched: (e) async {
emit(
state.copyWith(
@@ -37,8 +40,8 @@ class PaymentMethodAnalyticLoaderBloc
);
final result = await _repository.getPaymentMethod(
dateFrom: DateTime.now().subtract(const Duration(days: 30)),
dateTo: DateTime.now(),
dateFrom: state.dateFrom,
dateTo: state.dateTo,
);
var data = result.fold(
@@ -19,27 +19,34 @@ final _privateConstructorUsedError = UnsupportedError(
mixin _$PaymentMethodAnalyticLoaderEvent {
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function(DateTime dateFrom, DateTime dateTo)
rangeDateChanged,
required TResult Function() fetched,
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function(DateTime dateFrom, DateTime dateTo)? rangeDateChanged,
TResult? Function()? fetched,
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function(DateTime dateFrom, DateTime dateTo)? rangeDateChanged,
TResult Function()? fetched,
required TResult orElse(),
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult map<TResult extends Object?>({
required TResult Function(_RangeDateChanged value) rangeDateChanged,
required TResult Function(_Fetched value) fetched,
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult? Function(_RangeDateChanged value)? rangeDateChanged,
TResult? Function(_Fetched value)? fetched,
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
TResult Function(_RangeDateChanged value)? rangeDateChanged,
TResult Function(_Fetched value)? fetched,
required TResult orElse(),
}) => throw _privateConstructorUsedError;
@@ -74,6 +81,168 @@ class _$PaymentMethodAnalyticLoaderEventCopyWithImpl<
/// with the given fields replaced by the non-null parameter values.
}
/// @nodoc
abstract class _$$RangeDateChangedImplCopyWith<$Res> {
factory _$$RangeDateChangedImplCopyWith(
_$RangeDateChangedImpl value,
$Res Function(_$RangeDateChangedImpl) then,
) = __$$RangeDateChangedImplCopyWithImpl<$Res>;
@useResult
$Res call({DateTime dateFrom, DateTime dateTo});
}
/// @nodoc
class __$$RangeDateChangedImplCopyWithImpl<$Res>
extends
_$PaymentMethodAnalyticLoaderEventCopyWithImpl<
$Res,
_$RangeDateChangedImpl
>
implements _$$RangeDateChangedImplCopyWith<$Res> {
__$$RangeDateChangedImplCopyWithImpl(
_$RangeDateChangedImpl _value,
$Res Function(_$RangeDateChangedImpl) _then,
) : super(_value, _then);
/// Create a copy of PaymentMethodAnalyticLoaderEvent
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({Object? dateFrom = null, Object? dateTo = null}) {
return _then(
_$RangeDateChangedImpl(
null == dateFrom
? _value.dateFrom
: dateFrom // ignore: cast_nullable_to_non_nullable
as DateTime,
null == dateTo
? _value.dateTo
: dateTo // ignore: cast_nullable_to_non_nullable
as DateTime,
),
);
}
}
/// @nodoc
class _$RangeDateChangedImpl implements _RangeDateChanged {
const _$RangeDateChangedImpl(this.dateFrom, this.dateTo);
@override
final DateTime dateFrom;
@override
final DateTime dateTo;
@override
String toString() {
return 'PaymentMethodAnalyticLoaderEvent.rangeDateChanged(dateFrom: $dateFrom, dateTo: $dateTo)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$RangeDateChangedImpl &&
(identical(other.dateFrom, dateFrom) ||
other.dateFrom == dateFrom) &&
(identical(other.dateTo, dateTo) || other.dateTo == dateTo));
}
@override
int get hashCode => Object.hash(runtimeType, dateFrom, dateTo);
/// Create a copy of PaymentMethodAnalyticLoaderEvent
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$RangeDateChangedImplCopyWith<_$RangeDateChangedImpl> get copyWith =>
__$$RangeDateChangedImplCopyWithImpl<_$RangeDateChangedImpl>(
this,
_$identity,
);
@override
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function(DateTime dateFrom, DateTime dateTo)
rangeDateChanged,
required TResult Function() fetched,
}) {
return rangeDateChanged(dateFrom, dateTo);
}
@override
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function(DateTime dateFrom, DateTime dateTo)? rangeDateChanged,
TResult? Function()? fetched,
}) {
return rangeDateChanged?.call(dateFrom, dateTo);
}
@override
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function(DateTime dateFrom, DateTime dateTo)? rangeDateChanged,
TResult Function()? fetched,
required TResult orElse(),
}) {
if (rangeDateChanged != null) {
return rangeDateChanged(dateFrom, dateTo);
}
return orElse();
}
@override
@optionalTypeArgs
TResult map<TResult extends Object?>({
required TResult Function(_RangeDateChanged value) rangeDateChanged,
required TResult Function(_Fetched value) fetched,
}) {
return rangeDateChanged(this);
}
@override
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult? Function(_RangeDateChanged value)? rangeDateChanged,
TResult? Function(_Fetched value)? fetched,
}) {
return rangeDateChanged?.call(this);
}
@override
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
TResult Function(_RangeDateChanged value)? rangeDateChanged,
TResult Function(_Fetched value)? fetched,
required TResult orElse(),
}) {
if (rangeDateChanged != null) {
return rangeDateChanged(this);
}
return orElse();
}
}
abstract class _RangeDateChanged implements PaymentMethodAnalyticLoaderEvent {
const factory _RangeDateChanged(
final DateTime dateFrom,
final DateTime dateTo,
) = _$RangeDateChangedImpl;
DateTime get dateFrom;
DateTime get dateTo;
/// Create a copy of PaymentMethodAnalyticLoaderEvent
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
_$$RangeDateChangedImplCopyWith<_$RangeDateChangedImpl> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class _$$FetchedImplCopyWith<$Res> {
factory _$$FetchedImplCopyWith(
@@ -116,19 +285,27 @@ class _$FetchedImpl implements _Fetched {
@override
@optionalTypeArgs
TResult when<TResult extends Object?>({required TResult Function() fetched}) {
TResult when<TResult extends Object?>({
required TResult Function(DateTime dateFrom, DateTime dateTo)
rangeDateChanged,
required TResult Function() fetched,
}) {
return fetched();
}
@override
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({TResult? Function()? fetched}) {
TResult? whenOrNull<TResult extends Object?>({
TResult? Function(DateTime dateFrom, DateTime dateTo)? rangeDateChanged,
TResult? Function()? fetched,
}) {
return fetched?.call();
}
@override
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function(DateTime dateFrom, DateTime dateTo)? rangeDateChanged,
TResult Function()? fetched,
required TResult orElse(),
}) {
@@ -141,6 +318,7 @@ class _$FetchedImpl implements _Fetched {
@override
@optionalTypeArgs
TResult map<TResult extends Object?>({
required TResult Function(_RangeDateChanged value) rangeDateChanged,
required TResult Function(_Fetched value) fetched,
}) {
return fetched(this);
@@ -149,6 +327,7 @@ class _$FetchedImpl implements _Fetched {
@override
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult? Function(_RangeDateChanged value)? rangeDateChanged,
TResult? Function(_Fetched value)? fetched,
}) {
return fetched?.call(this);
@@ -157,6 +336,7 @@ class _$FetchedImpl implements _Fetched {
@override
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
TResult Function(_RangeDateChanged value)? rangeDateChanged,
TResult Function(_Fetched value)? fetched,
required TResult orElse(),
}) {
@@ -178,6 +358,8 @@ mixin _$PaymentMethodAnalyticLoaderState {
Option<AnalyticFailure> get failureOptionPaymentMethodAnalytic =>
throw _privateConstructorUsedError;
bool get isFetching => throw _privateConstructorUsedError;
DateTime get dateFrom => throw _privateConstructorUsedError;
DateTime get dateTo => throw _privateConstructorUsedError;
/// Create a copy of PaymentMethodAnalyticLoaderState
/// with the given fields replaced by the non-null parameter values.
@@ -201,6 +383,8 @@ abstract class $PaymentMethodAnalyticLoaderStateCopyWith<$Res> {
PaymentMethodAnalytic paymentMethodAnalytic,
Option<AnalyticFailure> failureOptionPaymentMethodAnalytic,
bool isFetching,
DateTime dateFrom,
DateTime dateTo,
});
$PaymentMethodAnalyticCopyWith<$Res> get paymentMethodAnalytic;
@@ -227,6 +411,8 @@ class _$PaymentMethodAnalyticLoaderStateCopyWithImpl<
Object? paymentMethodAnalytic = null,
Object? failureOptionPaymentMethodAnalytic = null,
Object? isFetching = null,
Object? dateFrom = null,
Object? dateTo = null,
}) {
return _then(
_value.copyWith(
@@ -243,6 +429,14 @@ class _$PaymentMethodAnalyticLoaderStateCopyWithImpl<
? _value.isFetching
: isFetching // ignore: cast_nullable_to_non_nullable
as bool,
dateFrom: null == dateFrom
? _value.dateFrom
: dateFrom // ignore: cast_nullable_to_non_nullable
as DateTime,
dateTo: null == dateTo
? _value.dateTo
: dateTo // ignore: cast_nullable_to_non_nullable
as DateTime,
)
as $Val,
);
@@ -274,6 +468,8 @@ abstract class _$$PaymentMethodAnalyticLoaderStateImplCopyWith<$Res>
PaymentMethodAnalytic paymentMethodAnalytic,
Option<AnalyticFailure> failureOptionPaymentMethodAnalytic,
bool isFetching,
DateTime dateFrom,
DateTime dateTo,
});
@override
@@ -301,6 +497,8 @@ class __$$PaymentMethodAnalyticLoaderStateImplCopyWithImpl<$Res>
Object? paymentMethodAnalytic = null,
Object? failureOptionPaymentMethodAnalytic = null,
Object? isFetching = null,
Object? dateFrom = null,
Object? dateTo = null,
}) {
return _then(
_$PaymentMethodAnalyticLoaderStateImpl(
@@ -317,6 +515,14 @@ class __$$PaymentMethodAnalyticLoaderStateImplCopyWithImpl<$Res>
? _value.isFetching
: isFetching // ignore: cast_nullable_to_non_nullable
as bool,
dateFrom: null == dateFrom
? _value.dateFrom
: dateFrom // ignore: cast_nullable_to_non_nullable
as DateTime,
dateTo: null == dateTo
? _value.dateTo
: dateTo // ignore: cast_nullable_to_non_nullable
as DateTime,
),
);
}
@@ -330,6 +536,8 @@ class _$PaymentMethodAnalyticLoaderStateImpl
required this.paymentMethodAnalytic,
required this.failureOptionPaymentMethodAnalytic,
this.isFetching = false,
required this.dateFrom,
required this.dateTo,
});
@override
@@ -339,10 +547,14 @@ class _$PaymentMethodAnalyticLoaderStateImpl
@override
@JsonKey()
final bool isFetching;
@override
final DateTime dateFrom;
@override
final DateTime dateTo;
@override
String toString() {
return 'PaymentMethodAnalyticLoaderState(paymentMethodAnalytic: $paymentMethodAnalytic, failureOptionPaymentMethodAnalytic: $failureOptionPaymentMethodAnalytic, isFetching: $isFetching)';
return 'PaymentMethodAnalyticLoaderState(paymentMethodAnalytic: $paymentMethodAnalytic, failureOptionPaymentMethodAnalytic: $failureOptionPaymentMethodAnalytic, isFetching: $isFetching, dateFrom: $dateFrom, dateTo: $dateTo)';
}
@override
@@ -359,7 +571,10 @@ class _$PaymentMethodAnalyticLoaderStateImpl
other.failureOptionPaymentMethodAnalytic ==
failureOptionPaymentMethodAnalytic) &&
(identical(other.isFetching, isFetching) ||
other.isFetching == isFetching));
other.isFetching == isFetching) &&
(identical(other.dateFrom, dateFrom) ||
other.dateFrom == dateFrom) &&
(identical(other.dateTo, dateTo) || other.dateTo == dateTo));
}
@override
@@ -368,6 +583,8 @@ class _$PaymentMethodAnalyticLoaderStateImpl
paymentMethodAnalytic,
failureOptionPaymentMethodAnalytic,
isFetching,
dateFrom,
dateTo,
);
/// Create a copy of PaymentMethodAnalyticLoaderState
@@ -390,6 +607,8 @@ abstract class _PaymentMethodAnalyticLoaderState
required final PaymentMethodAnalytic paymentMethodAnalytic,
required final Option<AnalyticFailure> failureOptionPaymentMethodAnalytic,
final bool isFetching,
required final DateTime dateFrom,
required final DateTime dateTo,
}) = _$PaymentMethodAnalyticLoaderStateImpl;
@override
@@ -398,6 +617,10 @@ abstract class _PaymentMethodAnalyticLoaderState
Option<AnalyticFailure> get failureOptionPaymentMethodAnalytic;
@override
bool get isFetching;
@override
DateTime get dateFrom;
@override
DateTime get dateTo;
/// Create a copy of PaymentMethodAnalyticLoaderState
/// with the given fields replaced by the non-null parameter values.
@@ -2,5 +2,9 @@ part of 'payment_method_analytic_loader_bloc.dart';
@freezed
class PaymentMethodAnalyticLoaderEvent with _$PaymentMethodAnalyticLoaderEvent {
const factory PaymentMethodAnalyticLoaderEvent.rangeDateChanged(
DateTime dateFrom,
DateTime dateTo,
) = _RangeDateChanged;
const factory PaymentMethodAnalyticLoaderEvent.fetched() = _Fetched;
}
@@ -6,11 +6,15 @@ class PaymentMethodAnalyticLoaderState with _$PaymentMethodAnalyticLoaderState {
required PaymentMethodAnalytic paymentMethodAnalytic,
required Option<AnalyticFailure> failureOptionPaymentMethodAnalytic,
@Default(false) bool isFetching,
required DateTime dateFrom,
required DateTime dateTo,
}) = _PaymentMethodAnalyticLoaderState;
factory PaymentMethodAnalyticLoaderState.initial() =>
PaymentMethodAnalyticLoaderState(
paymentMethodAnalytic: PaymentMethodAnalytic.empty(),
failureOptionPaymentMethodAnalytic: none(),
dateFrom: DateTime.now(),
dateTo: DateTime.now(),
);
}
@@ -13,7 +13,7 @@ class ProductAnalyticLoaderState with _$ProductAnalyticLoaderState {
factory ProductAnalyticLoaderState.initial() => ProductAnalyticLoaderState(
productAnalytic: ProductAnalytic.empty(),
failureOptionProductAnalytic: none(),
dateFrom: DateTime.now().subtract(const Duration(days: 30)),
dateFrom: DateTime.now(),
dateTo: DateTime.now(),
);
}
@@ -0,0 +1,65 @@
import 'package:dartz/dartz.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:injectable/injectable.dart';
import '../../../domain/analytic/analytic.dart';
import '../../../domain/analytic/repositories/i_analytic_repository.dart';
part 'profit_sharing_detail_loader_event.dart';
part 'profit_sharing_detail_loader_state.dart';
part 'profit_sharing_detail_loader_bloc.freezed.dart';
@injectable
class ProfitSharingDetailLoaderBloc
extends
Bloc<ProfitSharingDetailLoaderEvent, ProfitSharingDetailLoaderState> {
final IAnalyticRepository _repository;
ProfitSharingDetailLoaderBloc(this._repository)
: super(ProfitSharingDetailLoaderState.initial()) {
on<ProfitSharingDetailLoaderEvent>(_onProfitSharingDetailLoaderEvent);
}
Future<void> _onProfitSharingDetailLoaderEvent(
ProfitSharingDetailLoaderEvent event,
Emitter<ProfitSharingDetailLoaderState> emit,
) {
return event.map(
expandedCategoryChanged: (e) async {
final isSame = state.expandedCategoryId == e.categoryId;
emit(state.copyWith(expandedCategoryId: isSame ? '' : e.categoryId));
},
fetched: (e) async {
emit(
state.copyWith(
isFetching: true,
failureOption: none(),
parentCategoryId: e.parentCategoryId,
dateFrom: e.dateFrom,
dateTo: e.dateTo,
),
);
final result = await _repository.getProfitSharingDetail(
parentCategoryId: e.parentCategoryId,
dateFrom: e.dateFrom,
dateTo: e.dateTo,
);
final newState = result.fold(
(f) => state.copyWith(failureOption: optionOf(f)),
(detail) => state.copyWith(
detail: detail,
// Sub kategori pertama langsung terbuka supaya produknya kelihatan.
expandedCategoryId: detail.categories.isEmpty
? ''
: detail.categories.first.categoryId,
),
);
emit(newState.copyWith(isFetching: false));
},
);
}
}
@@ -0,0 +1,772 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'profit_sharing_detail_loader_bloc.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models',
);
/// @nodoc
mixin _$ProfitSharingDetailLoaderEvent {
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function(
String parentCategoryId,
DateTime dateFrom,
DateTime dateTo,
)
fetched,
required TResult Function(String categoryId) expandedCategoryChanged,
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function(
String parentCategoryId,
DateTime dateFrom,
DateTime dateTo,
)?
fetched,
TResult? Function(String categoryId)? expandedCategoryChanged,
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function(
String parentCategoryId,
DateTime dateFrom,
DateTime dateTo,
)?
fetched,
TResult Function(String categoryId)? expandedCategoryChanged,
required TResult orElse(),
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult map<TResult extends Object?>({
required TResult Function(_Fetched value) fetched,
required TResult Function(_ExpandedCategoryChanged value)
expandedCategoryChanged,
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult? Function(_Fetched value)? fetched,
TResult? Function(_ExpandedCategoryChanged value)? expandedCategoryChanged,
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
TResult Function(_Fetched value)? fetched,
TResult Function(_ExpandedCategoryChanged value)? expandedCategoryChanged,
required TResult orElse(),
}) => throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $ProfitSharingDetailLoaderEventCopyWith<$Res> {
factory $ProfitSharingDetailLoaderEventCopyWith(
ProfitSharingDetailLoaderEvent value,
$Res Function(ProfitSharingDetailLoaderEvent) then,
) =
_$ProfitSharingDetailLoaderEventCopyWithImpl<
$Res,
ProfitSharingDetailLoaderEvent
>;
}
/// @nodoc
class _$ProfitSharingDetailLoaderEventCopyWithImpl<
$Res,
$Val extends ProfitSharingDetailLoaderEvent
>
implements $ProfitSharingDetailLoaderEventCopyWith<$Res> {
_$ProfitSharingDetailLoaderEventCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of ProfitSharingDetailLoaderEvent
/// with the given fields replaced by the non-null parameter values.
}
/// @nodoc
abstract class _$$FetchedImplCopyWith<$Res> {
factory _$$FetchedImplCopyWith(
_$FetchedImpl value,
$Res Function(_$FetchedImpl) then,
) = __$$FetchedImplCopyWithImpl<$Res>;
@useResult
$Res call({String parentCategoryId, DateTime dateFrom, DateTime dateTo});
}
/// @nodoc
class __$$FetchedImplCopyWithImpl<$Res>
extends _$ProfitSharingDetailLoaderEventCopyWithImpl<$Res, _$FetchedImpl>
implements _$$FetchedImplCopyWith<$Res> {
__$$FetchedImplCopyWithImpl(
_$FetchedImpl _value,
$Res Function(_$FetchedImpl) _then,
) : super(_value, _then);
/// Create a copy of ProfitSharingDetailLoaderEvent
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? parentCategoryId = null,
Object? dateFrom = null,
Object? dateTo = null,
}) {
return _then(
_$FetchedImpl(
parentCategoryId: null == parentCategoryId
? _value.parentCategoryId
: parentCategoryId // ignore: cast_nullable_to_non_nullable
as String,
dateFrom: null == dateFrom
? _value.dateFrom
: dateFrom // ignore: cast_nullable_to_non_nullable
as DateTime,
dateTo: null == dateTo
? _value.dateTo
: dateTo // ignore: cast_nullable_to_non_nullable
as DateTime,
),
);
}
}
/// @nodoc
class _$FetchedImpl implements _Fetched {
const _$FetchedImpl({
required this.parentCategoryId,
required this.dateFrom,
required this.dateTo,
});
@override
final String parentCategoryId;
@override
final DateTime dateFrom;
@override
final DateTime dateTo;
@override
String toString() {
return 'ProfitSharingDetailLoaderEvent.fetched(parentCategoryId: $parentCategoryId, dateFrom: $dateFrom, dateTo: $dateTo)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$FetchedImpl &&
(identical(other.parentCategoryId, parentCategoryId) ||
other.parentCategoryId == parentCategoryId) &&
(identical(other.dateFrom, dateFrom) ||
other.dateFrom == dateFrom) &&
(identical(other.dateTo, dateTo) || other.dateTo == dateTo));
}
@override
int get hashCode =>
Object.hash(runtimeType, parentCategoryId, dateFrom, dateTo);
/// Create a copy of ProfitSharingDetailLoaderEvent
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$FetchedImplCopyWith<_$FetchedImpl> get copyWith =>
__$$FetchedImplCopyWithImpl<_$FetchedImpl>(this, _$identity);
@override
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function(
String parentCategoryId,
DateTime dateFrom,
DateTime dateTo,
)
fetched,
required TResult Function(String categoryId) expandedCategoryChanged,
}) {
return fetched(parentCategoryId, dateFrom, dateTo);
}
@override
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function(
String parentCategoryId,
DateTime dateFrom,
DateTime dateTo,
)?
fetched,
TResult? Function(String categoryId)? expandedCategoryChanged,
}) {
return fetched?.call(parentCategoryId, dateFrom, dateTo);
}
@override
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function(
String parentCategoryId,
DateTime dateFrom,
DateTime dateTo,
)?
fetched,
TResult Function(String categoryId)? expandedCategoryChanged,
required TResult orElse(),
}) {
if (fetched != null) {
return fetched(parentCategoryId, dateFrom, dateTo);
}
return orElse();
}
@override
@optionalTypeArgs
TResult map<TResult extends Object?>({
required TResult Function(_Fetched value) fetched,
required TResult Function(_ExpandedCategoryChanged value)
expandedCategoryChanged,
}) {
return fetched(this);
}
@override
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult? Function(_Fetched value)? fetched,
TResult? Function(_ExpandedCategoryChanged value)? expandedCategoryChanged,
}) {
return fetched?.call(this);
}
@override
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
TResult Function(_Fetched value)? fetched,
TResult Function(_ExpandedCategoryChanged value)? expandedCategoryChanged,
required TResult orElse(),
}) {
if (fetched != null) {
return fetched(this);
}
return orElse();
}
}
abstract class _Fetched implements ProfitSharingDetailLoaderEvent {
const factory _Fetched({
required final String parentCategoryId,
required final DateTime dateFrom,
required final DateTime dateTo,
}) = _$FetchedImpl;
String get parentCategoryId;
DateTime get dateFrom;
DateTime get dateTo;
/// Create a copy of ProfitSharingDetailLoaderEvent
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
_$$FetchedImplCopyWith<_$FetchedImpl> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class _$$ExpandedCategoryChangedImplCopyWith<$Res> {
factory _$$ExpandedCategoryChangedImplCopyWith(
_$ExpandedCategoryChangedImpl value,
$Res Function(_$ExpandedCategoryChangedImpl) then,
) = __$$ExpandedCategoryChangedImplCopyWithImpl<$Res>;
@useResult
$Res call({String categoryId});
}
/// @nodoc
class __$$ExpandedCategoryChangedImplCopyWithImpl<$Res>
extends
_$ProfitSharingDetailLoaderEventCopyWithImpl<
$Res,
_$ExpandedCategoryChangedImpl
>
implements _$$ExpandedCategoryChangedImplCopyWith<$Res> {
__$$ExpandedCategoryChangedImplCopyWithImpl(
_$ExpandedCategoryChangedImpl _value,
$Res Function(_$ExpandedCategoryChangedImpl) _then,
) : super(_value, _then);
/// Create a copy of ProfitSharingDetailLoaderEvent
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({Object? categoryId = null}) {
return _then(
_$ExpandedCategoryChangedImpl(
null == categoryId
? _value.categoryId
: categoryId // ignore: cast_nullable_to_non_nullable
as String,
),
);
}
}
/// @nodoc
class _$ExpandedCategoryChangedImpl implements _ExpandedCategoryChanged {
const _$ExpandedCategoryChangedImpl(this.categoryId);
@override
final String categoryId;
@override
String toString() {
return 'ProfitSharingDetailLoaderEvent.expandedCategoryChanged(categoryId: $categoryId)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$ExpandedCategoryChangedImpl &&
(identical(other.categoryId, categoryId) ||
other.categoryId == categoryId));
}
@override
int get hashCode => Object.hash(runtimeType, categoryId);
/// Create a copy of ProfitSharingDetailLoaderEvent
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$ExpandedCategoryChangedImplCopyWith<_$ExpandedCategoryChangedImpl>
get copyWith =>
__$$ExpandedCategoryChangedImplCopyWithImpl<
_$ExpandedCategoryChangedImpl
>(this, _$identity);
@override
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function(
String parentCategoryId,
DateTime dateFrom,
DateTime dateTo,
)
fetched,
required TResult Function(String categoryId) expandedCategoryChanged,
}) {
return expandedCategoryChanged(categoryId);
}
@override
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function(
String parentCategoryId,
DateTime dateFrom,
DateTime dateTo,
)?
fetched,
TResult? Function(String categoryId)? expandedCategoryChanged,
}) {
return expandedCategoryChanged?.call(categoryId);
}
@override
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function(
String parentCategoryId,
DateTime dateFrom,
DateTime dateTo,
)?
fetched,
TResult Function(String categoryId)? expandedCategoryChanged,
required TResult orElse(),
}) {
if (expandedCategoryChanged != null) {
return expandedCategoryChanged(categoryId);
}
return orElse();
}
@override
@optionalTypeArgs
TResult map<TResult extends Object?>({
required TResult Function(_Fetched value) fetched,
required TResult Function(_ExpandedCategoryChanged value)
expandedCategoryChanged,
}) {
return expandedCategoryChanged(this);
}
@override
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult? Function(_Fetched value)? fetched,
TResult? Function(_ExpandedCategoryChanged value)? expandedCategoryChanged,
}) {
return expandedCategoryChanged?.call(this);
}
@override
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
TResult Function(_Fetched value)? fetched,
TResult Function(_ExpandedCategoryChanged value)? expandedCategoryChanged,
required TResult orElse(),
}) {
if (expandedCategoryChanged != null) {
return expandedCategoryChanged(this);
}
return orElse();
}
}
abstract class _ExpandedCategoryChanged
implements ProfitSharingDetailLoaderEvent {
const factory _ExpandedCategoryChanged(final String categoryId) =
_$ExpandedCategoryChangedImpl;
String get categoryId;
/// Create a copy of ProfitSharingDetailLoaderEvent
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
_$$ExpandedCategoryChangedImplCopyWith<_$ExpandedCategoryChangedImpl>
get copyWith => throw _privateConstructorUsedError;
}
/// @nodoc
mixin _$ProfitSharingDetailLoaderState {
ProfitSharingDetail get detail => throw _privateConstructorUsedError;
Option<AnalyticFailure> get failureOption =>
throw _privateConstructorUsedError;
bool get isFetching => throw _privateConstructorUsedError;
String get parentCategoryId => throw _privateConstructorUsedError;
String get expandedCategoryId => throw _privateConstructorUsedError;
DateTime get dateFrom => throw _privateConstructorUsedError;
DateTime get dateTo => throw _privateConstructorUsedError;
/// Create a copy of ProfitSharingDetailLoaderState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$ProfitSharingDetailLoaderStateCopyWith<ProfitSharingDetailLoaderState>
get copyWith => throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $ProfitSharingDetailLoaderStateCopyWith<$Res> {
factory $ProfitSharingDetailLoaderStateCopyWith(
ProfitSharingDetailLoaderState value,
$Res Function(ProfitSharingDetailLoaderState) then,
) =
_$ProfitSharingDetailLoaderStateCopyWithImpl<
$Res,
ProfitSharingDetailLoaderState
>;
@useResult
$Res call({
ProfitSharingDetail detail,
Option<AnalyticFailure> failureOption,
bool isFetching,
String parentCategoryId,
String expandedCategoryId,
DateTime dateFrom,
DateTime dateTo,
});
$ProfitSharingDetailCopyWith<$Res> get detail;
}
/// @nodoc
class _$ProfitSharingDetailLoaderStateCopyWithImpl<
$Res,
$Val extends ProfitSharingDetailLoaderState
>
implements $ProfitSharingDetailLoaderStateCopyWith<$Res> {
_$ProfitSharingDetailLoaderStateCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of ProfitSharingDetailLoaderState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? detail = null,
Object? failureOption = null,
Object? isFetching = null,
Object? parentCategoryId = null,
Object? expandedCategoryId = null,
Object? dateFrom = null,
Object? dateTo = null,
}) {
return _then(
_value.copyWith(
detail: null == detail
? _value.detail
: detail // ignore: cast_nullable_to_non_nullable
as ProfitSharingDetail,
failureOption: null == failureOption
? _value.failureOption
: failureOption // ignore: cast_nullable_to_non_nullable
as Option<AnalyticFailure>,
isFetching: null == isFetching
? _value.isFetching
: isFetching // ignore: cast_nullable_to_non_nullable
as bool,
parentCategoryId: null == parentCategoryId
? _value.parentCategoryId
: parentCategoryId // ignore: cast_nullable_to_non_nullable
as String,
expandedCategoryId: null == expandedCategoryId
? _value.expandedCategoryId
: expandedCategoryId // ignore: cast_nullable_to_non_nullable
as String,
dateFrom: null == dateFrom
? _value.dateFrom
: dateFrom // ignore: cast_nullable_to_non_nullable
as DateTime,
dateTo: null == dateTo
? _value.dateTo
: dateTo // ignore: cast_nullable_to_non_nullable
as DateTime,
)
as $Val,
);
}
/// Create a copy of ProfitSharingDetailLoaderState
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$ProfitSharingDetailCopyWith<$Res> get detail {
return $ProfitSharingDetailCopyWith<$Res>(_value.detail, (value) {
return _then(_value.copyWith(detail: value) as $Val);
});
}
}
/// @nodoc
abstract class _$$ProfitSharingDetailLoaderStateImplCopyWith<$Res>
implements $ProfitSharingDetailLoaderStateCopyWith<$Res> {
factory _$$ProfitSharingDetailLoaderStateImplCopyWith(
_$ProfitSharingDetailLoaderStateImpl value,
$Res Function(_$ProfitSharingDetailLoaderStateImpl) then,
) = __$$ProfitSharingDetailLoaderStateImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({
ProfitSharingDetail detail,
Option<AnalyticFailure> failureOption,
bool isFetching,
String parentCategoryId,
String expandedCategoryId,
DateTime dateFrom,
DateTime dateTo,
});
@override
$ProfitSharingDetailCopyWith<$Res> get detail;
}
/// @nodoc
class __$$ProfitSharingDetailLoaderStateImplCopyWithImpl<$Res>
extends
_$ProfitSharingDetailLoaderStateCopyWithImpl<
$Res,
_$ProfitSharingDetailLoaderStateImpl
>
implements _$$ProfitSharingDetailLoaderStateImplCopyWith<$Res> {
__$$ProfitSharingDetailLoaderStateImplCopyWithImpl(
_$ProfitSharingDetailLoaderStateImpl _value,
$Res Function(_$ProfitSharingDetailLoaderStateImpl) _then,
) : super(_value, _then);
/// Create a copy of ProfitSharingDetailLoaderState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? detail = null,
Object? failureOption = null,
Object? isFetching = null,
Object? parentCategoryId = null,
Object? expandedCategoryId = null,
Object? dateFrom = null,
Object? dateTo = null,
}) {
return _then(
_$ProfitSharingDetailLoaderStateImpl(
detail: null == detail
? _value.detail
: detail // ignore: cast_nullable_to_non_nullable
as ProfitSharingDetail,
failureOption: null == failureOption
? _value.failureOption
: failureOption // ignore: cast_nullable_to_non_nullable
as Option<AnalyticFailure>,
isFetching: null == isFetching
? _value.isFetching
: isFetching // ignore: cast_nullable_to_non_nullable
as bool,
parentCategoryId: null == parentCategoryId
? _value.parentCategoryId
: parentCategoryId // ignore: cast_nullable_to_non_nullable
as String,
expandedCategoryId: null == expandedCategoryId
? _value.expandedCategoryId
: expandedCategoryId // ignore: cast_nullable_to_non_nullable
as String,
dateFrom: null == dateFrom
? _value.dateFrom
: dateFrom // ignore: cast_nullable_to_non_nullable
as DateTime,
dateTo: null == dateTo
? _value.dateTo
: dateTo // ignore: cast_nullable_to_non_nullable
as DateTime,
),
);
}
}
/// @nodoc
class _$ProfitSharingDetailLoaderStateImpl
extends _ProfitSharingDetailLoaderState {
const _$ProfitSharingDetailLoaderStateImpl({
required this.detail,
required this.failureOption,
this.isFetching = false,
this.parentCategoryId = '',
this.expandedCategoryId = '',
required this.dateFrom,
required this.dateTo,
}) : super._();
@override
final ProfitSharingDetail detail;
@override
final Option<AnalyticFailure> failureOption;
@override
@JsonKey()
final bool isFetching;
@override
@JsonKey()
final String parentCategoryId;
@override
@JsonKey()
final String expandedCategoryId;
@override
final DateTime dateFrom;
@override
final DateTime dateTo;
@override
String toString() {
return 'ProfitSharingDetailLoaderState(detail: $detail, failureOption: $failureOption, isFetching: $isFetching, parentCategoryId: $parentCategoryId, expandedCategoryId: $expandedCategoryId, dateFrom: $dateFrom, dateTo: $dateTo)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$ProfitSharingDetailLoaderStateImpl &&
(identical(other.detail, detail) || other.detail == detail) &&
(identical(other.failureOption, failureOption) ||
other.failureOption == failureOption) &&
(identical(other.isFetching, isFetching) ||
other.isFetching == isFetching) &&
(identical(other.parentCategoryId, parentCategoryId) ||
other.parentCategoryId == parentCategoryId) &&
(identical(other.expandedCategoryId, expandedCategoryId) ||
other.expandedCategoryId == expandedCategoryId) &&
(identical(other.dateFrom, dateFrom) ||
other.dateFrom == dateFrom) &&
(identical(other.dateTo, dateTo) || other.dateTo == dateTo));
}
@override
int get hashCode => Object.hash(
runtimeType,
detail,
failureOption,
isFetching,
parentCategoryId,
expandedCategoryId,
dateFrom,
dateTo,
);
/// Create a copy of ProfitSharingDetailLoaderState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$ProfitSharingDetailLoaderStateImplCopyWith<
_$ProfitSharingDetailLoaderStateImpl
>
get copyWith =>
__$$ProfitSharingDetailLoaderStateImplCopyWithImpl<
_$ProfitSharingDetailLoaderStateImpl
>(this, _$identity);
}
abstract class _ProfitSharingDetailLoaderState
extends ProfitSharingDetailLoaderState {
const factory _ProfitSharingDetailLoaderState({
required final ProfitSharingDetail detail,
required final Option<AnalyticFailure> failureOption,
final bool isFetching,
final String parentCategoryId,
final String expandedCategoryId,
required final DateTime dateFrom,
required final DateTime dateTo,
}) = _$ProfitSharingDetailLoaderStateImpl;
const _ProfitSharingDetailLoaderState._() : super._();
@override
ProfitSharingDetail get detail;
@override
Option<AnalyticFailure> get failureOption;
@override
bool get isFetching;
@override
String get parentCategoryId;
@override
String get expandedCategoryId;
@override
DateTime get dateFrom;
@override
DateTime get dateTo;
/// Create a copy of ProfitSharingDetailLoaderState
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$ProfitSharingDetailLoaderStateImplCopyWith<
_$ProfitSharingDetailLoaderStateImpl
>
get copyWith => throw _privateConstructorUsedError;
}
@@ -0,0 +1,15 @@
part of 'profit_sharing_detail_loader_bloc.dart';
@freezed
class ProfitSharingDetailLoaderEvent with _$ProfitSharingDetailLoaderEvent {
const factory ProfitSharingDetailLoaderEvent.fetched({
required String parentCategoryId,
required DateTime dateFrom,
required DateTime dateTo,
}) = _Fetched;
/// Buka/tutup daftar produk pada satu sub kategori.
const factory ProfitSharingDetailLoaderEvent.expandedCategoryChanged(
String categoryId,
) = _ExpandedCategoryChanged;
}
@@ -0,0 +1,28 @@
part of 'profit_sharing_detail_loader_bloc.dart';
@freezed
class ProfitSharingDetailLoaderState with _$ProfitSharingDetailLoaderState {
const ProfitSharingDetailLoaderState._();
const factory ProfitSharingDetailLoaderState({
required ProfitSharingDetail detail,
required Option<AnalyticFailure> failureOption,
@Default(false) bool isFetching,
@Default('') String parentCategoryId,
@Default('') String expandedCategoryId,
required DateTime dateFrom,
required DateTime dateTo,
}) = _ProfitSharingDetailLoaderState;
factory ProfitSharingDetailLoaderState.initial() {
final now = DateTime.now();
return ProfitSharingDetailLoaderState(
detail: ProfitSharingDetail.empty(),
failureOption: none(),
dateFrom: DateTime(now.year, now.month, 1),
dateTo: DateTime(now.year, now.month + 1, 0),
);
}
bool isExpanded(String categoryId) => expandedCategoryId == categoryId;
}
@@ -0,0 +1,51 @@
import 'package:dartz/dartz.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:injectable/injectable.dart';
import '../../../domain/analytic/analytic.dart';
import '../../../domain/analytic/repositories/i_analytic_repository.dart';
part 'profit_sharing_loader_event.dart';
part 'profit_sharing_loader_state.dart';
part 'profit_sharing_loader_bloc.freezed.dart';
@injectable
class ProfitSharingLoaderBloc
extends Bloc<ProfitSharingLoaderEvent, ProfitSharingLoaderState> {
final IAnalyticRepository _repository;
ProfitSharingLoaderBloc(this._repository)
: super(ProfitSharingLoaderState.initial()) {
on<ProfitSharingLoaderEvent>(_onProfitSharingLoaderEvent);
}
Future<void> _onProfitSharingLoaderEvent(
ProfitSharingLoaderEvent event,
Emitter<ProfitSharingLoaderState> emit,
) {
return event.map(
rangeDateChanged: (e) async {
emit(state.copyWith(dateFrom: e.dateFrom, dateTo: e.dateTo));
},
periodTypeChanged: (e) async {
emit(state.copyWith(periodType: e.periodType));
},
fetched: (e) async {
emit(state.copyWith(isFetching: true, failureOption: none()));
final result = await _repository.getProfitSharing(
dateFrom: state.dateFrom,
dateTo: state.dateTo,
);
final newState = result.fold(
(f) => state.copyWith(failureOption: optionOf(f)),
(profitSharing) => state.copyWith(profitSharing: profitSharing),
);
emit(newState.copyWith(isFetching: false));
},
);
}
}
@@ -0,0 +1,807 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'profit_sharing_loader_bloc.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models',
);
/// @nodoc
mixin _$ProfitSharingLoaderEvent {
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function(DateTime dateFrom, DateTime dateTo)
rangeDateChanged,
required TResult Function(ProfitSharingPeriodType periodType)
periodTypeChanged,
required TResult Function() fetched,
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function(DateTime dateFrom, DateTime dateTo)? rangeDateChanged,
TResult? Function(ProfitSharingPeriodType periodType)? periodTypeChanged,
TResult? Function()? fetched,
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function(DateTime dateFrom, DateTime dateTo)? rangeDateChanged,
TResult Function(ProfitSharingPeriodType periodType)? periodTypeChanged,
TResult Function()? fetched,
required TResult orElse(),
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult map<TResult extends Object?>({
required TResult Function(_RangeDateChanged value) rangeDateChanged,
required TResult Function(_PeriodTypeChanged value) periodTypeChanged,
required TResult Function(_Fetched value) fetched,
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult? Function(_RangeDateChanged value)? rangeDateChanged,
TResult? Function(_PeriodTypeChanged value)? periodTypeChanged,
TResult? Function(_Fetched value)? fetched,
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
TResult Function(_RangeDateChanged value)? rangeDateChanged,
TResult Function(_PeriodTypeChanged value)? periodTypeChanged,
TResult Function(_Fetched value)? fetched,
required TResult orElse(),
}) => throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $ProfitSharingLoaderEventCopyWith<$Res> {
factory $ProfitSharingLoaderEventCopyWith(
ProfitSharingLoaderEvent value,
$Res Function(ProfitSharingLoaderEvent) then,
) = _$ProfitSharingLoaderEventCopyWithImpl<$Res, ProfitSharingLoaderEvent>;
}
/// @nodoc
class _$ProfitSharingLoaderEventCopyWithImpl<
$Res,
$Val extends ProfitSharingLoaderEvent
>
implements $ProfitSharingLoaderEventCopyWith<$Res> {
_$ProfitSharingLoaderEventCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of ProfitSharingLoaderEvent
/// with the given fields replaced by the non-null parameter values.
}
/// @nodoc
abstract class _$$RangeDateChangedImplCopyWith<$Res> {
factory _$$RangeDateChangedImplCopyWith(
_$RangeDateChangedImpl value,
$Res Function(_$RangeDateChangedImpl) then,
) = __$$RangeDateChangedImplCopyWithImpl<$Res>;
@useResult
$Res call({DateTime dateFrom, DateTime dateTo});
}
/// @nodoc
class __$$RangeDateChangedImplCopyWithImpl<$Res>
extends _$ProfitSharingLoaderEventCopyWithImpl<$Res, _$RangeDateChangedImpl>
implements _$$RangeDateChangedImplCopyWith<$Res> {
__$$RangeDateChangedImplCopyWithImpl(
_$RangeDateChangedImpl _value,
$Res Function(_$RangeDateChangedImpl) _then,
) : super(_value, _then);
/// Create a copy of ProfitSharingLoaderEvent
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({Object? dateFrom = null, Object? dateTo = null}) {
return _then(
_$RangeDateChangedImpl(
null == dateFrom
? _value.dateFrom
: dateFrom // ignore: cast_nullable_to_non_nullable
as DateTime,
null == dateTo
? _value.dateTo
: dateTo // ignore: cast_nullable_to_non_nullable
as DateTime,
),
);
}
}
/// @nodoc
class _$RangeDateChangedImpl implements _RangeDateChanged {
const _$RangeDateChangedImpl(this.dateFrom, this.dateTo);
@override
final DateTime dateFrom;
@override
final DateTime dateTo;
@override
String toString() {
return 'ProfitSharingLoaderEvent.rangeDateChanged(dateFrom: $dateFrom, dateTo: $dateTo)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$RangeDateChangedImpl &&
(identical(other.dateFrom, dateFrom) ||
other.dateFrom == dateFrom) &&
(identical(other.dateTo, dateTo) || other.dateTo == dateTo));
}
@override
int get hashCode => Object.hash(runtimeType, dateFrom, dateTo);
/// Create a copy of ProfitSharingLoaderEvent
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$RangeDateChangedImplCopyWith<_$RangeDateChangedImpl> get copyWith =>
__$$RangeDateChangedImplCopyWithImpl<_$RangeDateChangedImpl>(
this,
_$identity,
);
@override
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function(DateTime dateFrom, DateTime dateTo)
rangeDateChanged,
required TResult Function(ProfitSharingPeriodType periodType)
periodTypeChanged,
required TResult Function() fetched,
}) {
return rangeDateChanged(dateFrom, dateTo);
}
@override
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function(DateTime dateFrom, DateTime dateTo)? rangeDateChanged,
TResult? Function(ProfitSharingPeriodType periodType)? periodTypeChanged,
TResult? Function()? fetched,
}) {
return rangeDateChanged?.call(dateFrom, dateTo);
}
@override
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function(DateTime dateFrom, DateTime dateTo)? rangeDateChanged,
TResult Function(ProfitSharingPeriodType periodType)? periodTypeChanged,
TResult Function()? fetched,
required TResult orElse(),
}) {
if (rangeDateChanged != null) {
return rangeDateChanged(dateFrom, dateTo);
}
return orElse();
}
@override
@optionalTypeArgs
TResult map<TResult extends Object?>({
required TResult Function(_RangeDateChanged value) rangeDateChanged,
required TResult Function(_PeriodTypeChanged value) periodTypeChanged,
required TResult Function(_Fetched value) fetched,
}) {
return rangeDateChanged(this);
}
@override
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult? Function(_RangeDateChanged value)? rangeDateChanged,
TResult? Function(_PeriodTypeChanged value)? periodTypeChanged,
TResult? Function(_Fetched value)? fetched,
}) {
return rangeDateChanged?.call(this);
}
@override
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
TResult Function(_RangeDateChanged value)? rangeDateChanged,
TResult Function(_PeriodTypeChanged value)? periodTypeChanged,
TResult Function(_Fetched value)? fetched,
required TResult orElse(),
}) {
if (rangeDateChanged != null) {
return rangeDateChanged(this);
}
return orElse();
}
}
abstract class _RangeDateChanged implements ProfitSharingLoaderEvent {
const factory _RangeDateChanged(
final DateTime dateFrom,
final DateTime dateTo,
) = _$RangeDateChangedImpl;
DateTime get dateFrom;
DateTime get dateTo;
/// Create a copy of ProfitSharingLoaderEvent
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
_$$RangeDateChangedImplCopyWith<_$RangeDateChangedImpl> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class _$$PeriodTypeChangedImplCopyWith<$Res> {
factory _$$PeriodTypeChangedImplCopyWith(
_$PeriodTypeChangedImpl value,
$Res Function(_$PeriodTypeChangedImpl) then,
) = __$$PeriodTypeChangedImplCopyWithImpl<$Res>;
@useResult
$Res call({ProfitSharingPeriodType periodType});
}
/// @nodoc
class __$$PeriodTypeChangedImplCopyWithImpl<$Res>
extends
_$ProfitSharingLoaderEventCopyWithImpl<$Res, _$PeriodTypeChangedImpl>
implements _$$PeriodTypeChangedImplCopyWith<$Res> {
__$$PeriodTypeChangedImplCopyWithImpl(
_$PeriodTypeChangedImpl _value,
$Res Function(_$PeriodTypeChangedImpl) _then,
) : super(_value, _then);
/// Create a copy of ProfitSharingLoaderEvent
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({Object? periodType = null}) {
return _then(
_$PeriodTypeChangedImpl(
null == periodType
? _value.periodType
: periodType // ignore: cast_nullable_to_non_nullable
as ProfitSharingPeriodType,
),
);
}
}
/// @nodoc
class _$PeriodTypeChangedImpl implements _PeriodTypeChanged {
const _$PeriodTypeChangedImpl(this.periodType);
@override
final ProfitSharingPeriodType periodType;
@override
String toString() {
return 'ProfitSharingLoaderEvent.periodTypeChanged(periodType: $periodType)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$PeriodTypeChangedImpl &&
(identical(other.periodType, periodType) ||
other.periodType == periodType));
}
@override
int get hashCode => Object.hash(runtimeType, periodType);
/// Create a copy of ProfitSharingLoaderEvent
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$PeriodTypeChangedImplCopyWith<_$PeriodTypeChangedImpl> get copyWith =>
__$$PeriodTypeChangedImplCopyWithImpl<_$PeriodTypeChangedImpl>(
this,
_$identity,
);
@override
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function(DateTime dateFrom, DateTime dateTo)
rangeDateChanged,
required TResult Function(ProfitSharingPeriodType periodType)
periodTypeChanged,
required TResult Function() fetched,
}) {
return periodTypeChanged(periodType);
}
@override
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function(DateTime dateFrom, DateTime dateTo)? rangeDateChanged,
TResult? Function(ProfitSharingPeriodType periodType)? periodTypeChanged,
TResult? Function()? fetched,
}) {
return periodTypeChanged?.call(periodType);
}
@override
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function(DateTime dateFrom, DateTime dateTo)? rangeDateChanged,
TResult Function(ProfitSharingPeriodType periodType)? periodTypeChanged,
TResult Function()? fetched,
required TResult orElse(),
}) {
if (periodTypeChanged != null) {
return periodTypeChanged(periodType);
}
return orElse();
}
@override
@optionalTypeArgs
TResult map<TResult extends Object?>({
required TResult Function(_RangeDateChanged value) rangeDateChanged,
required TResult Function(_PeriodTypeChanged value) periodTypeChanged,
required TResult Function(_Fetched value) fetched,
}) {
return periodTypeChanged(this);
}
@override
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult? Function(_RangeDateChanged value)? rangeDateChanged,
TResult? Function(_PeriodTypeChanged value)? periodTypeChanged,
TResult? Function(_Fetched value)? fetched,
}) {
return periodTypeChanged?.call(this);
}
@override
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
TResult Function(_RangeDateChanged value)? rangeDateChanged,
TResult Function(_PeriodTypeChanged value)? periodTypeChanged,
TResult Function(_Fetched value)? fetched,
required TResult orElse(),
}) {
if (periodTypeChanged != null) {
return periodTypeChanged(this);
}
return orElse();
}
}
abstract class _PeriodTypeChanged implements ProfitSharingLoaderEvent {
const factory _PeriodTypeChanged(final ProfitSharingPeriodType periodType) =
_$PeriodTypeChangedImpl;
ProfitSharingPeriodType get periodType;
/// Create a copy of ProfitSharingLoaderEvent
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
_$$PeriodTypeChangedImplCopyWith<_$PeriodTypeChangedImpl> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class _$$FetchedImplCopyWith<$Res> {
factory _$$FetchedImplCopyWith(
_$FetchedImpl value,
$Res Function(_$FetchedImpl) then,
) = __$$FetchedImplCopyWithImpl<$Res>;
}
/// @nodoc
class __$$FetchedImplCopyWithImpl<$Res>
extends _$ProfitSharingLoaderEventCopyWithImpl<$Res, _$FetchedImpl>
implements _$$FetchedImplCopyWith<$Res> {
__$$FetchedImplCopyWithImpl(
_$FetchedImpl _value,
$Res Function(_$FetchedImpl) _then,
) : super(_value, _then);
/// Create a copy of ProfitSharingLoaderEvent
/// with the given fields replaced by the non-null parameter values.
}
/// @nodoc
class _$FetchedImpl implements _Fetched {
const _$FetchedImpl();
@override
String toString() {
return 'ProfitSharingLoaderEvent.fetched()';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType && other is _$FetchedImpl);
}
@override
int get hashCode => runtimeType.hashCode;
@override
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function(DateTime dateFrom, DateTime dateTo)
rangeDateChanged,
required TResult Function(ProfitSharingPeriodType periodType)
periodTypeChanged,
required TResult Function() fetched,
}) {
return fetched();
}
@override
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function(DateTime dateFrom, DateTime dateTo)? rangeDateChanged,
TResult? Function(ProfitSharingPeriodType periodType)? periodTypeChanged,
TResult? Function()? fetched,
}) {
return fetched?.call();
}
@override
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function(DateTime dateFrom, DateTime dateTo)? rangeDateChanged,
TResult Function(ProfitSharingPeriodType periodType)? periodTypeChanged,
TResult Function()? fetched,
required TResult orElse(),
}) {
if (fetched != null) {
return fetched();
}
return orElse();
}
@override
@optionalTypeArgs
TResult map<TResult extends Object?>({
required TResult Function(_RangeDateChanged value) rangeDateChanged,
required TResult Function(_PeriodTypeChanged value) periodTypeChanged,
required TResult Function(_Fetched value) fetched,
}) {
return fetched(this);
}
@override
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult? Function(_RangeDateChanged value)? rangeDateChanged,
TResult? Function(_PeriodTypeChanged value)? periodTypeChanged,
TResult? Function(_Fetched value)? fetched,
}) {
return fetched?.call(this);
}
@override
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
TResult Function(_RangeDateChanged value)? rangeDateChanged,
TResult Function(_PeriodTypeChanged value)? periodTypeChanged,
TResult Function(_Fetched value)? fetched,
required TResult orElse(),
}) {
if (fetched != null) {
return fetched(this);
}
return orElse();
}
}
abstract class _Fetched implements ProfitSharingLoaderEvent {
const factory _Fetched() = _$FetchedImpl;
}
/// @nodoc
mixin _$ProfitSharingLoaderState {
ProfitSharing get profitSharing => throw _privateConstructorUsedError;
Option<AnalyticFailure> get failureOption =>
throw _privateConstructorUsedError;
bool get isFetching => throw _privateConstructorUsedError;
ProfitSharingPeriodType get periodType => throw _privateConstructorUsedError;
DateTime get dateFrom => throw _privateConstructorUsedError;
DateTime get dateTo => throw _privateConstructorUsedError;
/// Create a copy of ProfitSharingLoaderState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$ProfitSharingLoaderStateCopyWith<ProfitSharingLoaderState> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $ProfitSharingLoaderStateCopyWith<$Res> {
factory $ProfitSharingLoaderStateCopyWith(
ProfitSharingLoaderState value,
$Res Function(ProfitSharingLoaderState) then,
) = _$ProfitSharingLoaderStateCopyWithImpl<$Res, ProfitSharingLoaderState>;
@useResult
$Res call({
ProfitSharing profitSharing,
Option<AnalyticFailure> failureOption,
bool isFetching,
ProfitSharingPeriodType periodType,
DateTime dateFrom,
DateTime dateTo,
});
$ProfitSharingCopyWith<$Res> get profitSharing;
}
/// @nodoc
class _$ProfitSharingLoaderStateCopyWithImpl<
$Res,
$Val extends ProfitSharingLoaderState
>
implements $ProfitSharingLoaderStateCopyWith<$Res> {
_$ProfitSharingLoaderStateCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of ProfitSharingLoaderState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? profitSharing = null,
Object? failureOption = null,
Object? isFetching = null,
Object? periodType = null,
Object? dateFrom = null,
Object? dateTo = null,
}) {
return _then(
_value.copyWith(
profitSharing: null == profitSharing
? _value.profitSharing
: profitSharing // ignore: cast_nullable_to_non_nullable
as ProfitSharing,
failureOption: null == failureOption
? _value.failureOption
: failureOption // ignore: cast_nullable_to_non_nullable
as Option<AnalyticFailure>,
isFetching: null == isFetching
? _value.isFetching
: isFetching // ignore: cast_nullable_to_non_nullable
as bool,
periodType: null == periodType
? _value.periodType
: periodType // ignore: cast_nullable_to_non_nullable
as ProfitSharingPeriodType,
dateFrom: null == dateFrom
? _value.dateFrom
: dateFrom // ignore: cast_nullable_to_non_nullable
as DateTime,
dateTo: null == dateTo
? _value.dateTo
: dateTo // ignore: cast_nullable_to_non_nullable
as DateTime,
)
as $Val,
);
}
/// Create a copy of ProfitSharingLoaderState
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$ProfitSharingCopyWith<$Res> get profitSharing {
return $ProfitSharingCopyWith<$Res>(_value.profitSharing, (value) {
return _then(_value.copyWith(profitSharing: value) as $Val);
});
}
}
/// @nodoc
abstract class _$$ProfitSharingLoaderStateImplCopyWith<$Res>
implements $ProfitSharingLoaderStateCopyWith<$Res> {
factory _$$ProfitSharingLoaderStateImplCopyWith(
_$ProfitSharingLoaderStateImpl value,
$Res Function(_$ProfitSharingLoaderStateImpl) then,
) = __$$ProfitSharingLoaderStateImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({
ProfitSharing profitSharing,
Option<AnalyticFailure> failureOption,
bool isFetching,
ProfitSharingPeriodType periodType,
DateTime dateFrom,
DateTime dateTo,
});
@override
$ProfitSharingCopyWith<$Res> get profitSharing;
}
/// @nodoc
class __$$ProfitSharingLoaderStateImplCopyWithImpl<$Res>
extends
_$ProfitSharingLoaderStateCopyWithImpl<
$Res,
_$ProfitSharingLoaderStateImpl
>
implements _$$ProfitSharingLoaderStateImplCopyWith<$Res> {
__$$ProfitSharingLoaderStateImplCopyWithImpl(
_$ProfitSharingLoaderStateImpl _value,
$Res Function(_$ProfitSharingLoaderStateImpl) _then,
) : super(_value, _then);
/// Create a copy of ProfitSharingLoaderState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? profitSharing = null,
Object? failureOption = null,
Object? isFetching = null,
Object? periodType = null,
Object? dateFrom = null,
Object? dateTo = null,
}) {
return _then(
_$ProfitSharingLoaderStateImpl(
profitSharing: null == profitSharing
? _value.profitSharing
: profitSharing // ignore: cast_nullable_to_non_nullable
as ProfitSharing,
failureOption: null == failureOption
? _value.failureOption
: failureOption // ignore: cast_nullable_to_non_nullable
as Option<AnalyticFailure>,
isFetching: null == isFetching
? _value.isFetching
: isFetching // ignore: cast_nullable_to_non_nullable
as bool,
periodType: null == periodType
? _value.periodType
: periodType // ignore: cast_nullable_to_non_nullable
as ProfitSharingPeriodType,
dateFrom: null == dateFrom
? _value.dateFrom
: dateFrom // ignore: cast_nullable_to_non_nullable
as DateTime,
dateTo: null == dateTo
? _value.dateTo
: dateTo // ignore: cast_nullable_to_non_nullable
as DateTime,
),
);
}
}
/// @nodoc
class _$ProfitSharingLoaderStateImpl extends _ProfitSharingLoaderState {
const _$ProfitSharingLoaderStateImpl({
required this.profitSharing,
required this.failureOption,
this.isFetching = false,
this.periodType = ProfitSharingPeriodType.weekly,
required this.dateFrom,
required this.dateTo,
}) : super._();
@override
final ProfitSharing profitSharing;
@override
final Option<AnalyticFailure> failureOption;
@override
@JsonKey()
final bool isFetching;
@override
@JsonKey()
final ProfitSharingPeriodType periodType;
@override
final DateTime dateFrom;
@override
final DateTime dateTo;
@override
String toString() {
return 'ProfitSharingLoaderState(profitSharing: $profitSharing, failureOption: $failureOption, isFetching: $isFetching, periodType: $periodType, dateFrom: $dateFrom, dateTo: $dateTo)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$ProfitSharingLoaderStateImpl &&
(identical(other.profitSharing, profitSharing) ||
other.profitSharing == profitSharing) &&
(identical(other.failureOption, failureOption) ||
other.failureOption == failureOption) &&
(identical(other.isFetching, isFetching) ||
other.isFetching == isFetching) &&
(identical(other.periodType, periodType) ||
other.periodType == periodType) &&
(identical(other.dateFrom, dateFrom) ||
other.dateFrom == dateFrom) &&
(identical(other.dateTo, dateTo) || other.dateTo == dateTo));
}
@override
int get hashCode => Object.hash(
runtimeType,
profitSharing,
failureOption,
isFetching,
periodType,
dateFrom,
dateTo,
);
/// Create a copy of ProfitSharingLoaderState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$ProfitSharingLoaderStateImplCopyWith<_$ProfitSharingLoaderStateImpl>
get copyWith =>
__$$ProfitSharingLoaderStateImplCopyWithImpl<
_$ProfitSharingLoaderStateImpl
>(this, _$identity);
}
abstract class _ProfitSharingLoaderState extends ProfitSharingLoaderState {
const factory _ProfitSharingLoaderState({
required final ProfitSharing profitSharing,
required final Option<AnalyticFailure> failureOption,
final bool isFetching,
final ProfitSharingPeriodType periodType,
required final DateTime dateFrom,
required final DateTime dateTo,
}) = _$ProfitSharingLoaderStateImpl;
const _ProfitSharingLoaderState._() : super._();
@override
ProfitSharing get profitSharing;
@override
Option<AnalyticFailure> get failureOption;
@override
bool get isFetching;
@override
ProfitSharingPeriodType get periodType;
@override
DateTime get dateFrom;
@override
DateTime get dateTo;
/// Create a copy of ProfitSharingLoaderState
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$ProfitSharingLoaderStateImplCopyWith<_$ProfitSharingLoaderStateImpl>
get copyWith => throw _privateConstructorUsedError;
}
@@ -0,0 +1,13 @@
part of 'profit_sharing_loader_bloc.dart';
@freezed
class ProfitSharingLoaderEvent with _$ProfitSharingLoaderEvent {
const factory ProfitSharingLoaderEvent.rangeDateChanged(
DateTime dateFrom,
DateTime dateTo,
) = _RangeDateChanged;
const factory ProfitSharingLoaderEvent.periodTypeChanged(
ProfitSharingPeriodType periodType,
) = _PeriodTypeChanged;
const factory ProfitSharingLoaderEvent.fetched() = _Fetched;
}
@@ -0,0 +1,37 @@
part of 'profit_sharing_loader_bloc.dart';
/// Tampilan rincian bagi hasil: per minggu atau per bulan.
enum ProfitSharingPeriodType { weekly, monthly }
@freezed
class ProfitSharingLoaderState with _$ProfitSharingLoaderState {
const ProfitSharingLoaderState._();
const factory ProfitSharingLoaderState({
required ProfitSharing profitSharing,
required Option<AnalyticFailure> failureOption,
@Default(false) bool isFetching,
@Default(ProfitSharingPeriodType.weekly) ProfitSharingPeriodType periodType,
required DateTime dateFrom,
required DateTime dateTo,
}) = _ProfitSharingLoaderState;
factory ProfitSharingLoaderState.initial() {
final now = DateTime.now();
return ProfitSharingLoaderState(
profitSharing: ProfitSharing.empty(),
failureOption: none(),
dateFrom: DateTime(now.year, now.month, 1),
dateTo: DateTime(now.year, now.month + 1, 0),
);
}
/// Baris periode sesuai tab yang dipilih, periode kosong disembunyikan.
List<ProfitSharingPeriod> get periods {
final budget = profitSharing.budget;
final list = periodType == ProfitSharingPeriodType.weekly
? budget.weekly
: budget.monthly;
return list.where((e) => e.hasRevenue).toList();
}
}
@@ -13,7 +13,7 @@ class SalesLoaderState with _$SalesLoaderState {
factory SalesLoaderState.initial() => SalesLoaderState(
sales: SalesAnalytic.empty(),
failureOptionSales: none(),
dateFrom: DateTime.now().subtract(const Duration(days: 30)),
dateFrom: DateTime.now(),
dateTo: DateTime.now(),
);
}
@@ -20,6 +20,6 @@ class OrderLoaderState with _$OrderLoaderState {
failureOptionOrder: none(),
dateFrom: DateTime.now(),
dateTo: DateTime.now(),
status: 'all',
status: 'pending',
);
}
@@ -22,14 +22,29 @@ class SelectedOutletBloc
Future<void> _onSelectedOutletEvent(
SelectedOutletEvent event,
Emitter<SelectedOutletState> emit,
) {
) {
return event.map(
loaded: (e) async {
final savedId = _localDataProvider.getSelectedOutletId();
emit(state.copyWith(selectedOutletId: savedId));
final savedName = _localDataProvider.getSelectedOutletName();
if (savedId != null && savedName != null) {
emit(
state.copyWith(
selectedOutletId: savedId,
selectedOutlet: Outlet.empty().copyWith(
id: savedId,
name: savedName,
),
),
);
} else {
emit(state.copyWith(selectedOutletId: savedId));
}
},
selected: (e) async {
await _localDataProvider.saveSelectedOutletId(e.outlet.id);
await _localDataProvider.saveSelectedOutletName(e.outlet.name);
emit(
state.copyWith(
selectedOutlet: e.outlet,
@@ -3,4 +3,5 @@ class LocalStorageKey {
static const String token = 'token';
static const String user = 'user';
static const String selectedOutletId = 'selected_outlet_id';
static const String selectedOutletName = 'selected_outlet_name';
}
+3
View File
@@ -6,4 +6,7 @@ extension IntegerExt on int {
symbol: 'Rp. ',
decimalDigits: 0,
).format(this);
/// Format ribuan tanpa simbol mata uang, contoh: 1.639
String get thousandFormat => NumberFormat.decimalPattern('id').format(this);
}
+7 -1
View File
@@ -9,6 +9,11 @@ part 'app_value.dart';
class ThemeApp {
static ThemeData get theme => ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(
seedColor: AppColor.primary,
primary: AppColor.primary,
brightness: Brightness.light,
),
scaffoldBackgroundColor: AppColor.background,
fontFamily: FontFamily.quicksand,
inputDecorationTheme: InputDecorationTheme(
@@ -67,6 +72,7 @@ class ThemeApp {
),
bottomSheetTheme: BottomSheetThemeData(
backgroundColor: AppColor.white,
)
surfaceTintColor: Colors.transparent,
),
);
}
+3 -1
View File
@@ -10,10 +10,12 @@ class ApiPath {
static const String dashboardAnalytic = '/api/v1/analytics/dashboard';
static const String productAnalytic = '/api/v1/analytics/products';
static const String paymentMethodAnalytic =
'/api/v1/analytics/paymentMethods';
'/api/v1/analytics/payment-methods';
static const String purchasingAnalytic = '/api/v1/analytics/purchasing';
static const String exclusiveSummaryAnalytic =
'/api/v1/analytics/exclusive-summary/period';
static const String parentCategoryAnalytic =
'/api/v1/analytics/parent-categories';
// Inventory
static const String inventoryReportDetail =
+1
View File
@@ -13,4 +13,5 @@ part 'entities/product_analytic_entity.dart';
part 'entities/payment_method_analytic_entity.dart';
part 'entities/purchasing_analytic_entity.dart';
part 'entities/exclusive_summary_entity.dart';
part 'entities/profit_sharing_entity.dart';
part 'failures/analytic_failure.dart';
File diff suppressed because it is too large Load Diff
@@ -5,6 +5,7 @@ class CategoryAnalytic with _$CategoryAnalytic {
const factory CategoryAnalytic({
required String organizationId,
required String outletId,
required String outletName,
required String dateFrom,
required String dateTo,
required List<CategoryAnalyticItem> data,
@@ -13,6 +14,7 @@ class CategoryAnalytic with _$CategoryAnalytic {
factory CategoryAnalytic.empty() => const CategoryAnalytic(
organizationId: "",
outletId: "",
outletName: "",
dateFrom: "",
dateTo: "",
data: [],
@@ -5,6 +5,7 @@ class DashboardAnalytic with _$DashboardAnalytic {
const factory DashboardAnalytic({
required String organizationId,
required String outletId,
required String outletName,
required String dateFrom,
required String dateTo,
required DashboardOverview overview,
@@ -16,6 +17,7 @@ class DashboardAnalytic with _$DashboardAnalytic {
factory DashboardAnalytic.empty() => DashboardAnalytic(
organizationId: '',
outletId: '',
outletName: '',
dateFrom: '',
dateTo: '',
overview: DashboardOverview.empty(),
@@ -34,6 +36,9 @@ class DashboardOverview with _$DashboardOverview {
required int totalCustomers,
required int voidedOrders,
required int refundedOrders,
required int totalItemSold,
required int totalLowStock,
required int totalProductActive,
}) = _DashboardOverview;
factory DashboardOverview.empty() => const DashboardOverview(
@@ -43,6 +48,9 @@ class DashboardOverview with _$DashboardOverview {
totalCustomers: 0,
voidedOrders: 0,
refundedOrders: 0,
totalItemSold: 0,
totalLowStock: 0,
totalProductActive: 0,
);
}
@@ -5,6 +5,7 @@ class ExclusiveSummary with _$ExclusiveSummary {
const factory ExclusiveSummary({
required String organizationId,
required String outletId,
required String outletName,
required ExclusiveSummaryPeriod period,
required ExclusiveSummarySummary summary,
required ExclusiveSummaryReimburse reimburse,
@@ -17,6 +18,7 @@ class ExclusiveSummary with _$ExclusiveSummary {
factory ExclusiveSummary.empty() => ExclusiveSummary(
organizationId: '',
outletId: '',
outletName: '',
period: ExclusiveSummaryPeriod.empty(),
summary: ExclusiveSummarySummary.empty(),
reimburse: ExclusiveSummaryReimburse.empty(),
@@ -79,12 +81,11 @@ class ExclusiveSummaryReimburse with _$ExclusiveSummaryReimburse {
required int totalReimburse,
}) = _ExclusiveSummaryReimburse;
factory ExclusiveSummaryReimburse.empty() =>
const ExclusiveSummaryReimburse(
totalCost: 0,
excludedSalaryStaff: 0,
totalReimburse: 0,
);
factory ExclusiveSummaryReimburse.empty() => const ExclusiveSummaryReimburse(
totalCost: 0,
excludedSalaryStaff: 0,
totalReimburse: 0,
);
}
@freezed
@@ -5,6 +5,7 @@ class PaymentMethodAnalytic with _$PaymentMethodAnalytic {
const factory PaymentMethodAnalytic({
required String organizationId,
required String outletId,
required String outletName,
required String dateFrom,
required String dateTo,
required String groupBy,
@@ -15,6 +16,7 @@ class PaymentMethodAnalytic with _$PaymentMethodAnalytic {
factory PaymentMethodAnalytic.empty() => PaymentMethodAnalytic(
organizationId: '',
outletId: '',
outletName: '',
dateFrom: '',
dateTo: '',
groupBy: '',
@@ -5,6 +5,7 @@ class ProductAnalytic with _$ProductAnalytic {
const factory ProductAnalytic({
required String organizationId,
required String outletId,
required String outletName,
required String dateFrom,
required String dateTo,
required List<ProductAnalyticData> data,
@@ -13,6 +14,7 @@ class ProductAnalytic with _$ProductAnalytic {
factory ProductAnalytic.empty() => const ProductAnalytic(
organizationId: '',
outletId: '',
outletName: '',
dateFrom: '',
dateTo: '',
data: [],
@@ -24,22 +26,40 @@ class ProductAnalyticData with _$ProductAnalyticData {
const factory ProductAnalyticData({
required String productId,
required String productName,
required String productSku,
required int productPrice,
required String categoryId,
required String categoryName,
required int categoryOrder,
required int quantitySold,
required int revenue,
required double averagePrice,
required int orderCount,
required int standardHppPerUnit,
required int standardHppTotal,
required int fifoHppPerUnit,
required int fifoHppTotal,
required int movingAverageHppPerUnit,
required int movingAverageHppTotal,
}) = _ProductAnalyticData;
factory ProductAnalyticData.empty() => const ProductAnalyticData(
productId: '',
productName: '',
productSku: '',
productPrice: 0,
categoryId: '',
categoryName: '',
categoryOrder: 0,
quantitySold: 0,
revenue: 0,
averagePrice: 0.0,
orderCount: 0,
standardHppPerUnit: 0,
standardHppTotal: 0,
fifoHppPerUnit: 0,
fifoHppTotal: 0,
movingAverageHppPerUnit: 0,
movingAverageHppTotal: 0,
);
}
@@ -4,22 +4,34 @@ part of '../analytic.dart';
class ProfitLossAnalytic with _$ProfitLossAnalytic {
const factory ProfitLossAnalytic({
required String organizationId,
required String outletId,
required String outletName,
required String dateFrom,
required String dateTo,
required String groupBy,
required ProfitLossSummary summary,
required List<ProfitLossDailyData> data,
required List<ProfitLossProductData> productData,
required List<ProfitLossMainSummaryItem> mainSummary,
required ProfitLossPurchasing purchasing,
required List<ProfitLossOperationalExpense> operationalExpenses,
required int operationalExpensesTotal,
}) = _ProfitLossAnalytic;
factory ProfitLossAnalytic.empty() => ProfitLossAnalytic(
organizationId: '',
outletId: '',
outletName: '',
dateFrom: '',
dateTo: '',
groupBy: '',
summary: ProfitLossSummary.empty(),
data: [],
productData: [],
mainSummary: [],
purchasing: ProfitLossPurchasing.empty(),
operationalExpenses: [],
operationalExpensesTotal: 0,
);
}
@@ -115,3 +127,79 @@ class ProfitLossProductData with _$ProfitLossProductData {
profitPerUnit: 0,
);
}
@freezed
class ProfitLossMainSummaryItem with _$ProfitLossMainSummaryItem {
const factory ProfitLossMainSummaryItem({
required String id,
required String label,
required bool isBold,
required int todayNominal,
required double todayPct,
required int mtdNominal,
required double mtdPct,
@Default([]) List<ProfitLossMainSummaryItem> subItems,
}) = _ProfitLossMainSummaryItem;
factory ProfitLossMainSummaryItem.empty() => const ProfitLossMainSummaryItem(
id: '',
label: '',
isBold: false,
todayNominal: 0,
todayPct: 0,
mtdNominal: 0,
mtdPct: 0,
subItems: [],
);
}
@freezed
class ProfitLossPurchasing with _$ProfitLossPurchasing {
const factory ProfitLossPurchasing({
required int todayTotal,
required int mtdTotal,
required int todayRawMaterial,
required int mtdRawMaterial,
required int todayExpense,
required int mtdExpense,
@Default([]) List<ProfitLossPurchasingItem> items,
}) = _ProfitLossPurchasing;
factory ProfitLossPurchasing.empty() => const ProfitLossPurchasing(
todayTotal: 0,
mtdTotal: 0,
todayRawMaterial: 0,
mtdRawMaterial: 0,
todayExpense: 0,
mtdExpense: 0,
items: [],
);
}
@freezed
class ProfitLossPurchasingItem with _$ProfitLossPurchasingItem {
const factory ProfitLossPurchasingItem({
required String date,
required String item,
required int quantity,
required int nominal,
}) = _ProfitLossPurchasingItem;
factory ProfitLossPurchasingItem.empty() => const ProfitLossPurchasingItem(
date: '',
item: '',
quantity: 0,
nominal: 0,
);
}
@freezed
class ProfitLossOperationalExpense with _$ProfitLossOperationalExpense {
const factory ProfitLossOperationalExpense({
required String item,
required int nominal,
}) = _ProfitLossOperationalExpense;
factory ProfitLossOperationalExpense.empty() =>
const ProfitLossOperationalExpense(item: '', nominal: 0);
}
@@ -0,0 +1,219 @@
part of '../analytic.dart';
/// Bagi hasil (profit sharing) berdasarkan omzet per parent category.
@freezed
class ProfitSharing with _$ProfitSharing {
const factory ProfitSharing({
required String organizationId,
required String outletId,
required String outletName,
required DateTime dateFrom,
required DateTime dateTo,
required List<ProfitSharingCategory> categories,
required ProfitSharingBudget budget,
}) = _ProfitSharing;
factory ProfitSharing.empty() => ProfitSharing(
organizationId: '',
outletId: '',
outletName: '',
dateFrom: DateTime.fromMillisecondsSinceEpoch(0),
dateTo: DateTime.fromMillisecondsSinceEpoch(0),
categories: [],
budget: ProfitSharingBudget.empty(),
);
}
@freezed
class ProfitSharingCategory with _$ProfitSharingCategory {
const ProfitSharingCategory._();
const factory ProfitSharingCategory({
required String parentCategoryId,
required String parentCategoryName,
required int totalRevenue,
required int totalQuantity,
required int categoryCount,
required int productCount,
required int orderCount,
required int totalStandardHpp,
required int totalFifoHpp,
required int totalMovingAverageHpp,
}) = _ProfitSharingCategory;
factory ProfitSharingCategory.empty() => const ProfitSharingCategory(
parentCategoryId: '',
parentCategoryName: '',
totalRevenue: 0,
totalQuantity: 0,
categoryCount: 0,
productCount: 0,
orderCount: 0,
totalStandardHpp: 0,
totalFifoHpp: 0,
totalMovingAverageHpp: 0,
);
/// Laba kotor memakai HPP standar (acuan yang dipakai di laporan laba rugi).
int get grossProfit => totalRevenue - totalStandardHpp;
double get grossMargin =>
totalRevenue == 0 ? 0 : (grossProfit / totalRevenue) * 100;
/// % HPP standar terhadap omzet.
double get standardHppPercentage =>
totalRevenue == 0 ? 0 : (totalStandardHpp / totalRevenue) * 100;
/// % HPP riil (FIFO) terhadap omzet — dipakai untuk status kesehatan margin.
double get realHppPercentage =>
totalRevenue == 0 ? 0 : (totalFifoHpp / totalRevenue) * 100;
}
@freezed
class ProfitSharingBudget with _$ProfitSharingBudget {
const factory ProfitSharingBudget({
required ProfitSharingPercentage percentages,
required DateTime cutOffFrom,
required DateTime cutOffTo,
required ProfitSharingPeriod total,
required List<ProfitSharingPeriod> weekly,
required List<ProfitSharingPeriod> monthly,
}) = _ProfitSharingBudget;
factory ProfitSharingBudget.empty() => ProfitSharingBudget(
percentages: ProfitSharingPercentage.empty(),
cutOffFrom: DateTime.fromMillisecondsSinceEpoch(0),
cutOffTo: DateTime.fromMillisecondsSinceEpoch(0),
total: ProfitSharingPeriod.empty(),
weekly: [],
monthly: [],
);
}
@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);
}
/// Satu baris periode bagi hasil. Dipakai untuk total, mingguan, dan bulanan.
/// [month] dan [weekCount] hanya terisi untuk data bulanan.
@freezed
class ProfitSharingPeriod with _$ProfitSharingPeriod {
const ProfitSharingPeriod._();
const factory ProfitSharingPeriod({
required DateTime periodStart,
required DateTime periodEnd,
required int revenue,
required int orderCount,
required int limitPurchase,
required int limitOwner,
required int limitTeam,
@Default('') String month,
@Default(0) int weekCount,
}) = _ProfitSharingPeriod;
factory ProfitSharingPeriod.empty() => ProfitSharingPeriod(
periodStart: DateTime.fromMillisecondsSinceEpoch(0),
periodEnd: DateTime.fromMillisecondsSinceEpoch(0),
revenue: 0,
orderCount: 0,
limitPurchase: 0,
limitOwner: 0,
limitTeam: 0,
);
bool get hasRevenue => revenue > 0;
int get averageOrderValue => orderCount == 0 ? 0 : revenue ~/ orderCount;
}
/// Rincian satu kategori induk: ringkasan, sub kategori beserta produknya,
/// dan bagi hasil khusus kategori tersebut.
@freezed
class ProfitSharingDetail with _$ProfitSharingDetail {
const factory ProfitSharingDetail({
required String organizationId,
required String outletId,
required String outletName,
required DateTime dateFrom,
required DateTime dateTo,
required String parentCategoryId,
required String parentCategoryName,
required ProfitSharingCategory summary,
required List<ProfitSharingSubCategory> categories,
required ProfitSharingBudget budget,
}) = _ProfitSharingDetail;
factory ProfitSharingDetail.empty() => ProfitSharingDetail(
organizationId: '',
outletId: '',
outletName: '',
dateFrom: DateTime.fromMillisecondsSinceEpoch(0),
dateTo: DateTime.fromMillisecondsSinceEpoch(0),
parentCategoryId: '',
parentCategoryName: '',
summary: ProfitSharingCategory.empty(),
categories: [],
budget: ProfitSharingBudget.empty(),
);
}
@freezed
class ProfitSharingSubCategory with _$ProfitSharingSubCategory {
const ProfitSharingSubCategory._();
const factory ProfitSharingSubCategory({
required String categoryId,
required String categoryName,
required int totalRevenue,
required int totalQuantity,
required int productCount,
required int orderCount,
required int totalStandardHpp,
required int totalFifoHpp,
required int totalMovingAverageHpp,
required List<ProfitSharingProduct> products,
}) = _ProfitSharingSubCategory;
double get standardHppPercentage =>
totalRevenue == 0 ? 0 : (totalStandardHpp / totalRevenue) * 100;
double get realHppPercentage =>
totalRevenue == 0 ? 0 : (totalFifoHpp / totalRevenue) * 100;
}
@freezed
class ProfitSharingProduct with _$ProfitSharingProduct {
const ProfitSharingProduct._();
const factory ProfitSharingProduct({
required String productId,
required String productName,
required String productSku,
required int productPrice,
required int quantitySold,
required int revenue,
required double averagePrice,
required int orderCount,
required double standardHppPerUnit,
required int standardHppTotal,
required double fifoHppPerUnit,
required int fifoHppTotal,
required double movingAverageHppPerUnit,
required int movingAverageHppTotal,
}) = _ProfitSharingProduct;
double get standardHppPercentage =>
revenue == 0 ? 0 : (standardHppTotal / revenue) * 100;
double get realHppPercentage =>
revenue == 0 ? 0 : (fifoHppTotal / revenue) * 100;
}
@@ -5,6 +5,7 @@ class SalesAnalytic with _$SalesAnalytic {
const factory SalesAnalytic({
required String organizationId,
required String outletId,
required String outletName,
required DateTime dateFrom,
required DateTime dateTo,
required String groupBy,
@@ -15,6 +16,7 @@ class SalesAnalytic with _$SalesAnalytic {
factory SalesAnalytic.empty() => SalesAnalytic(
organizationId: '',
outletId: '',
outletName: '',
dateFrom: DateTime.fromMillisecondsSinceEpoch(0),
dateTo: DateTime.fromMillisecondsSinceEpoch(0),
groupBy: '',
@@ -57,4 +57,18 @@ abstract class IAnalyticRepository {
required DateTime dateTo,
String? outletId,
});
Future<Either<AnalyticFailure, ProfitSharing>> getProfitSharing({
required DateTime dateFrom,
required DateTime dateTo,
String? outletId,
String groupBy = 'day',
});
Future<Either<AnalyticFailure, ProfitSharingDetail>> getProfitSharingDetail({
required String parentCategoryId,
required DateTime dateFrom,
required DateTime dateTo,
String? outletId,
});
}
+2 -2
View File
@@ -9,12 +9,12 @@ abstract class Env {
@dev
class DevEnv implements Env {
@override
String get baseUrl => 'https://api-pos.apskel.id'; // example value
String get baseUrl => 'https://enaklo-pos-api.altru.id'; // example value
}
@Injectable(as: Env)
@prod
class ProdEnv implements Env {
@override
String get baseUrl => 'https://api-pos.apskel.id';
String get baseUrl => 'https://enaklo-pos-api.altru.id';
}
@@ -14,3 +14,4 @@ part 'dto/product_analytic_dto.dart';
part 'dto/payment_method_analytic_dto.dart';
part 'dto/purchasing_analytic_dto.dart';
part 'dto/exclusive_summary_dto.dart';
part 'dto/profit_sharing_dto.dart';
File diff suppressed because it is too large Load Diff
@@ -11,6 +11,7 @@ _$SalesAnalyticDtoImpl _$$SalesAnalyticDtoImplFromJson(
) => _$SalesAnalyticDtoImpl(
organizationId: json['organization_id'] as String?,
outletId: json['outlet_id'] as String?,
outletName: json['outlet_name'] as String?,
dateFrom: json['date_from'] == null
? null
: DateTime.parse(json['date_from'] as String),
@@ -33,6 +34,7 @@ Map<String, dynamic> _$$SalesAnalyticDtoImplToJson(
) => <String, dynamic>{
'organization_id': instance.organizationId,
'outlet_id': instance.outletId,
'outlet_name': instance.outletName,
'date_from': instance.dateFrom?.toIso8601String(),
'date_to': instance.dateTo?.toIso8601String(),
'group_by': instance.groupBy,
@@ -92,6 +94,8 @@ _$ProfitLossAnalyticDtoImpl _$$ProfitLossAnalyticDtoImplFromJson(
Map<String, dynamic> json,
) => _$ProfitLossAnalyticDtoImpl(
organizationId: json['organization_id'] as String?,
outletId: json['outlet_id'] as String?,
outletName: json['outlet_name'] as String?,
dateFrom: json['date_from'] as String?,
dateTo: json['date_to'] as String?,
groupBy: json['group_by'] as String?,
@@ -104,18 +108,42 @@ _$ProfitLossAnalyticDtoImpl _$$ProfitLossAnalyticDtoImplFromJson(
productData: (json['product_data'] as List<dynamic>?)
?.map((e) => ProfitLossProductDataDto.fromJson(e as Map<String, dynamic>))
.toList(),
mainSummary: (json['main_summary'] as List<dynamic>?)
?.map(
(e) => ProfitLossMainSummaryItemDto.fromJson(e as Map<String, dynamic>),
)
.toList(),
purchasing: json['purchasing'] == null
? null
: ProfitLossPurchasingDto.fromJson(
json['purchasing'] as Map<String, dynamic>,
),
operationalExpenses: (json['operational_expenses'] as List<dynamic>?)
?.map(
(e) =>
ProfitLossOperationalExpenseDto.fromJson(e as Map<String, dynamic>),
)
.toList(),
operationalExpensesTotal: (json['operational_expenses_total'] as num?)
?.toInt(),
);
Map<String, dynamic> _$$ProfitLossAnalyticDtoImplToJson(
_$ProfitLossAnalyticDtoImpl instance,
) => <String, dynamic>{
'organization_id': instance.organizationId,
'outlet_id': instance.outletId,
'outlet_name': instance.outletName,
'date_from': instance.dateFrom,
'date_to': instance.dateTo,
'group_by': instance.groupBy,
'summary': instance.summary,
'data': instance.data,
'product_data': instance.productData,
'main_summary': instance.mainSummary,
'purchasing': instance.purchasing,
'operational_expenses': instance.operationalExpenses,
'operational_expenses_total': instance.operationalExpensesTotal,
};
_$ProfitLossSummaryDtoImpl _$$ProfitLossSummaryDtoImplFromJson(
@@ -214,11 +242,99 @@ Map<String, dynamic> _$$ProfitLossProductDataDtoImplToJson(
'profit_per_unit': instance.profitPerUnit,
};
_$ProfitLossMainSummaryItemDtoImpl _$$ProfitLossMainSummaryItemDtoImplFromJson(
Map<String, dynamic> json,
) => _$ProfitLossMainSummaryItemDtoImpl(
id: json['id'] as String?,
label: json['label'] as String?,
isBold: json['is_bold'] as bool?,
todayNominal: (json['today_nominal'] as num?)?.toInt(),
todayPct: (json['today_pct'] as num?)?.toDouble(),
mtdNominal: (json['mtd_nominal'] as num?)?.toInt(),
mtdPct: (json['mtd_pct'] as num?)?.toDouble(),
subItems: (json['sub_items'] as List<dynamic>?)
?.map(
(e) => ProfitLossMainSummaryItemDto.fromJson(e as Map<String, dynamic>),
)
.toList(),
);
Map<String, dynamic> _$$ProfitLossMainSummaryItemDtoImplToJson(
_$ProfitLossMainSummaryItemDtoImpl instance,
) => <String, dynamic>{
'id': instance.id,
'label': instance.label,
'is_bold': instance.isBold,
'today_nominal': instance.todayNominal,
'today_pct': instance.todayPct,
'mtd_nominal': instance.mtdNominal,
'mtd_pct': instance.mtdPct,
'sub_items': instance.subItems,
};
_$ProfitLossPurchasingDtoImpl _$$ProfitLossPurchasingDtoImplFromJson(
Map<String, dynamic> json,
) => _$ProfitLossPurchasingDtoImpl(
todayTotal: (json['today_total'] as num?)?.toInt(),
mtdTotal: (json['mtd_total'] as num?)?.toInt(),
todayRawMaterial: (json['today_raw_material'] as num?)?.toInt(),
mtdRawMaterial: (json['mtd_raw_material'] as num?)?.toInt(),
todayExpense: (json['today_expense'] as num?)?.toInt(),
mtdExpense: (json['mtd_expense'] as num?)?.toInt(),
items: (json['items'] as List<dynamic>?)
?.map(
(e) => ProfitLossPurchasingItemDto.fromJson(e as Map<String, dynamic>),
)
.toList(),
);
Map<String, dynamic> _$$ProfitLossPurchasingDtoImplToJson(
_$ProfitLossPurchasingDtoImpl instance,
) => <String, dynamic>{
'today_total': instance.todayTotal,
'mtd_total': instance.mtdTotal,
'today_raw_material': instance.todayRawMaterial,
'mtd_raw_material': instance.mtdRawMaterial,
'today_expense': instance.todayExpense,
'mtd_expense': instance.mtdExpense,
'items': instance.items,
};
_$ProfitLossPurchasingItemDtoImpl _$$ProfitLossPurchasingItemDtoImplFromJson(
Map<String, dynamic> json,
) => _$ProfitLossPurchasingItemDtoImpl(
date: json['date'] as String?,
item: json['item'] as String?,
quantity: (json['quantity'] as num?)?.toInt(),
nominal: (json['nominal'] as num?)?.toInt(),
);
Map<String, dynamic> _$$ProfitLossPurchasingItemDtoImplToJson(
_$ProfitLossPurchasingItemDtoImpl instance,
) => <String, dynamic>{
'date': instance.date,
'item': instance.item,
'quantity': instance.quantity,
'nominal': instance.nominal,
};
_$ProfitLossOperationalExpenseDtoImpl
_$$ProfitLossOperationalExpenseDtoImplFromJson(Map<String, dynamic> json) =>
_$ProfitLossOperationalExpenseDtoImpl(
item: json['item'] as String?,
nominal: (json['nominal'] as num?)?.toInt(),
);
Map<String, dynamic> _$$ProfitLossOperationalExpenseDtoImplToJson(
_$ProfitLossOperationalExpenseDtoImpl instance,
) => <String, dynamic>{'item': instance.item, 'nominal': instance.nominal};
_$CategoryAnalyticDtoImpl _$$CategoryAnalyticDtoImplFromJson(
Map<String, dynamic> json,
) => _$CategoryAnalyticDtoImpl(
organizationId: json['organization_id'] as String?,
outletId: json['outlet_id'] as String?,
outletName: json['outlet_name'] as String?,
dateFrom: json['date_from'] as String?,
dateTo: json['date_to'] as String?,
data: (json['data'] as List<dynamic>?)
@@ -231,6 +347,7 @@ Map<String, dynamic> _$$CategoryAnalyticDtoImplToJson(
) => <String, dynamic>{
'organization_id': instance.organizationId,
'outlet_id': instance.outletId,
'outlet_name': instance.outletName,
'date_from': instance.dateFrom,
'date_to': instance.dateTo,
'data': instance.data,
@@ -391,6 +508,7 @@ _$DashboardAnalyticDtoImpl _$$DashboardAnalyticDtoImplFromJson(
) => _$DashboardAnalyticDtoImpl(
organizationId: json['organization_id'] as String?,
outletId: json['outlet_id'] as String?,
outletName: json['outlet_name'] as String?,
dateFrom: json['date_from'] as String?,
dateTo: json['date_to'] as String?,
overview: json['overview'] == null
@@ -414,6 +532,7 @@ Map<String, dynamic> _$$DashboardAnalyticDtoImplToJson(
) => <String, dynamic>{
'organization_id': instance.organizationId,
'outlet_id': instance.outletId,
'outlet_name': instance.outletName,
'date_from': instance.dateFrom,
'date_to': instance.dateTo,
'overview': instance.overview,
@@ -431,6 +550,9 @@ _$DashboardOverviewDtoImpl _$$DashboardOverviewDtoImplFromJson(
totalCustomers: (json['total_customers'] as num?)?.toInt(),
voidedOrders: (json['voided_orders'] as num?)?.toInt(),
refundedOrders: (json['refunded_orders'] as num?)?.toInt(),
totalItemSold: (json['total_item_sold'] as num?)?.toInt(),
totalLowStock: (json['total_low_stock'] as num?)?.toInt(),
totalProductActive: (json['total_product_active'] as num?)?.toInt(),
);
Map<String, dynamic> _$$DashboardOverviewDtoImplToJson(
@@ -442,6 +564,9 @@ Map<String, dynamic> _$$DashboardOverviewDtoImplToJson(
'total_customers': instance.totalCustomers,
'voided_orders': instance.voidedOrders,
'refunded_orders': instance.refundedOrders,
'total_item_sold': instance.totalItemSold,
'total_low_stock': instance.totalLowStock,
'total_product_active': instance.totalProductActive,
};
_$DashboardTopProductDtoImpl _$$DashboardTopProductDtoImplFromJson(
@@ -523,6 +648,7 @@ _$ProductAnalyticDtoImpl _$$ProductAnalyticDtoImplFromJson(
) => _$ProductAnalyticDtoImpl(
organizationId: json['organization_id'] as String?,
outletId: json['outlet_id'] as String?,
outletName: json['outlet_name'] as String?,
dateFrom: json['date_from'] as String?,
dateTo: json['date_to'] as String?,
data: (json['data'] as List<dynamic>?)
@@ -535,6 +661,7 @@ Map<String, dynamic> _$$ProductAnalyticDtoImplToJson(
) => <String, dynamic>{
'organization_id': instance.organizationId,
'outlet_id': instance.outletId,
'outlet_name': instance.outletName,
'date_from': instance.dateFrom,
'date_to': instance.dateTo,
'data': instance.data,
@@ -545,12 +672,22 @@ _$ProductAnalyticDataDtoImpl _$$ProductAnalyticDataDtoImplFromJson(
) => _$ProductAnalyticDataDtoImpl(
productId: json['product_id'] as String?,
productName: json['product_name'] as String?,
productSku: json['product_sku'] as String?,
productPrice: (json['product_price'] as num?)?.toInt(),
categoryId: json['category_id'] as String?,
categoryName: json['category_name'] as String?,
categoryOrder: (json['category_order'] as num?)?.toInt(),
quantitySold: (json['quantity_sold'] as num?)?.toInt(),
revenue: (json['revenue'] as num?)?.toInt(),
averagePrice: (json['average_price'] as num?)?.toDouble(),
orderCount: (json['order_count'] as num?)?.toInt(),
standardHppPerUnit: (json['standard_hpp_per_unit'] as num?)?.toInt(),
standardHppTotal: (json['standard_hpp_total'] as num?)?.toInt(),
fifoHppPerUnit: (json['fifo_hpp_per_unit'] as num?)?.toInt(),
fifoHppTotal: (json['fifo_hpp_total'] as num?)?.toInt(),
movingAverageHppPerUnit: (json['moving_average_hpp_per_unit'] as num?)
?.toInt(),
movingAverageHppTotal: (json['moving_average_hpp_total'] as num?)?.toInt(),
);
Map<String, dynamic> _$$ProductAnalyticDataDtoImplToJson(
@@ -558,12 +695,21 @@ Map<String, dynamic> _$$ProductAnalyticDataDtoImplToJson(
) => <String, dynamic>{
'product_id': instance.productId,
'product_name': instance.productName,
'product_sku': instance.productSku,
'product_price': instance.productPrice,
'category_id': instance.categoryId,
'category_name': instance.categoryName,
'category_order': instance.categoryOrder,
'quantity_sold': instance.quantitySold,
'revenue': instance.revenue,
'average_price': instance.averagePrice,
'order_count': instance.orderCount,
'standard_hpp_per_unit': instance.standardHppPerUnit,
'standard_hpp_total': instance.standardHppTotal,
'fifo_hpp_per_unit': instance.fifoHppPerUnit,
'fifo_hpp_total': instance.fifoHppTotal,
'moving_average_hpp_per_unit': instance.movingAverageHppPerUnit,
'moving_average_hpp_total': instance.movingAverageHppTotal,
};
_$PaymentMethodAnalyticDtoImpl _$$PaymentMethodAnalyticDtoImplFromJson(
@@ -571,6 +717,7 @@ _$PaymentMethodAnalyticDtoImpl _$$PaymentMethodAnalyticDtoImplFromJson(
) => _$PaymentMethodAnalyticDtoImpl(
organizationId: json['organization_id'] as String?,
outletId: json['outlet_id'] as String?,
outletName: json['outlet_name'] as String?,
dateFrom: json['date_from'] as String?,
dateTo: json['date_to'] as String?,
groupBy: json['group_by'] as String?,
@@ -589,6 +736,7 @@ Map<String, dynamic> _$$PaymentMethodAnalyticDtoImplToJson(
) => <String, dynamic>{
'organization_id': instance.organizationId,
'outlet_id': instance.outletId,
'outlet_name': instance.outletName,
'date_from': instance.dateFrom,
'date_to': instance.dateTo,
'group_by': instance.groupBy,
@@ -795,6 +943,7 @@ _$ExclusiveSummaryDtoImpl _$$ExclusiveSummaryDtoImplFromJson(
) => _$ExclusiveSummaryDtoImpl(
organizationId: json['organization_id'] as String?,
outletId: json['outlet_id'] as String?,
outletName: json['outlet_name'] as String?,
period: json['period'] == null
? null
: ExclusiveSummaryPeriodDto.fromJson(
@@ -839,6 +988,7 @@ Map<String, dynamic> _$$ExclusiveSummaryDtoImplToJson(
) => <String, dynamic>{
'organization_id': instance.organizationId,
'outlet_id': instance.outletId,
'outlet_name': instance.outletName,
'period': instance.period,
'summary': instance.summary,
'reimburse': instance.reimburse,
@@ -971,3 +1121,289 @@ Map<String, dynamic> _$$ExclusiveSummaryTransactionDtoImplToJson(
'amount': instance.amount,
'source': instance.source,
};
_$ProfitSharingDtoImpl _$$ProfitSharingDtoImplFromJson(
Map<String, dynamic> json,
) => _$ProfitSharingDtoImpl(
organizationId: json['organization_id'] as String?,
outletId: json['outlet_id'] as String?,
outletName: json['outlet_name'] as String?,
dateFrom: json['date_from'] == null
? null
: DateTime.parse(json['date_from'] as String),
dateTo: json['date_to'] == null
? null
: DateTime.parse(json['date_to'] as String),
data: (json['data'] as List<dynamic>?)
?.map((e) => ProfitSharingCategoryDto.fromJson(e as Map<String, dynamic>))
.toList(),
budget: json['budget'] == null
? null
: ProfitSharingBudgetDto.fromJson(json['budget'] as Map<String, dynamic>),
);
Map<String, dynamic> _$$ProfitSharingDtoImplToJson(
_$ProfitSharingDtoImpl instance,
) => <String, dynamic>{
'organization_id': instance.organizationId,
'outlet_id': instance.outletId,
'outlet_name': instance.outletName,
'date_from': instance.dateFrom?.toIso8601String(),
'date_to': instance.dateTo?.toIso8601String(),
'data': instance.data,
'budget': instance.budget,
};
_$ProfitSharingCategoryDtoImpl _$$ProfitSharingCategoryDtoImplFromJson(
Map<String, dynamic> json,
) => _$ProfitSharingCategoryDtoImpl(
parentCategoryId: json['parent_category_id'] as String?,
parentCategoryName: json['parent_category_name'] as String?,
totalRevenue: json['total_revenue'] as num?,
totalQuantity: json['total_quantity'] as num?,
categoryCount: json['category_count'] as num?,
productCount: json['product_count'] as num?,
orderCount: json['order_count'] as num?,
totalStandardHpp: json['total_standard_hpp'] as num?,
totalFifoHpp: json['total_fifo_hpp'] as num?,
totalMovingAverageHpp: json['total_moving_average_hpp'] as num?,
);
Map<String, dynamic> _$$ProfitSharingCategoryDtoImplToJson(
_$ProfitSharingCategoryDtoImpl instance,
) => <String, dynamic>{
'parent_category_id': instance.parentCategoryId,
'parent_category_name': instance.parentCategoryName,
'total_revenue': instance.totalRevenue,
'total_quantity': instance.totalQuantity,
'category_count': instance.categoryCount,
'product_count': instance.productCount,
'order_count': instance.orderCount,
'total_standard_hpp': instance.totalStandardHpp,
'total_fifo_hpp': instance.totalFifoHpp,
'total_moving_average_hpp': instance.totalMovingAverageHpp,
};
_$ProfitSharingBudgetDtoImpl _$$ProfitSharingBudgetDtoImplFromJson(
Map<String, dynamic> json,
) => _$ProfitSharingBudgetDtoImpl(
percentages: json['percentages'] == null
? null
: ProfitSharingPercentageDto.fromJson(
json['percentages'] as Map<String, dynamic>,
),
cutOffFrom: json['cut_off_from'] == null
? null
: DateTime.parse(json['cut_off_from'] as String),
cutOffTo: json['cut_off_to'] == null
? null
: DateTime.parse(json['cut_off_to'] as String),
total: json['total'] == null
? null
: ProfitSharingPeriodDto.fromJson(json['total'] as Map<String, dynamic>),
weekly: (json['weekly'] as List<dynamic>?)
?.map((e) => ProfitSharingPeriodDto.fromJson(e as Map<String, dynamic>))
.toList(),
monthly: (json['monthly'] as List<dynamic>?)
?.map((e) => ProfitSharingPeriodDto.fromJson(e as Map<String, dynamic>))
.toList(),
);
Map<String, dynamic> _$$ProfitSharingBudgetDtoImplToJson(
_$ProfitSharingBudgetDtoImpl instance,
) => <String, dynamic>{
'percentages': instance.percentages,
'cut_off_from': instance.cutOffFrom?.toIso8601String(),
'cut_off_to': instance.cutOffTo?.toIso8601String(),
'total': instance.total,
'weekly': instance.weekly,
'monthly': instance.monthly,
};
_$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,
};
_$ProfitSharingPeriodDtoImpl _$$ProfitSharingPeriodDtoImplFromJson(
Map<String, dynamic> json,
) => _$ProfitSharingPeriodDtoImpl(
periodStart: json['period_start'] == null
? null
: DateTime.parse(json['period_start'] as String),
periodEnd: json['period_end'] == null
? null
: DateTime.parse(json['period_end'] as String),
revenue: json['revenue'] as num?,
orderCount: json['order_count'] as num?,
limitPurchase: json['limit_purchase'] as num?,
limitOwner: json['limit_owner'] as num?,
limitTeam: json['limit_team'] as num?,
month: json['month'] as String?,
weekCount: json['week_count'] as num?,
);
Map<String, dynamic> _$$ProfitSharingPeriodDtoImplToJson(
_$ProfitSharingPeriodDtoImpl instance,
) => <String, dynamic>{
'period_start': instance.periodStart?.toIso8601String(),
'period_end': instance.periodEnd?.toIso8601String(),
'revenue': instance.revenue,
'order_count': instance.orderCount,
'limit_purchase': instance.limitPurchase,
'limit_owner': instance.limitOwner,
'limit_team': instance.limitTeam,
'month': instance.month,
'week_count': instance.weekCount,
};
_$ProfitSharingDetailDtoImpl _$$ProfitSharingDetailDtoImplFromJson(
Map<String, dynamic> json,
) => _$ProfitSharingDetailDtoImpl(
organizationId: json['organization_id'] as String?,
outletId: json['outlet_id'] as String?,
outletName: json['outlet_name'] as String?,
dateFrom: json['date_from'] == null
? null
: DateTime.parse(json['date_from'] as String),
dateTo: json['date_to'] == null
? null
: DateTime.parse(json['date_to'] as String),
parentCategoryId: json['parent_category_id'] as String?,
parentCategoryName: json['parent_category_name'] as String?,
summary: json['summary'] == null
? null
: ProfitSharingSummaryDto.fromJson(
json['summary'] as Map<String, dynamic>,
),
categories: (json['categories'] as List<dynamic>?)
?.map(
(e) => ProfitSharingSubCategoryDto.fromJson(e as Map<String, dynamic>),
)
.toList(),
budget: json['budget'] == null
? null
: ProfitSharingBudgetDto.fromJson(json['budget'] as Map<String, dynamic>),
);
Map<String, dynamic> _$$ProfitSharingDetailDtoImplToJson(
_$ProfitSharingDetailDtoImpl instance,
) => <String, dynamic>{
'organization_id': instance.organizationId,
'outlet_id': instance.outletId,
'outlet_name': instance.outletName,
'date_from': instance.dateFrom?.toIso8601String(),
'date_to': instance.dateTo?.toIso8601String(),
'parent_category_id': instance.parentCategoryId,
'parent_category_name': instance.parentCategoryName,
'summary': instance.summary,
'categories': instance.categories,
'budget': instance.budget,
};
_$ProfitSharingSummaryDtoImpl _$$ProfitSharingSummaryDtoImplFromJson(
Map<String, dynamic> json,
) => _$ProfitSharingSummaryDtoImpl(
totalRevenue: json['total_revenue'] as num?,
totalQuantity: json['total_quantity'] as num?,
categoryCount: json['category_count'] as num?,
productCount: json['product_count'] as num?,
orderCount: json['order_count'] as num?,
totalStandardHpp: json['total_standard_hpp'] as num?,
totalFifoHpp: json['total_fifo_hpp'] as num?,
totalMovingAverageHpp: json['total_moving_average_hpp'] as num?,
);
Map<String, dynamic> _$$ProfitSharingSummaryDtoImplToJson(
_$ProfitSharingSummaryDtoImpl instance,
) => <String, dynamic>{
'total_revenue': instance.totalRevenue,
'total_quantity': instance.totalQuantity,
'category_count': instance.categoryCount,
'product_count': instance.productCount,
'order_count': instance.orderCount,
'total_standard_hpp': instance.totalStandardHpp,
'total_fifo_hpp': instance.totalFifoHpp,
'total_moving_average_hpp': instance.totalMovingAverageHpp,
};
_$ProfitSharingSubCategoryDtoImpl _$$ProfitSharingSubCategoryDtoImplFromJson(
Map<String, dynamic> json,
) => _$ProfitSharingSubCategoryDtoImpl(
categoryId: json['category_id'] as String?,
categoryName: json['category_name'] as String?,
totalRevenue: json['total_revenue'] as num?,
totalQuantity: json['total_quantity'] as num?,
productCount: json['product_count'] as num?,
orderCount: json['order_count'] as num?,
totalStandardHpp: json['total_standard_hpp'] as num?,
totalFifoHpp: json['total_fifo_hpp'] as num?,
totalMovingAverageHpp: json['total_moving_average_hpp'] as num?,
products: (json['products'] as List<dynamic>?)
?.map((e) => ProfitSharingProductDto.fromJson(e as Map<String, dynamic>))
.toList(),
);
Map<String, dynamic> _$$ProfitSharingSubCategoryDtoImplToJson(
_$ProfitSharingSubCategoryDtoImpl instance,
) => <String, dynamic>{
'category_id': instance.categoryId,
'category_name': instance.categoryName,
'total_revenue': instance.totalRevenue,
'total_quantity': instance.totalQuantity,
'product_count': instance.productCount,
'order_count': instance.orderCount,
'total_standard_hpp': instance.totalStandardHpp,
'total_fifo_hpp': instance.totalFifoHpp,
'total_moving_average_hpp': instance.totalMovingAverageHpp,
'products': instance.products,
};
_$ProfitSharingProductDtoImpl _$$ProfitSharingProductDtoImplFromJson(
Map<String, dynamic> json,
) => _$ProfitSharingProductDtoImpl(
productId: json['product_id'] as String?,
productName: json['product_name'] as String?,
productSku: json['product_sku'] as String?,
productPrice: json['product_price'] as num?,
quantitySold: json['quantity_sold'] as num?,
revenue: json['revenue'] as num?,
averagePrice: json['average_price'] as num?,
orderCount: json['order_count'] as num?,
standardHppPerUnit: json['standard_hpp_per_unit'] as num?,
standardHppTotal: json['standard_hpp_total'] as num?,
fifoHppPerUnit: json['fifo_hpp_per_unit'] as num?,
fifoHppTotal: json['fifo_hpp_total'] as num?,
movingAverageHppPerUnit: json['moving_average_hpp_per_unit'] as num?,
movingAverageHppTotal: json['moving_average_hpp_total'] as num?,
);
Map<String, dynamic> _$$ProfitSharingProductDtoImplToJson(
_$ProfitSharingProductDtoImpl instance,
) => <String, dynamic>{
'product_id': instance.productId,
'product_name': instance.productName,
'product_sku': instance.productSku,
'product_price': instance.productPrice,
'quantity_sold': instance.quantitySold,
'revenue': instance.revenue,
'average_price': instance.averagePrice,
'order_count': instance.orderCount,
'standard_hpp_per_unit': instance.standardHppPerUnit,
'standard_hpp_total': instance.standardHppTotal,
'fifo_hpp_per_unit': instance.fifoHppPerUnit,
'fifo_hpp_total': instance.fifoHppTotal,
'moving_average_hpp_per_unit': instance.movingAverageHppPerUnit,
'moving_average_hpp_total': instance.movingAverageHppTotal,
};
@@ -295,4 +295,74 @@ class AnalyticRemoteDataProvider {
return DC.error(AnalyticFailure.serverError(e));
}
}
Future<DC<AnalyticFailure, ProfitSharingDto>> fetchProfitSharing({
required DateTime dateFrom,
required DateTime dateTo,
String? outletId,
String groupBy = 'day',
}) async {
try {
final Map<String, dynamic> params = {
'date_from': dateFrom.toServerDate,
'date_to': dateTo.toServerDate,
'group_by': groupBy,
};
if (outletId != null) params['outlet_id'] = outletId;
final response = await _apiClient.get(
ApiPath.parentCategoryAnalytic,
params: params,
headers: getAuthorizationHeader(),
);
if (response.data['data'] == null) {
return DC.error(AnalyticFailure.empty());
}
final dto = ProfitSharingDto.fromJson(response.data['data']);
return DC.data(dto);
} on ApiFailure catch (e, s) {
log('fetchProfitSharingError', name: _logName, error: e, stackTrace: s);
return DC.error(AnalyticFailure.serverError(e));
}
}
Future<DC<AnalyticFailure, ProfitSharingDetailDto>> fetchProfitSharingDetail({
required String parentCategoryId,
required DateTime dateFrom,
required DateTime dateTo,
String? outletId,
}) async {
try {
final Map<String, dynamic> params = {
'date_from': dateFrom.toServerDate,
'date_to': dateTo.toServerDate,
};
if (outletId != null) params['outlet_id'] = outletId;
final response = await _apiClient.get(
'${ApiPath.parentCategoryAnalytic}/$parentCategoryId',
params: params,
headers: getAuthorizationHeader(),
);
if (response.data['data'] == null) {
return DC.error(AnalyticFailure.empty());
}
final dto = ProfitSharingDetailDto.fromJson(response.data['data']);
return DC.data(dto);
} on ApiFailure catch (e, s) {
log(
'fetchProfitSharingDetailError',
name: _logName,
error: e,
stackTrace: s,
);
return DC.error(AnalyticFailure.serverError(e));
}
}
}
@@ -7,6 +7,7 @@ class CategoryAnalyticDto with _$CategoryAnalyticDto {
const factory CategoryAnalyticDto({
@JsonKey(name: 'organization_id') String? organizationId,
@JsonKey(name: 'outlet_id') String? outletId,
@JsonKey(name: 'outlet_name') String? outletName,
@JsonKey(name: 'date_from') String? dateFrom,
@JsonKey(name: 'date_to') String? dateTo,
@JsonKey(name: 'data') List<CategoryAnalyticItemDto>? data,
@@ -18,6 +19,7 @@ class CategoryAnalyticDto with _$CategoryAnalyticDto {
CategoryAnalytic toDomain() => CategoryAnalytic(
organizationId: organizationId ?? "",
outletId: outletId ?? "",
outletName: outletName ?? "",
dateFrom: dateFrom ?? "",
dateTo: dateTo ?? "",
data: data?.map((e) => e.toDomain()).toList() ?? [],
@@ -7,6 +7,7 @@ class DashboardAnalyticDto with _$DashboardAnalyticDto {
const factory DashboardAnalyticDto({
@JsonKey(name: 'organization_id') String? organizationId,
@JsonKey(name: 'outlet_id') String? outletId,
@JsonKey(name: 'outlet_name') String? outletName,
@JsonKey(name: 'date_from') String? dateFrom,
@JsonKey(name: 'date_to') String? dateTo,
@JsonKey(name: 'overview') DashboardOverviewDto? overview,
@@ -22,6 +23,7 @@ class DashboardAnalyticDto with _$DashboardAnalyticDto {
DashboardAnalytic toDomain() => DashboardAnalytic(
organizationId: organizationId ?? '',
outletId: outletId ?? '',
outletName: outletName ?? '',
dateFrom: dateFrom ?? '',
dateTo: dateTo ?? '',
overview: overview?.toDomain() ?? DashboardOverview.empty(),
@@ -42,6 +44,9 @@ class DashboardOverviewDto with _$DashboardOverviewDto {
@JsonKey(name: 'total_customers') int? totalCustomers,
@JsonKey(name: 'voided_orders') int? voidedOrders,
@JsonKey(name: 'refunded_orders') int? refundedOrders,
@JsonKey(name: 'total_item_sold') int? totalItemSold,
@JsonKey(name: 'total_low_stock') int? totalLowStock,
@JsonKey(name: 'total_product_active') int? totalProductActive,
}) = _DashboardOverviewDto;
factory DashboardOverviewDto.fromJson(Map<String, dynamic> json) =>
@@ -54,6 +59,9 @@ class DashboardOverviewDto with _$DashboardOverviewDto {
totalCustomers: totalCustomers ?? 0,
voidedOrders: voidedOrders ?? 0,
refundedOrders: refundedOrders ?? 0,
totalItemSold: totalItemSold ?? 0,
totalLowStock: totalLowStock ?? 0,
totalProductActive: totalProductActive ?? 0,
);
}
@@ -7,6 +7,7 @@ class ExclusiveSummaryDto with _$ExclusiveSummaryDto {
const factory ExclusiveSummaryDto({
@JsonKey(name: 'organization_id') String? organizationId,
@JsonKey(name: 'outlet_id') String? outletId,
@JsonKey(name: 'outlet_name') String? outletName,
@JsonKey(name: 'period') ExclusiveSummaryPeriodDto? period,
@JsonKey(name: 'summary') ExclusiveSummarySummaryDto? summary,
@JsonKey(name: 'reimburse') ExclusiveSummaryReimburseDto? reimburse,
@@ -26,6 +27,7 @@ class ExclusiveSummaryDto with _$ExclusiveSummaryDto {
ExclusiveSummary toDomain() => ExclusiveSummary(
organizationId: organizationId ?? '',
outletId: outletId ?? '',
outletName: outletName ?? '',
period: period?.toDomain() ?? ExclusiveSummaryPeriod.empty(),
summary: summary?.toDomain() ?? ExclusiveSummarySummary.empty(),
reimburse: reimburse?.toDomain() ?? ExclusiveSummaryReimburse.empty(),
@@ -7,6 +7,7 @@ class PaymentMethodAnalyticDto with _$PaymentMethodAnalyticDto {
const factory PaymentMethodAnalyticDto({
@JsonKey(name: 'organization_id') String? organizationId,
@JsonKey(name: 'outlet_id') String? outletId,
@JsonKey(name: 'outlet_name') String? outletName,
@JsonKey(name: 'date_from') String? dateFrom,
@JsonKey(name: 'date_to') String? dateTo,
@JsonKey(name: 'group_by') String? groupBy,
@@ -21,6 +22,7 @@ class PaymentMethodAnalyticDto with _$PaymentMethodAnalyticDto {
return PaymentMethodAnalytic(
organizationId: organizationId ?? '',
outletId: outletId ?? '',
outletName: outletName ?? '',
dateFrom: dateFrom ?? '',
dateTo: dateTo ?? '',
groupBy: groupBy ?? '',
@@ -7,6 +7,7 @@ class ProductAnalyticDto with _$ProductAnalyticDto {
const factory ProductAnalyticDto({
@JsonKey(name: 'organization_id') String? organizationId,
@JsonKey(name: 'outlet_id') String? outletId,
@JsonKey(name: 'outlet_name') String? outletName,
@JsonKey(name: 'date_from') String? dateFrom,
@JsonKey(name: 'date_to') String? dateTo,
@JsonKey(name: 'data') List<ProductAnalyticDataDto>? data,
@@ -18,6 +19,7 @@ class ProductAnalyticDto with _$ProductAnalyticDto {
ProductAnalytic toDomain() => ProductAnalytic(
organizationId: organizationId ?? "",
outletId: outletId ?? "",
outletName: outletName ?? "",
dateFrom: dateFrom ?? "",
dateTo: dateTo ?? "",
data: data?.map((e) => e.toDomain()).toList() ?? [],
@@ -31,12 +33,21 @@ class ProductAnalyticDataDto with _$ProductAnalyticDataDto {
const factory ProductAnalyticDataDto({
@JsonKey(name: 'product_id') String? productId,
@JsonKey(name: 'product_name') String? productName,
@JsonKey(name: 'product_sku') String? productSku,
@JsonKey(name: 'product_price') int? productPrice,
@JsonKey(name: 'category_id') String? categoryId,
@JsonKey(name: 'category_name') String? categoryName,
@JsonKey(name: 'category_order') int? categoryOrder,
@JsonKey(name: 'quantity_sold') int? quantitySold,
@JsonKey(name: 'revenue') int? revenue,
@JsonKey(name: 'average_price') double? averagePrice,
@JsonKey(name: 'order_count') int? orderCount,
@JsonKey(name: 'standard_hpp_per_unit') int? standardHppPerUnit,
@JsonKey(name: 'standard_hpp_total') int? standardHppTotal,
@JsonKey(name: 'fifo_hpp_per_unit') int? fifoHppPerUnit,
@JsonKey(name: 'fifo_hpp_total') int? fifoHppTotal,
@JsonKey(name: 'moving_average_hpp_per_unit') int? movingAverageHppPerUnit,
@JsonKey(name: 'moving_average_hpp_total') int? movingAverageHppTotal,
}) = _ProductAnalyticDataDto;
factory ProductAnalyticDataDto.fromJson(Map<String, dynamic> json) =>
@@ -45,11 +56,20 @@ class ProductAnalyticDataDto with _$ProductAnalyticDataDto {
ProductAnalyticData toDomain() => ProductAnalyticData(
productId: productId ?? "",
productName: productName ?? "",
productSku: productSku ?? "",
productPrice: productPrice ?? 0,
categoryId: categoryId ?? "",
categoryName: categoryName ?? "",
categoryOrder: categoryOrder ?? 0,
quantitySold: quantitySold ?? 0,
revenue: revenue ?? 0,
averagePrice: averagePrice ?? 0,
orderCount: orderCount ?? 0,
standardHppPerUnit: standardHppPerUnit ?? 0,
standardHppTotal: standardHppTotal ?? 0,
fifoHppPerUnit: fifoHppPerUnit ?? 0,
fifoHppTotal: fifoHppTotal ?? 0,
movingAverageHppPerUnit: movingAverageHppPerUnit ?? 0,
movingAverageHppTotal: movingAverageHppTotal ?? 0,
);
}
@@ -6,12 +6,20 @@ class ProfitLossAnalyticDto with _$ProfitLossAnalyticDto {
const factory ProfitLossAnalyticDto({
@JsonKey(name: 'organization_id') String? organizationId,
@JsonKey(name: 'outlet_id') String? outletId,
@JsonKey(name: 'outlet_name') String? outletName,
@JsonKey(name: 'date_from') String? dateFrom,
@JsonKey(name: 'date_to') String? dateTo,
@JsonKey(name: 'group_by') String? groupBy,
@JsonKey(name: 'summary') ProfitLossSummaryDto? summary,
@JsonKey(name: 'data') List<ProfitLossDailyDataDto>? data,
@JsonKey(name: 'product_data') List<ProfitLossProductDataDto>? productData,
@JsonKey(name: 'main_summary')
List<ProfitLossMainSummaryItemDto>? mainSummary,
@JsonKey(name: 'purchasing') ProfitLossPurchasingDto? purchasing,
@JsonKey(name: 'operational_expenses')
List<ProfitLossOperationalExpenseDto>? operationalExpenses,
@JsonKey(name: 'operational_expenses_total') int? operationalExpensesTotal,
}) = _ProfitLossAnalyticDto;
factory ProfitLossAnalyticDto.fromJson(Map<String, dynamic> json) =>
@@ -19,12 +27,20 @@ class ProfitLossAnalyticDto with _$ProfitLossAnalyticDto {
ProfitLossAnalytic toDomain() => ProfitLossAnalytic(
organizationId: organizationId ?? '',
outletId: outletId ?? '',
outletName: outletName ?? '',
dateFrom: dateFrom ?? '',
dateTo: dateTo ?? '',
groupBy: groupBy ?? '',
summary: summary?.toDomain() ?? ProfitLossSummary.empty(),
data: (data ?? []).map((e) => e.toDomain()).toList(),
productData: (productData ?? []).map((e) => e.toDomain()).toList(),
mainSummary: (mainSummary ?? []).map((e) => e.toDomain()).toList(),
purchasing: purchasing?.toDomain() ?? ProfitLossPurchasing.empty(),
operationalExpenses: (operationalExpenses ?? [])
.map((e) => e.toDomain())
.toList(),
operationalExpensesTotal: operationalExpensesTotal ?? 0,
);
}
@@ -135,3 +151,99 @@ class ProfitLossProductDataDto with _$ProfitLossProductDataDto {
profitPerUnit: profitPerUnit ?? 0,
);
}
@freezed
class ProfitLossMainSummaryItemDto with _$ProfitLossMainSummaryItemDto {
const ProfitLossMainSummaryItemDto._();
const factory ProfitLossMainSummaryItemDto({
@JsonKey(name: 'id') String? id,
@JsonKey(name: 'label') String? label,
@JsonKey(name: 'is_bold') bool? isBold,
@JsonKey(name: 'today_nominal') int? todayNominal,
@JsonKey(name: 'today_pct') double? todayPct,
@JsonKey(name: 'mtd_nominal') int? mtdNominal,
@JsonKey(name: 'mtd_pct') double? mtdPct,
@JsonKey(name: 'sub_items') List<ProfitLossMainSummaryItemDto>? subItems,
}) = _ProfitLossMainSummaryItemDto;
factory ProfitLossMainSummaryItemDto.fromJson(Map<String, dynamic> json) =>
_$ProfitLossMainSummaryItemDtoFromJson(json);
ProfitLossMainSummaryItem toDomain() => ProfitLossMainSummaryItem(
id: id ?? '',
label: label ?? '',
isBold: isBold ?? false,
todayNominal: todayNominal ?? 0,
todayPct: todayPct ?? 0.0,
mtdNominal: mtdNominal ?? 0,
mtdPct: mtdPct ?? 0.0,
subItems: (subItems ?? []).map((e) => e.toDomain()).toList(),
);
}
@freezed
class ProfitLossPurchasingDto with _$ProfitLossPurchasingDto {
const ProfitLossPurchasingDto._();
const factory ProfitLossPurchasingDto({
@JsonKey(name: 'today_total') int? todayTotal,
@JsonKey(name: 'mtd_total') int? mtdTotal,
@JsonKey(name: 'today_raw_material') int? todayRawMaterial,
@JsonKey(name: 'mtd_raw_material') int? mtdRawMaterial,
@JsonKey(name: 'today_expense') int? todayExpense,
@JsonKey(name: 'mtd_expense') int? mtdExpense,
@JsonKey(name: 'items') List<ProfitLossPurchasingItemDto>? items,
}) = _ProfitLossPurchasingDto;
factory ProfitLossPurchasingDto.fromJson(Map<String, dynamic> json) =>
_$ProfitLossPurchasingDtoFromJson(json);
ProfitLossPurchasing toDomain() => ProfitLossPurchasing(
todayTotal: todayTotal ?? 0,
mtdTotal: mtdTotal ?? 0,
todayRawMaterial: todayRawMaterial ?? 0,
mtdRawMaterial: mtdRawMaterial ?? 0,
todayExpense: todayExpense ?? 0,
mtdExpense: mtdExpense ?? 0,
items: (items ?? []).map((e) => e.toDomain()).toList(),
);
}
@freezed
class ProfitLossPurchasingItemDto with _$ProfitLossPurchasingItemDto {
const ProfitLossPurchasingItemDto._();
const factory ProfitLossPurchasingItemDto({
@JsonKey(name: 'date') String? date,
@JsonKey(name: 'item') String? item,
@JsonKey(name: 'quantity') int? quantity,
@JsonKey(name: 'nominal') int? nominal,
}) = _ProfitLossPurchasingItemDto;
factory ProfitLossPurchasingItemDto.fromJson(Map<String, dynamic> json) =>
_$ProfitLossPurchasingItemDtoFromJson(json);
ProfitLossPurchasingItem toDomain() => ProfitLossPurchasingItem(
date: date ?? '',
item: item ?? '',
quantity: quantity ?? 0,
nominal: nominal ?? 0,
);
}
@freezed
class ProfitLossOperationalExpenseDto with _$ProfitLossOperationalExpenseDto {
const ProfitLossOperationalExpenseDto._();
const factory ProfitLossOperationalExpenseDto({
@JsonKey(name: 'item') String? item,
@JsonKey(name: 'nominal') int? nominal,
}) = _ProfitLossOperationalExpenseDto;
factory ProfitLossOperationalExpenseDto.fromJson(Map<String, dynamic> json) =>
_$ProfitLossOperationalExpenseDtoFromJson(json);
ProfitLossOperationalExpense toDomain() =>
ProfitLossOperationalExpense(item: item ?? '', nominal: nominal ?? 0);
}
@@ -0,0 +1,294 @@
part of '../analytic_dtos.dart';
@freezed
class ProfitSharingDto with _$ProfitSharingDto {
const ProfitSharingDto._();
const factory ProfitSharingDto({
@JsonKey(name: 'organization_id') String? organizationId,
@JsonKey(name: 'outlet_id') String? outletId,
@JsonKey(name: 'outlet_name') String? outletName,
@JsonKey(name: 'date_from') DateTime? dateFrom,
@JsonKey(name: 'date_to') DateTime? dateTo,
@JsonKey(name: 'data') List<ProfitSharingCategoryDto>? data,
@JsonKey(name: 'budget') ProfitSharingBudgetDto? budget,
}) = _ProfitSharingDto;
factory ProfitSharingDto.fromJson(Map<String, dynamic> json) =>
_$ProfitSharingDtoFromJson(json);
ProfitSharing toDomain() => ProfitSharing(
organizationId: organizationId ?? '',
outletId: outletId ?? '',
outletName: outletName ?? '',
dateFrom: dateFrom?.toLocal() ?? DateTime.fromMillisecondsSinceEpoch(0),
dateTo: dateTo?.toLocal() ?? DateTime.fromMillisecondsSinceEpoch(0),
categories: data?.map((e) => e.toDomain()).toList() ?? [],
budget: budget?.toDomain() ?? ProfitSharingBudget.empty(),
);
}
@freezed
class ProfitSharingCategoryDto with _$ProfitSharingCategoryDto {
const ProfitSharingCategoryDto._();
const factory ProfitSharingCategoryDto({
@JsonKey(name: 'parent_category_id') String? parentCategoryId,
@JsonKey(name: 'parent_category_name') String? parentCategoryName,
@JsonKey(name: 'total_revenue') num? totalRevenue,
@JsonKey(name: 'total_quantity') num? totalQuantity,
@JsonKey(name: 'category_count') num? categoryCount,
@JsonKey(name: 'product_count') num? productCount,
@JsonKey(name: 'order_count') num? orderCount,
@JsonKey(name: 'total_standard_hpp') num? totalStandardHpp,
@JsonKey(name: 'total_fifo_hpp') num? totalFifoHpp,
@JsonKey(name: 'total_moving_average_hpp') num? totalMovingAverageHpp,
}) = _ProfitSharingCategoryDto;
factory ProfitSharingCategoryDto.fromJson(Map<String, dynamic> json) =>
_$ProfitSharingCategoryDtoFromJson(json);
ProfitSharingCategory toDomain() => ProfitSharingCategory(
parentCategoryId: parentCategoryId ?? '',
parentCategoryName: parentCategoryName ?? '',
totalRevenue: totalRevenue?.toInt() ?? 0,
totalQuantity: totalQuantity?.toInt() ?? 0,
categoryCount: categoryCount?.toInt() ?? 0,
productCount: productCount?.toInt() ?? 0,
orderCount: orderCount?.toInt() ?? 0,
totalStandardHpp: totalStandardHpp?.toInt() ?? 0,
totalFifoHpp: totalFifoHpp?.toInt() ?? 0,
totalMovingAverageHpp: totalMovingAverageHpp?.toInt() ?? 0,
);
}
@freezed
class ProfitSharingBudgetDto with _$ProfitSharingBudgetDto {
const ProfitSharingBudgetDto._();
const factory ProfitSharingBudgetDto({
@JsonKey(name: 'percentages') ProfitSharingPercentageDto? percentages,
@JsonKey(name: 'cut_off_from') DateTime? cutOffFrom,
@JsonKey(name: 'cut_off_to') DateTime? cutOffTo,
@JsonKey(name: 'total') ProfitSharingPeriodDto? total,
@JsonKey(name: 'weekly') List<ProfitSharingPeriodDto>? weekly,
@JsonKey(name: 'monthly') List<ProfitSharingPeriodDto>? monthly,
}) = _ProfitSharingBudgetDto;
factory ProfitSharingBudgetDto.fromJson(Map<String, dynamic> json) =>
_$ProfitSharingBudgetDtoFromJson(json);
ProfitSharingBudget toDomain() => ProfitSharingBudget(
percentages: percentages?.toDomain() ?? ProfitSharingPercentage.empty(),
cutOffFrom: cutOffFrom?.toLocal() ?? DateTime.fromMillisecondsSinceEpoch(0),
cutOffTo: cutOffTo?.toLocal() ?? DateTime.fromMillisecondsSinceEpoch(0),
total: total?.toDomain() ?? ProfitSharingPeriod.empty(),
weekly: weekly?.map((e) => e.toDomain()).toList() ?? [],
monthly: monthly?.map((e) => e.toDomain()).toList() ?? [],
);
}
@freezed
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;
factory ProfitSharingPercentageDto.fromJson(Map<String, dynamic> json) =>
_$ProfitSharingPercentageDtoFromJson(json);
ProfitSharingPercentage toDomain() => ProfitSharingPercentage(
purchase: purchase?.toDouble() ?? 0,
owner: owner?.toDouble() ?? 0,
team: team?.toDouble() ?? 0,
);
}
@freezed
class ProfitSharingPeriodDto with _$ProfitSharingPeriodDto {
const ProfitSharingPeriodDto._();
const factory ProfitSharingPeriodDto({
@JsonKey(name: 'period_start') DateTime? periodStart,
@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: 'limit_owner') num? limitOwner,
@JsonKey(name: 'limit_team') num? limitTeam,
@JsonKey(name: 'month') String? month,
@JsonKey(name: 'week_count') num? weekCount,
}) = _ProfitSharingPeriodDto;
factory ProfitSharingPeriodDto.fromJson(Map<String, dynamic> json) =>
_$ProfitSharingPeriodDtoFromJson(json);
ProfitSharingPeriod toDomain() => ProfitSharingPeriod(
periodStart:
periodStart?.toLocal() ?? DateTime.fromMillisecondsSinceEpoch(0),
periodEnd: periodEnd?.toLocal() ?? DateTime.fromMillisecondsSinceEpoch(0),
revenue: revenue?.toInt() ?? 0,
orderCount: orderCount?.toInt() ?? 0,
limitPurchase: limitPurchase?.toInt() ?? 0,
limitOwner: limitOwner?.toInt() ?? 0,
limitTeam: limitTeam?.toInt() ?? 0,
month: month ?? '',
weekCount: weekCount?.toInt() ?? 0,
);
}
@freezed
class ProfitSharingDetailDto with _$ProfitSharingDetailDto {
const ProfitSharingDetailDto._();
const factory ProfitSharingDetailDto({
@JsonKey(name: 'organization_id') String? organizationId,
@JsonKey(name: 'outlet_id') String? outletId,
@JsonKey(name: 'outlet_name') String? outletName,
@JsonKey(name: 'date_from') DateTime? dateFrom,
@JsonKey(name: 'date_to') DateTime? dateTo,
@JsonKey(name: 'parent_category_id') String? parentCategoryId,
@JsonKey(name: 'parent_category_name') String? parentCategoryName,
@JsonKey(name: 'summary') ProfitSharingSummaryDto? summary,
@JsonKey(name: 'categories') List<ProfitSharingSubCategoryDto>? categories,
@JsonKey(name: 'budget') ProfitSharingBudgetDto? budget,
}) = _ProfitSharingDetailDto;
factory ProfitSharingDetailDto.fromJson(Map<String, dynamic> json) =>
_$ProfitSharingDetailDtoFromJson(json);
ProfitSharingDetail toDomain() => ProfitSharingDetail(
organizationId: organizationId ?? '',
outletId: outletId ?? '',
outletName: outletName ?? '',
dateFrom: dateFrom?.toLocal() ?? DateTime.fromMillisecondsSinceEpoch(0),
dateTo: dateTo?.toLocal() ?? DateTime.fromMillisecondsSinceEpoch(0),
parentCategoryId: parentCategoryId ?? '',
parentCategoryName: parentCategoryName ?? '',
summary:
summary?.toDomain(
parentCategoryId: parentCategoryId ?? '',
parentCategoryName: parentCategoryName ?? '',
) ??
ProfitSharingCategory.empty(),
categories: categories?.map((e) => e.toDomain()).toList() ?? [],
budget: budget?.toDomain() ?? ProfitSharingBudget.empty(),
);
}
/// Ringkasan kategori induk pada endpoint detail — bentuknya sama dengan item
/// pada endpoint daftar, hanya saja id & nama kategori ada di level atas.
@freezed
class ProfitSharingSummaryDto with _$ProfitSharingSummaryDto {
const ProfitSharingSummaryDto._();
const factory ProfitSharingSummaryDto({
@JsonKey(name: 'total_revenue') num? totalRevenue,
@JsonKey(name: 'total_quantity') num? totalQuantity,
@JsonKey(name: 'category_count') num? categoryCount,
@JsonKey(name: 'product_count') num? productCount,
@JsonKey(name: 'order_count') num? orderCount,
@JsonKey(name: 'total_standard_hpp') num? totalStandardHpp,
@JsonKey(name: 'total_fifo_hpp') num? totalFifoHpp,
@JsonKey(name: 'total_moving_average_hpp') num? totalMovingAverageHpp,
}) = _ProfitSharingSummaryDto;
factory ProfitSharingSummaryDto.fromJson(Map<String, dynamic> json) =>
_$ProfitSharingSummaryDtoFromJson(json);
ProfitSharingCategory toDomain({
required String parentCategoryId,
required String parentCategoryName,
}) => ProfitSharingCategory(
parentCategoryId: parentCategoryId,
parentCategoryName: parentCategoryName,
totalRevenue: totalRevenue?.toInt() ?? 0,
totalQuantity: totalQuantity?.toInt() ?? 0,
categoryCount: categoryCount?.toInt() ?? 0,
productCount: productCount?.toInt() ?? 0,
orderCount: orderCount?.toInt() ?? 0,
totalStandardHpp: totalStandardHpp?.toInt() ?? 0,
totalFifoHpp: totalFifoHpp?.toInt() ?? 0,
totalMovingAverageHpp: totalMovingAverageHpp?.toInt() ?? 0,
);
}
@freezed
class ProfitSharingSubCategoryDto with _$ProfitSharingSubCategoryDto {
const ProfitSharingSubCategoryDto._();
const factory ProfitSharingSubCategoryDto({
@JsonKey(name: 'category_id') String? categoryId,
@JsonKey(name: 'category_name') String? categoryName,
@JsonKey(name: 'total_revenue') num? totalRevenue,
@JsonKey(name: 'total_quantity') num? totalQuantity,
@JsonKey(name: 'product_count') num? productCount,
@JsonKey(name: 'order_count') num? orderCount,
@JsonKey(name: 'total_standard_hpp') num? totalStandardHpp,
@JsonKey(name: 'total_fifo_hpp') num? totalFifoHpp,
@JsonKey(name: 'total_moving_average_hpp') num? totalMovingAverageHpp,
@JsonKey(name: 'products') List<ProfitSharingProductDto>? products,
}) = _ProfitSharingSubCategoryDto;
factory ProfitSharingSubCategoryDto.fromJson(Map<String, dynamic> json) =>
_$ProfitSharingSubCategoryDtoFromJson(json);
ProfitSharingSubCategory toDomain() => ProfitSharingSubCategory(
categoryId: categoryId ?? '',
categoryName: categoryName ?? '',
totalRevenue: totalRevenue?.toInt() ?? 0,
totalQuantity: totalQuantity?.toInt() ?? 0,
productCount: productCount?.toInt() ?? 0,
orderCount: orderCount?.toInt() ?? 0,
totalStandardHpp: totalStandardHpp?.toInt() ?? 0,
totalFifoHpp: totalFifoHpp?.toInt() ?? 0,
totalMovingAverageHpp: totalMovingAverageHpp?.toInt() ?? 0,
products: products?.map((e) => e.toDomain()).toList() ?? [],
);
}
@freezed
class ProfitSharingProductDto with _$ProfitSharingProductDto {
const ProfitSharingProductDto._();
const factory ProfitSharingProductDto({
@JsonKey(name: 'product_id') String? productId,
@JsonKey(name: 'product_name') String? productName,
@JsonKey(name: 'product_sku') String? productSku,
@JsonKey(name: 'product_price') num? productPrice,
@JsonKey(name: 'quantity_sold') num? quantitySold,
@JsonKey(name: 'revenue') num? revenue,
@JsonKey(name: 'average_price') num? averagePrice,
@JsonKey(name: 'order_count') num? orderCount,
@JsonKey(name: 'standard_hpp_per_unit') num? standardHppPerUnit,
@JsonKey(name: 'standard_hpp_total') num? standardHppTotal,
@JsonKey(name: 'fifo_hpp_per_unit') num? fifoHppPerUnit,
@JsonKey(name: 'fifo_hpp_total') num? fifoHppTotal,
@JsonKey(name: 'moving_average_hpp_per_unit') num? movingAverageHppPerUnit,
@JsonKey(name: 'moving_average_hpp_total') num? movingAverageHppTotal,
}) = _ProfitSharingProductDto;
factory ProfitSharingProductDto.fromJson(Map<String, dynamic> json) =>
_$ProfitSharingProductDtoFromJson(json);
ProfitSharingProduct toDomain() => ProfitSharingProduct(
productId: productId ?? '',
productName: productName ?? '',
productSku: productSku ?? '',
productPrice: productPrice?.toInt() ?? 0,
quantitySold: quantitySold?.toInt() ?? 0,
revenue: revenue?.toInt() ?? 0,
averagePrice: averagePrice?.toDouble() ?? 0,
orderCount: orderCount?.toInt() ?? 0,
standardHppPerUnit: standardHppPerUnit?.toDouble() ?? 0,
standardHppTotal: standardHppTotal?.toInt() ?? 0,
fifoHppPerUnit: fifoHppPerUnit?.toDouble() ?? 0,
fifoHppTotal: fifoHppTotal?.toInt() ?? 0,
movingAverageHppPerUnit: movingAverageHppPerUnit?.toDouble() ?? 0,
movingAverageHppTotal: movingAverageHppTotal?.toInt() ?? 0,
);
}
@@ -7,6 +7,7 @@ class SalesAnalyticDto with _$SalesAnalyticDto {
const factory SalesAnalyticDto({
@JsonKey(name: 'organization_id') String? organizationId,
@JsonKey(name: 'outlet_id') String? outletId,
@JsonKey(name: 'outlet_name') String? outletName,
@JsonKey(name: 'date_from') DateTime? dateFrom,
@JsonKey(name: 'date_to') DateTime? dateTo,
@JsonKey(name: 'group_by') String? groupBy,
@@ -20,6 +21,7 @@ class SalesAnalyticDto with _$SalesAnalyticDto {
SalesAnalytic toDomain() => SalesAnalytic(
organizationId: organizationId ?? '',
outletId: outletId ?? '',
outletName: outletName ?? '',
dateFrom: dateFrom ?? DateTime.fromMillisecondsSinceEpoch(0),
dateTo: dateTo ?? DateTime.fromMillisecondsSinceEpoch(0),
groupBy: groupBy ?? '',
@@ -220,4 +220,50 @@ class AnalyticRepository implements IAnalyticRepository {
return left(const AnalyticFailure.unexpectedError());
}
}
@override
Future<Either<AnalyticFailure, ProfitSharing>> getProfitSharing({
required DateTime dateFrom,
required DateTime dateTo,
String? outletId,
String groupBy = 'day',
}) async {
try {
final result = await _dataProvider.fetchProfitSharing(
dateFrom: dateFrom,
dateTo: dateTo,
outletId: _resolveOutletId(outletId),
groupBy: groupBy,
);
if (result.hasError) return left(result.error!);
return right(result.data!.toDomain());
} catch (e, s) {
log('getProfitSharingError', name: _logName, error: e, stackTrace: s);
return left(const AnalyticFailure.unexpectedError());
}
}
@override
Future<Either<AnalyticFailure, ProfitSharingDetail>> getProfitSharingDetail({
required String parentCategoryId,
required DateTime dateFrom,
required DateTime dateTo,
String? outletId,
}) async {
try {
final result = await _dataProvider.fetchProfitSharingDetail(
parentCategoryId: parentCategoryId,
dateFrom: dateFrom,
dateTo: dateTo,
outletId: _resolveOutletId(outletId),
);
if (result.hasError) return left(result.error!);
return right(result.data!.toDomain());
} catch (e, s) {
log('getProfitSharingDetailError', name: _logName, error: e, stackTrace: s);
return left(const AnalyticFailure.unexpectedError());
}
}
}
@@ -16,11 +16,23 @@ class OutletLocalDataProvider {
);
}
Future<void> saveSelectedOutletName(String outletName) async {
await _sharedPreferences.setString(
LocalStorageKey.selectedOutletName,
outletName,
);
}
String? getSelectedOutletId() {
return _sharedPreferences.getString(LocalStorageKey.selectedOutletId);
}
String? getSelectedOutletName() {
return _sharedPreferences.getString(LocalStorageKey.selectedOutletName);
}
Future<void> deleteSelectedOutletId() async {
await _sharedPreferences.remove(LocalStorageKey.selectedOutletId);
await _sharedPreferences.remove(LocalStorageKey.selectedOutletName);
}
}
+11
View File
@@ -23,6 +23,10 @@ import 'package:apskel_owner_flutter/application/analytic/product_analytic_loade
as _i221;
import 'package:apskel_owner_flutter/application/analytic/profit_loss_loader/profit_loss_loader_bloc.dart'
as _i11;
import 'package:apskel_owner_flutter/application/analytic/profit_sharing_detail_loader/profit_sharing_detail_loader_bloc.dart'
as _i400;
import 'package:apskel_owner_flutter/application/analytic/profit_sharing_loader/profit_sharing_loader_bloc.dart'
as _i631;
import 'package:apskel_owner_flutter/application/analytic/purchasing_analytic_loader/purchasing_analytic_loader_bloc.dart'
as _i755;
import 'package:apskel_owner_flutter/application/analytic/sales_loader/sales_loader_bloc.dart'
@@ -290,6 +294,13 @@ extension GetItInjectableX on _i174.GetIt {
gh.factory<_i11.ProfitLossLoaderBloc>(
() => _i11.ProfitLossLoaderBloc(gh<_i477.IAnalyticRepository>()),
);
gh.factory<_i631.ProfitSharingLoaderBloc>(
() => _i631.ProfitSharingLoaderBloc(gh<_i477.IAnalyticRepository>()),
);
gh.factory<_i400.ProfitSharingDetailLoaderBloc>(
() =>
_i400.ProfitSharingDetailLoaderBloc(gh<_i477.IAnalyticRepository>()),
);
gh.factory<_i945.AuthBloc>(
() => _i945.AuthBloc(gh<_i49.IAuthRepository>()),
);
+105 -1
View File
@@ -496,5 +496,109 @@
"example": "48"
}
}
}
},
"mtd_month": "MTD ({month})",
"@mtd_month": {
"placeholders": {
"month": {
"type": "String",
"example": "June"
}
}
},
"profit_loss_date": "Profit / Loss · {date}",
"@profit_loss_date": {
"placeholders": {
"date": {
"type": "String",
"example": "22 Jun 2026"
}
}
},
"profit_loss_report": "Profit & Loss Report",
"@profit_loss_report": {},
"net_profit_loss": "Net Profit/Loss",
"@net_profit_loss": {},
"cost_breakdown": "Cost Breakdown",
"@cost_breakdown": {},
"all_outlets": "All Outlets",
"@all_outlets": {},
"salary_dw": "DW Salary",
"@salary_dw": {},
"salary_staff": "Staff Salary",
"@salary_staff": {},
"salary_other": "Other Salary",
"@salary_other": {},
"other_operational_expenses": "Other Operational Expenses",
"@other_operational_expenses": {},
"daily_revenue": "Daily Revenue",
"@daily_revenue": {},
"daily_revenue_desc": "Monday – Sunday · in thousands of Rupiah",
"@daily_revenue_desc": {},
"no_data_yet": "No data yet",
"@no_data_yet": {},
"total_inventory_value": "Total Inventory Value",
"@total_inventory_value": {},
"profit_sharing": "Profit Sharing",
"@profit_sharing": {},
"profit_sharing_desc": "Revenue allocation for purchasing, owner, and team",
"@profit_sharing_desc": {},
"profit_sharing_allocation": "Profit Sharing Allocation",
"@profit_sharing_allocation": {},
"share_purchase": "Purchasing",
"@share_purchase": {},
"share_owner": "Owner",
"@share_owner": {},
"share_team": "Team",
"@share_team": {},
"weekly": "Weekly",
"@weekly": {},
"monthly": "Monthly",
"@monthly": {},
"period_breakdown": "Period Breakdown",
"@period_breakdown": {},
"revenue_by_brand": "Revenue by Brand",
"@revenue_by_brand": {},
"week_count": "{count} weeks",
"@week_count": {
"placeholders": {
"count": {
"type": "int",
"example": "5"
}
}
},
"order_count_label": "{count} orders",
"@order_count_label": {
"placeholders": {
"count": {
"type": "int",
"example": "328"
}
}
},
"gross_margin": "Gross Margin",
"@gross_margin": {},
"parent_category_summary": "Parent Category Summary",
"@parent_category_summary": {},
"sub_category": "Sub Category",
"@sub_category": {},
"qty": "Qty",
"@qty": {},
"std_hpp": "Std HPP",
"@std_hpp": {},
"real_hpp": "Real HPP",
"@real_hpp": {},
"status": "Status",
"@status": {},
"grand_total": "TOTAL",
"@grand_total": {},
"status_healthy": "Healthy",
"@status_healthy": {},
"status_watch": "Watch",
"@status_watch": {},
"status_critical": "Unhealthy",
"@status_critical": {},
"tap_row_for_detail": "Tap a card to see sub category and product details",
"@tap_row_for_detail": {}
}
+105 -1
View File
@@ -496,5 +496,109 @@
"example": "48"
}
}
}
},
"mtd_month": "MTD ({month})",
"@mtd_month": {
"placeholders": {
"month": {
"type": "String",
"example": "Juni"
}
}
},
"profit_loss_date": "Laba / Rugi · {date}",
"@profit_loss_date": {
"placeholders": {
"date": {
"type": "String",
"example": "22 Jun 2026"
}
}
},
"profit_loss_report": "Laporan Laba Rugi",
"@profit_loss_report": {},
"net_profit_loss": "Laba/Rugi Bersih",
"@net_profit_loss": {},
"cost_breakdown": "Rincian Biaya",
"@cost_breakdown": {},
"all_outlets": "Semua Outlet",
"@all_outlets": {},
"salary_dw": "Gaji DW",
"@salary_dw": {},
"salary_staff": "Gaji Staff",
"@salary_staff": {},
"salary_other": "Gaji Lainnya",
"@salary_other": {},
"other_operational_expenses": "Biaya Ops Lainnya",
"@other_operational_expenses": {},
"daily_revenue": "Omzet Harian",
"@daily_revenue": {},
"daily_revenue_desc": "Senin – Minggu · dalam ribuan Rupiah",
"@daily_revenue_desc": {},
"no_data_yet": "Belum ada data",
"@no_data_yet": {},
"total_inventory_value": "Total Nilai Inventori",
"@total_inventory_value": {},
"profit_sharing": "Bagi Hasil",
"@profit_sharing": {},
"profit_sharing_desc": "Alokasi omzet untuk belanja, owner, dan tim",
"@profit_sharing_desc": {},
"profit_sharing_allocation": "Alokasi Bagi Hasil",
"@profit_sharing_allocation": {},
"share_purchase": "Belanja",
"@share_purchase": {},
"share_owner": "Owner",
"@share_owner": {},
"share_team": "Tim",
"@share_team": {},
"weekly": "Mingguan",
"@weekly": {},
"monthly": "Bulanan",
"@monthly": {},
"period_breakdown": "Rincian Periode",
"@period_breakdown": {},
"revenue_by_brand": "Omzet per Brand",
"@revenue_by_brand": {},
"week_count": "{count} minggu",
"@week_count": {
"placeholders": {
"count": {
"type": "int",
"example": "5"
}
}
},
"order_count_label": "{count} pesanan",
"@order_count_label": {
"placeholders": {
"count": {
"type": "int",
"example": "328"
}
}
},
"gross_margin": "Margin Kotor",
"@gross_margin": {},
"parent_category_summary": "Ringkasan Kategori Induk",
"@parent_category_summary": {},
"sub_category": "Sub Kategori",
"@sub_category": {},
"qty": "Qty",
"@qty": {},
"std_hpp": "Std HPP",
"@std_hpp": {},
"real_hpp": "Real HPP",
"@real_hpp": {},
"status": "Status",
"@status": {},
"grand_total": "TOTAL",
"@grand_total": {},
"status_healthy": "Sehat",
"@status_healthy": {},
"status_watch": "Waspada",
"@status_watch": {},
"status_critical": "Tidak Sehat",
"@status_critical": {},
"tap_row_for_detail": "Ketuk kartu untuk melihat rincian sub kategori dan produk",
"@tap_row_for_detail": {}
}
+228
View File
@@ -1480,6 +1480,234 @@ abstract class AppLocalizations {
/// In en, this message translates to:
/// **'{count} portions sold'**
String portion_sold(int count);
/// No description provided for @mtd_month.
///
/// In en, this message translates to:
/// **'MTD ({month})'**
String mtd_month(String month);
/// No description provided for @profit_loss_date.
///
/// In en, this message translates to:
/// **'Profit / Loss · {date}'**
String profit_loss_date(String date);
/// No description provided for @profit_loss_report.
///
/// In en, this message translates to:
/// **'Profit & Loss Report'**
String get profit_loss_report;
/// No description provided for @net_profit_loss.
///
/// In en, this message translates to:
/// **'Net Profit/Loss'**
String get net_profit_loss;
/// No description provided for @cost_breakdown.
///
/// In en, this message translates to:
/// **'Cost Breakdown'**
String get cost_breakdown;
/// No description provided for @all_outlets.
///
/// In en, this message translates to:
/// **'All Outlets'**
String get all_outlets;
/// No description provided for @salary_dw.
///
/// In en, this message translates to:
/// **'DW Salary'**
String get salary_dw;
/// No description provided for @salary_staff.
///
/// In en, this message translates to:
/// **'Staff Salary'**
String get salary_staff;
/// No description provided for @salary_other.
///
/// In en, this message translates to:
/// **'Other Salary'**
String get salary_other;
/// No description provided for @other_operational_expenses.
///
/// In en, this message translates to:
/// **'Other Operational Expenses'**
String get other_operational_expenses;
/// No description provided for @daily_revenue.
///
/// In en, this message translates to:
/// **'Daily Revenue'**
String get daily_revenue;
/// No description provided for @daily_revenue_desc.
///
/// In en, this message translates to:
/// **'Monday – Sunday · in thousands of Rupiah'**
String get daily_revenue_desc;
/// No description provided for @no_data_yet.
///
/// In en, this message translates to:
/// **'No data yet'**
String get no_data_yet;
/// No description provided for @total_inventory_value.
///
/// In en, this message translates to:
/// **'Total Inventory Value'**
String get total_inventory_value;
/// No description provided for @profit_sharing.
///
/// In en, this message translates to:
/// **'Profit Sharing'**
String get profit_sharing;
/// No description provided for @profit_sharing_desc.
///
/// In en, this message translates to:
/// **'Revenue allocation for purchasing, owner, and team'**
String get profit_sharing_desc;
/// No description provided for @profit_sharing_allocation.
///
/// In en, this message translates to:
/// **'Profit Sharing Allocation'**
String get profit_sharing_allocation;
/// No description provided for @share_purchase.
///
/// In en, this message translates to:
/// **'Purchasing'**
String get share_purchase;
/// No description provided for @share_owner.
///
/// In en, this message translates to:
/// **'Owner'**
String get share_owner;
/// No description provided for @share_team.
///
/// In en, this message translates to:
/// **'Team'**
String get share_team;
/// No description provided for @weekly.
///
/// In en, this message translates to:
/// **'Weekly'**
String get weekly;
/// No description provided for @monthly.
///
/// In en, this message translates to:
/// **'Monthly'**
String get monthly;
/// No description provided for @period_breakdown.
///
/// In en, this message translates to:
/// **'Period Breakdown'**
String get period_breakdown;
/// No description provided for @revenue_by_brand.
///
/// In en, this message translates to:
/// **'Revenue by Brand'**
String get revenue_by_brand;
/// No description provided for @week_count.
///
/// In en, this message translates to:
/// **'{count} weeks'**
String week_count(int count);
/// No description provided for @order_count_label.
///
/// In en, this message translates to:
/// **'{count} orders'**
String order_count_label(int count);
/// No description provided for @gross_margin.
///
/// In en, this message translates to:
/// **'Gross Margin'**
String get gross_margin;
/// No description provided for @parent_category_summary.
///
/// In en, this message translates to:
/// **'Parent Category Summary'**
String get parent_category_summary;
/// No description provided for @sub_category.
///
/// In en, this message translates to:
/// **'Sub Category'**
String get sub_category;
/// No description provided for @qty.
///
/// In en, this message translates to:
/// **'Qty'**
String get qty;
/// No description provided for @std_hpp.
///
/// In en, this message translates to:
/// **'Std HPP'**
String get std_hpp;
/// No description provided for @real_hpp.
///
/// In en, this message translates to:
/// **'Real HPP'**
String get real_hpp;
/// No description provided for @status.
///
/// In en, this message translates to:
/// **'Status'**
String get status;
/// No description provided for @grand_total.
///
/// In en, this message translates to:
/// **'TOTAL'**
String get grand_total;
/// No description provided for @status_healthy.
///
/// In en, this message translates to:
/// **'Healthy'**
String get status_healthy;
/// No description provided for @status_watch.
///
/// In en, this message translates to:
/// **'Watch'**
String get status_watch;
/// No description provided for @status_critical.
///
/// In en, this message translates to:
/// **'Unhealthy'**
String get status_critical;
/// No description provided for @tap_row_for_detail.
///
/// In en, this message translates to:
/// **'Tap a card to see sub category and product details'**
String get tap_row_for_detail;
}
class _AppLocalizationsDelegate extends LocalizationsDelegate<AppLocalizations> {
+122
View File
@@ -710,4 +710,126 @@ class AppLocalizationsEn extends AppLocalizations {
String portion_sold(int count) {
return '$count portions sold';
}
@override
String mtd_month(String month) {
return 'MTD ($month)';
}
@override
String profit_loss_date(String date) {
return 'Profit / Loss · $date';
}
@override
String get profit_loss_report => 'Profit & Loss Report';
@override
String get net_profit_loss => 'Net Profit/Loss';
@override
String get cost_breakdown => 'Cost Breakdown';
@override
String get all_outlets => 'All Outlets';
@override
String get salary_dw => 'DW Salary';
@override
String get salary_staff => 'Staff Salary';
@override
String get salary_other => 'Other Salary';
@override
String get other_operational_expenses => 'Other Operational Expenses';
@override
String get daily_revenue => 'Daily Revenue';
@override
String get daily_revenue_desc => 'Monday – Sunday · in thousands of Rupiah';
@override
String get no_data_yet => 'No data yet';
@override
String get total_inventory_value => 'Total Inventory Value';
@override
String get profit_sharing => 'Profit Sharing';
@override
String get profit_sharing_desc => 'Revenue allocation for purchasing, owner, and team';
@override
String get profit_sharing_allocation => 'Profit Sharing Allocation';
@override
String get share_purchase => 'Purchasing';
@override
String get share_owner => 'Owner';
@override
String get share_team => 'Team';
@override
String get weekly => 'Weekly';
@override
String get monthly => 'Monthly';
@override
String get period_breakdown => 'Period Breakdown';
@override
String get revenue_by_brand => 'Revenue by Brand';
@override
String week_count(int count) {
return '$count weeks';
}
@override
String order_count_label(int count) {
return '$count orders';
}
@override
String get gross_margin => 'Gross Margin';
@override
String get parent_category_summary => 'Parent Category Summary';
@override
String get sub_category => 'Sub Category';
@override
String get qty => 'Qty';
@override
String get std_hpp => 'Std HPP';
@override
String get real_hpp => 'Real HPP';
@override
String get status => 'Status';
@override
String get grand_total => 'TOTAL';
@override
String get status_healthy => 'Healthy';
@override
String get status_watch => 'Watch';
@override
String get status_critical => 'Unhealthy';
@override
String get tap_row_for_detail => 'Tap a card to see sub category and product details';
}
+122
View File
@@ -710,4 +710,126 @@ class AppLocalizationsId extends AppLocalizations {
String portion_sold(int count) {
return '$count porsi terjual';
}
@override
String mtd_month(String month) {
return 'MTD ($month)';
}
@override
String profit_loss_date(String date) {
return 'Laba / Rugi · $date';
}
@override
String get profit_loss_report => 'Laporan Laba Rugi';
@override
String get net_profit_loss => 'Laba/Rugi Bersih';
@override
String get cost_breakdown => 'Rincian Biaya';
@override
String get all_outlets => 'Semua Outlet';
@override
String get salary_dw => 'Gaji DW';
@override
String get salary_staff => 'Gaji Staff';
@override
String get salary_other => 'Gaji Lainnya';
@override
String get other_operational_expenses => 'Biaya Ops Lainnya';
@override
String get daily_revenue => 'Omzet Harian';
@override
String get daily_revenue_desc => 'Senin – Minggu · dalam ribuan Rupiah';
@override
String get no_data_yet => 'Belum ada data';
@override
String get total_inventory_value => 'Total Nilai Inventori';
@override
String get profit_sharing => 'Bagi Hasil';
@override
String get profit_sharing_desc => 'Alokasi omzet untuk belanja, owner, dan tim';
@override
String get profit_sharing_allocation => 'Alokasi Bagi Hasil';
@override
String get share_purchase => 'Belanja';
@override
String get share_owner => 'Owner';
@override
String get share_team => 'Tim';
@override
String get weekly => 'Mingguan';
@override
String get monthly => 'Bulanan';
@override
String get period_breakdown => 'Rincian Periode';
@override
String get revenue_by_brand => 'Omzet per Brand';
@override
String week_count(int count) {
return '$count minggu';
}
@override
String order_count_label(int count) {
return '$count pesanan';
}
@override
String get gross_margin => 'Margin Kotor';
@override
String get parent_category_summary => 'Ringkasan Kategori Induk';
@override
String get sub_category => 'Sub Kategori';
@override
String get qty => 'Qty';
@override
String get std_hpp => 'Std HPP';
@override
String get real_hpp => 'Real HPP';
@override
String get status => 'Status';
@override
String get grand_total => 'TOTAL';
@override
String get status_healthy => 'Sehat';
@override
String get status_watch => 'Waspada';
@override
String get status_critical => 'Tidak Sehat';
@override
String get tap_row_for_detail => 'Ketuk kartu untuk melihat rincian sub kategori dan produk';
}
@@ -52,6 +52,10 @@ class $AssetsIconsGen {
class $AssetsImagesGen {
const $AssetsImagesGen();
/// File path: assets/images/ic_launcher.png
AssetGenImage get icLauncher =>
const AssetGenImage('assets/images/ic_launcher.png');
/// File path: assets/images/ic_notification.png
AssetGenImage get icNotification =>
const AssetGenImage('assets/images/ic_notification.png');
@@ -60,7 +64,7 @@ class $AssetsImagesGen {
AssetGenImage get logo => const AssetGenImage('assets/images/logo.png');
/// List of all assets
List<AssetGenImage> get values => [icNotification, logo];
List<AssetGenImage> get values => [icLauncher, icNotification, logo];
}
class Assets {
@@ -13,13 +13,14 @@ class DateRangePickerBottomSheet {
DateTime? maxDate,
String? confirmText,
String? cancelText,
Color primaryColor = Colors.blue,
Color primaryColor = const Color(0xFFD90000),
Function(DateTime? startDate, DateTime? endDate)? onChanged,
}) async {
return await showModalBottomSheet<DateRangePickerSelectionChangedArgs?>(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
barrierColor: Colors.transparent,
isDismissible: false,
enableDrag: false,
builder: (BuildContext context) => _DateRangePickerBottomSheet(
@@ -71,6 +72,8 @@ class _DateRangePickerBottomSheetState
DateRangePickerSelectionChangedArgs? _selectionChangedArgs;
late AnimationController _animationController;
late Animation<double> _slideAnimation;
final DateRangePickerController _pickerController =
DateRangePickerController();
@override
void initState() {
@@ -88,6 +91,7 @@ class _DateRangePickerBottomSheetState
@override
void dispose() {
_animationController.dispose();
_pickerController.dispose();
super.dispose();
}
@@ -135,10 +139,75 @@ class _DateRangePickerBottomSheetState
return false;
}
void _applyQuickFilter(DateTime start, DateTime end) {
final range = PickerDateRange(start, end);
_pickerController.selectedRange = range;
setState(() {
_selectionChangedArgs = DateRangePickerSelectionChangedArgs(range);
});
}
Widget _buildQuickFilters() {
final now = DateTime.now();
final todayStart = DateTime(now.year, now.month, now.day);
// This week (Monday to today)
final weekday = now.weekday; // 1=Mon, 7=Sun
final weekStart = todayStart.subtract(Duration(days: weekday - 1));
// This month (1st to today)
final monthStart = DateTime(now.year, now.month, 1);
return Wrap(
spacing: 8,
runSpacing: 8,
children: [
_buildFilterChip(
context.lang.today,
() => _applyQuickFilter(todayStart, todayStart),
),
_buildFilterChip(
'Minggu ini',
() => _applyQuickFilter(weekStart, todayStart),
),
_buildFilterChip(
'Bulan ini',
() => _applyQuickFilter(monthStart, todayStart),
),
_buildFilterChip(
'MTD',
() => _applyQuickFilter(monthStart, todayStart),
),
],
);
}
Widget _buildFilterChip(String label, VoidCallback onTap) {
return GestureDetector(
onTap: onTap,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
decoration: BoxDecoration(
color: widget.primaryColor.withOpacity(0.08),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: widget.primaryColor.withOpacity(0.3)),
),
child: Text(
label,
style: TextStyle(
color: widget.primaryColor,
fontWeight: FontWeight.w600,
fontSize: 13,
),
),
),
);
}
@override
Widget build(BuildContext context) {
final screenHeight = MediaQuery.of(context).size.height;
final bottomSheetHeight = screenHeight * 0.75;
final bottomSheetHeight = screenHeight * 0.85;
return AnimatedBuilder(
animation: _animationController,
@@ -209,7 +278,12 @@ class _DateRangePickerBottomSheetState
),
),
const SizedBox(height: 20),
const SizedBox(height: 16),
// Quick filter chips
_buildQuickFilters(),
const SizedBox(height: 16),
// Date Picker
Container(
@@ -221,8 +295,10 @@ class _DateRangePickerBottomSheetState
),
),
child: SfDateRangePicker(
controller: _pickerController,
onSelectionChanged: _onSelectionChanged,
selectionMode: DateRangePickerSelectionMode.range,
backgroundColor: Colors.white,
initialSelectedRange:
(widget.initialStartDate != null &&
widget.initialEndDate != null)
@@ -233,6 +309,7 @@ class _DateRangePickerBottomSheetState
: null,
minDate: widget.minDate,
maxDate: widget.maxDate,
selectionColor: widget.primaryColor,
startRangeSelectionColor: widget.primaryColor,
endRangeSelectionColor: widget.primaryColor,
rangeSelectionColor: widget.primaryColor
@@ -249,7 +326,7 @@ class _DateRangePickerBottomSheetState
),
monthViewSettings: DateRangePickerMonthViewSettings(
viewHeaderStyle: DateRangePickerViewHeaderStyle(
backgroundColor: Colors.grey.withOpacity(0.1),
backgroundColor: Colors.white,
textStyle: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,186 @@
import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart';
import '../../../../common/extension/extension.dart';
import '../../../../common/theme/theme.dart';
import '../../../../domain/analytic/analytic.dart';
class DailyRevenueChart extends StatelessWidget {
final List<DashboardRecentSale> salesData;
const DailyRevenueChart({super.key, required this.salesData});
@override
Widget build(BuildContext context) {
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: [
// Title
Text(
context.lang.daily_revenue,
style: AppStyle.lg.copyWith(
fontWeight: FontWeight.w700,
color: AppColor.textPrimary,
),
),
const SizedBox(height: 4),
Text(
context.lang.daily_revenue_desc,
style: AppStyle.sm.copyWith(
color: AppColor.textSecondary,
fontWeight: FontWeight.w400,
),
),
const SizedBox(height: 24),
// Bar Chart
salesData.isEmpty
? _buildEmptyState()
: SizedBox(height: 200, child: _buildBarChart()),
],
),
);
}
Widget _buildEmptyState() {
return SizedBox(
height: 200,
child: Center(
child: Builder(
builder: (context) => Text(
context.lang.no_data_yet,
style: AppStyle.md.copyWith(color: AppColor.textSecondary),
),
),
),
);
}
Widget _buildBarChart() {
final maxValue = _getMaxValue();
return BarChart(
BarChartData(
alignment: BarChartAlignment.spaceAround,
maxY: maxValue,
minY: 0,
barTouchData: BarTouchData(
enabled: true,
touchTooltipData: BarTouchTooltipData(
tooltipPadding: const EdgeInsets.all(8),
getTooltipItem: (group, groupIndex, rod, rodIndex) {
if (groupIndex < salesData.length) {
final sale = salesData[groupIndex];
return BarTooltipItem(
sale.sales.currencyFormatRp,
const TextStyle(
color: AppColor.textWhite,
fontWeight: FontWeight.bold,
fontSize: 12,
),
);
}
return null;
},
),
),
titlesData: FlTitlesData(
show: true,
topTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)),
rightTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)),
leftTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)),
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 30,
getTitlesWidget: (value, meta) {
final index = value.toInt();
if (index >= 0 && index < salesData.length) {
final date = DateTime.tryParse(salesData[index].date);
final dayName = date != null
? _getShortDayName(date.weekday)
: '';
return Padding(
padding: const EdgeInsets.only(top: 8),
child: Text(
dayName,
style: AppStyle.sm.copyWith(
color: AppColor.textSecondary,
fontWeight: FontWeight.w500,
),
),
);
}
return const SizedBox.shrink();
},
),
),
),
gridData: FlGridData(show: false),
borderData: FlBorderData(show: false),
barGroups: _buildBarGroups(maxValue),
),
);
}
List<BarChartGroupData> _buildBarGroups(double maxValue) {
return salesData.asMap().entries.map((entry) {
final index = entry.key;
final sale = entry.value;
final value = sale.sales.toDouble();
// Gradient from green to lighter green for higher values
final ratio = maxValue > 0 ? value / maxValue : 0.0;
final color = Color.lerp(
AppColor.success,
AppColor.success.withGreen(230),
ratio,
)!;
return BarChartGroupData(
x: index,
barRods: [
BarChartRodData(
toY: value,
width: 28,
borderRadius: BorderRadius.circular(8),
gradient: LinearGradient(
colors: [color, color.withOpacity(0.8)],
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
),
),
],
);
}).toList();
}
double _getMaxValue() {
if (salesData.isEmpty) return 1000000;
final maxValue = salesData
.map((e) => e.sales.toDouble())
.reduce((a, b) => a > b ? a : b);
return maxValue * 1.2;
}
String _getShortDayName(int weekday) {
const days = ['Sen', 'Sel', 'Rab', 'Kam', 'Jum', 'Sab', 'Min'];
return days[weekday - 1];
}
}
+50 -241
View File
@@ -1,21 +1,13 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:line_icons/line_icons.dart';
import '../../../application/analytic/category_analytic_loader/category_analytic_loader_bloc.dart';
import '../../../application/analytic/profit_loss_loader/profit_loss_loader_bloc.dart';
import '../../../common/extension/extension.dart';
import '../../../common/theme/theme.dart';
import '../../../domain/analytic/analytic.dart';
import '../../../injection.dart';
import '../../components/appbar/appbar.dart';
import '../../components/field/date_range_picker_field.dart';
import 'widgets/cash_flow.dart';
import 'widgets/category.dart';
import 'widgets/product.dart';
import 'widgets/profit_loss.dart';
import 'widgets/summary_card.dart';
import 'widgets/cost_breakdown.dart';
import 'widgets/profit_loss_header.dart';
import 'widgets/profit_loss_report.dart';
@RoutePage()
class FinancePage extends StatefulWidget implements AutoRouteWrapper {
@@ -25,80 +17,40 @@ class FinancePage extends StatefulWidget implements AutoRouteWrapper {
State<FinancePage> createState() => _FinancePageState();
@override
Widget wrappedRoute(BuildContext context) => MultiBlocProvider(
providers: [
BlocProvider(
create: (_) =>
getIt<ProfitLossLoaderBloc>()..add(ProfitLossLoaderEvent.fetched()),
),
BlocProvider(
create: (context) =>
getIt<CategoryAnalyticLoaderBloc>()
..add(CategoryAnalyticLoaderEvent.fetched()),
),
],
Widget wrappedRoute(BuildContext context) => BlocProvider(
create: (_) =>
getIt<ProfitLossLoaderBloc>()..add(ProfitLossLoaderEvent.fetched()),
child: this,
);
}
class _FinancePageState extends State<FinancePage>
with TickerProviderStateMixin {
late AnimationController _slideController;
with SingleTickerProviderStateMixin {
late AnimationController _fadeController;
late AnimationController _scaleController;
late Animation<Offset> _slideAnimation;
late Animation<double> _fadeAnimation;
late Animation<double> _scaleAnimation;
int _selectedTabIndex = 0;
@override
void initState() {
super.initState();
_slideController = AnimationController(
duration: const Duration(milliseconds: 800),
vsync: this,
);
_fadeController = AnimationController(
duration: const Duration(milliseconds: 1000),
vsync: this,
);
_scaleController = AnimationController(
duration: const Duration(milliseconds: 600),
vsync: this,
);
_slideAnimation =
Tween<Offset>(begin: const Offset(0, 0.3), end: Offset.zero).animate(
CurvedAnimation(parent: _slideController, curve: Curves.easeOutCubic),
);
_fadeAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(parent: _fadeController, curve: Curves.easeIn));
_scaleAnimation = Tween<double>(begin: 0.8, end: 1.0).animate(
CurvedAnimation(parent: _scaleController, curve: Curves.elasticOut),
);
// Start animations
_fadeController.forward();
Future.delayed(const Duration(milliseconds: 200), () {
_slideController.forward();
});
Future.delayed(const Duration(milliseconds: 400), () {
_scaleController.forward();
});
}
@override
void dispose() {
_slideController.dispose();
_fadeController.dispose();
_scaleController.dispose();
super.dispose();
}
@@ -119,89 +71,48 @@ class _FinancePageState extends State<FinancePage>
builder: (context, state) {
return CustomScrollView(
slivers: [
// SliverAppBar with animated background
SliverAppBar(
expandedHeight: 120,
floating: false,
pinned: true,
backgroundColor: AppColor.primary,
elevation: 0,
flexibleSpace: CustomAppBar(title: context.lang.profit_loss),
),
// Header dengan filter periode
// Header with gradient background, tabs, and summary
SliverToBoxAdapter(
child: FadeTransition(
opacity: _fadeAnimation,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: DateRangePickerField(
maxDate: DateTime.now(),
startDate: state.dateFrom,
endDate: state.dateTo,
onChanged: (startDate, endDate) {
context.read<ProfitLossLoaderBloc>().add(
ProfitLossLoaderEvent.rangeDateChanged(
startDate!,
endDate!,
),
);
},
),
child: ProfitLossHeader(
state: state,
selectedTabIndex: _selectedTabIndex,
onTabChanged: (index) {
setState(() {
_selectedTabIndex = index;
});
_onTabChanged(context, index);
},
),
),
),
// Summary Cards
SliverToBoxAdapter(
child: SlideTransition(
position: _slideAnimation,
child: _buildSummaryCards(state.profitLoss.summary),
),
),
// Cash Flow Analysis
SliverToBoxAdapter(
child: ScaleTransition(
scale: _scaleAnimation,
child: FinanceCashFlow(dailyData: state.profitLoss.data),
),
),
// Profit Loss Detail
// Profit Loss Report Table
SliverToBoxAdapter(
child: FadeTransition(
opacity: _fadeAnimation,
child: FinanceProfitLoss(data: state.profitLoss.summary),
child: ProfitLossReport(
mainSummary: state.profitLoss.mainSummary,
summary: state.profitLoss.summary,
selectedTabIndex: _selectedTabIndex,
),
),
),
BlocBuilder<
CategoryAnalyticLoaderBloc,
CategoryAnalyticLoaderState
>(
builder: (context, stateCategory) {
return SliverToBoxAdapter(
child: SlideTransition(
position: _slideAnimation,
child: FinanceCategory(
categories: stateCategory.categoryAnalytic.data,
),
),
);
},
),
// Product Analysis Section
// Cost Breakdown
SliverToBoxAdapter(
child: SlideTransition(
position: _slideAnimation,
child: _buildProductAnalysis(state.profitLoss.productData),
child: FadeTransition(
opacity: _fadeAnimation,
child: CostBreakdown(
purchasing: state.profitLoss.purchasing,
selectedTabIndex: _selectedTabIndex,
dateFrom: state.dateFrom,
dateTo: state.dateTo,
),
),
),
// Transaction Categories
// Bottom spacing
const SliverToBoxAdapter(child: SizedBox(height: 100)),
],
@@ -212,125 +123,23 @@ class _FinancePageState extends State<FinancePage>
);
}
Widget _buildSummaryCards(ProfitLossSummary summary) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
children: [
Row(
children: [
Expanded(
child: FinanceSummaryCard(
title: context.lang.total_revenue,
amount: summary.totalRevenue.currencyFormatRp,
icon: LineIcons.arrowUp,
color: AppColor.success,
isPositive: true,
),
),
const SizedBox(width: 12),
Expanded(
child: FinanceSummaryCard(
title: context.lang.total_expenditures,
amount: summary.totalCost.currencyFormatRp,
icon: LineIcons.arrowDown,
color: AppColor.error,
isPositive: false,
),
),
],
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: FinanceSummaryCard(
title: context.lang.net_profit,
amount: summary.netProfit.currencyFormatRp,
icon: LineIcons.lineChart,
color: AppColor.info,
isPositive: true,
),
),
const SizedBox(width: 12),
Expanded(
child: FinanceSummaryCard(
title: context.lang.margin_profit,
amount: '${summary.profitabilityRatio.round()}%',
icon: LineIcons.percent,
color: AppColor.warning,
isPositive: true,
),
),
],
),
],
),
);
}
void _onTabChanged(BuildContext context, int index) {
final now = DateTime.now();
DateTime dateFrom;
DateTime dateTo;
Widget _buildProductAnalysis(List<ProfitLossProductData> products) {
return Container(
margin: const EdgeInsets.all(16),
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: AppColor.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: AppColor.textLight.withOpacity(0.1),
spreadRadius: 1,
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: AppColor.info.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: const Icon(
LineIcons.shoppingBag,
color: AppColor.info,
size: 20,
),
),
const SizedBox(width: 12),
Text(
context.lang.product_analytic,
style: AppStyle.lg.copyWith(fontWeight: FontWeight.bold),
),
const Spacer(),
TextButton(
onPressed: () {},
child: Text(
context.lang.view_all,
style: AppStyle.sm.copyWith(color: AppColor.primary),
),
),
],
),
// Product list
ListView.separated(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
padding: const EdgeInsets.only(top: 12),
itemCount: products.length,
separatorBuilder: (context, index) => const SizedBox(height: 12),
itemBuilder: (context, index) {
final product = products[index];
return ProfitLossProduct(product: product);
},
),
],
),
if (index == 0) {
// Today
dateFrom = DateTime(now.year, now.month, now.day);
dateTo = DateTime(now.year, now.month, now.day, 23, 59, 59);
} else {
// MTD (Month-to-Date)
dateFrom = DateTime(now.year, now.month, 1);
dateTo = now;
}
context.read<ProfitLossLoaderBloc>().add(
ProfitLossLoaderEvent.rangeDateChanged(dateFrom, dateTo),
);
}
}
@@ -1,476 +0,0 @@
import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart';
import 'package:line_icons/line_icons.dart';
import 'package:intl/intl.dart';
import '../../../../common/extension/extension.dart';
import '../../../../common/theme/theme.dart';
import '../../../../domain/analytic/analytic.dart';
class FinanceCashFlow extends StatelessWidget {
final List<ProfitLossDailyData> dailyData;
const FinanceCashFlow({super.key, required this.dailyData});
@override
Widget build(BuildContext context) {
// Calculate totals from daily data
final totalCashIn = _calculateTotalCashIn();
final totalCashOut = _calculateTotalCashOut();
final netFlow = totalCashIn - totalCashOut;
return Container(
margin: const EdgeInsets.all(16),
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: AppColor.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: AppColor.textLight.withOpacity(0.1),
spreadRadius: 1,
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: AppColor.primaryGradient,
),
borderRadius: BorderRadius.circular(8),
),
child: const Icon(
LineIcons.areaChart,
color: AppColor.white,
size: 20,
),
),
const SizedBox(width: 12),
Text(
context.lang.cash_flow_analysis,
style: AppStyle.lg.copyWith(fontWeight: FontWeight.bold),
),
],
),
IconButton(
onPressed: () {},
icon: const Icon(
LineIcons.alternateExternalLink,
color: AppColor.primary,
),
),
],
),
const SizedBox(height: 20),
// Cash Flow Indicators
Row(
children: [
Expanded(
child: _buildCashFlowIndicator(
context.lang.cash_in,
_formatCurrency(totalCashIn),
LineIcons.arrowUp,
AppColor.success,
),
),
const SizedBox(width: 16),
Expanded(
child: _buildCashFlowIndicator(
context.lang.cash_out,
_formatCurrency(totalCashOut),
LineIcons.arrowDown,
AppColor.error,
),
),
const SizedBox(width: 16),
Expanded(
child: _buildCashFlowIndicator(
context.lang.net_flow,
_formatCurrency(netFlow),
LineIcons.equals,
AppColor.info,
),
),
],
),
const SizedBox(height: 20),
// FL Chart Implementation
Container(
height: 200,
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: [
Text(
context.lang.cash_flow_chart(dailyData.length),
style: AppStyle.sm.copyWith(
color: AppColor.textSecondary,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 16),
Expanded(
child: dailyData.isEmpty
? _buildEmptyChart()
: LineChart(_buildLineChartData()),
),
const SizedBox(height: 12),
// Legend
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_buildChartLegend(context.lang.cash_in, AppColor.success),
const SizedBox(width: 20),
_buildChartLegend(context.lang.cash_out, AppColor.error),
const SizedBox(width: 20),
_buildChartLegend(context.lang.net_flow, AppColor.info),
],
),
],
),
),
],
),
);
}
LineChartData _buildLineChartData() {
final maxValue = _getMaxChartValue();
final minValue = _getMinChartValue();
return LineChartData(
gridData: FlGridData(
show: true,
drawVerticalLine: false,
horizontalInterval: (maxValue / 5).roundToDouble(),
getDrawingHorizontalLine: (value) {
return FlLine(color: AppColor.borderLight, strokeWidth: 1);
},
),
titlesData: FlTitlesData(
show: true,
rightTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)),
topTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)),
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 30,
interval: 1,
getTitlesWidget: (double value, TitleMeta meta) {
final index = value.toInt();
if (index >= 0 && index < dailyData.length) {
final date = DateTime.parse(dailyData[index].date);
final dayName = _getDayName(date.weekday);
return SideTitleWidget(
meta: meta,
child: Text(
dayName,
style: const TextStyle(
color: AppColor.textSecondary,
fontWeight: FontWeight.w500,
fontSize: 10,
),
),
);
}
return SideTitleWidget(meta: meta, child: Text(''));
},
),
),
leftTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
interval: (maxValue / 3).roundToDouble(),
reservedSize: 42,
getTitlesWidget: (double value, TitleMeta meta) {
return Text(
_formatChartValue(value),
style: const TextStyle(
color: AppColor.textSecondary,
fontWeight: FontWeight.w500,
fontSize: 10,
),
textAlign: TextAlign.left,
);
},
),
),
),
borderData: FlBorderData(
show: true,
border: Border.all(color: AppColor.borderLight),
),
minX: 0,
maxX: (dailyData.length - 1).toDouble(),
minY: minValue,
maxY: maxValue,
lineBarsData: [
// Cash In Line (Revenue)
LineChartBarData(
spots: _buildCashInSpots(),
isCurved: true,
gradient: LinearGradient(
colors: [AppColor.success.withOpacity(0.8), AppColor.success],
),
barWidth: 3,
isStrokeCapRound: true,
dotData: FlDotData(
show: true,
getDotPainter: (spot, percent, barData, index) {
return FlDotCirclePainter(
radius: 4,
color: AppColor.success,
strokeWidth: 2,
strokeColor: AppColor.white,
);
},
),
belowBarData: BarAreaData(
show: true,
gradient: LinearGradient(
colors: [
AppColor.success.withOpacity(0.1),
AppColor.success.withOpacity(0.0),
],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
),
),
// Cash Out Line (Total Cost)
LineChartBarData(
spots: _buildCashOutSpots(),
isCurved: true,
gradient: LinearGradient(
colors: [AppColor.error.withOpacity(0.8), AppColor.error],
),
barWidth: 3,
isStrokeCapRound: true,
dotData: FlDotData(
show: true,
getDotPainter: (spot, percent, barData, index) {
return FlDotCirclePainter(
radius: 4,
color: AppColor.error,
strokeWidth: 2,
strokeColor: AppColor.white,
);
},
),
),
// Net Flow Line (Net Profit)
LineChartBarData(
spots: _buildNetFlowSpots(),
isCurved: true,
gradient: LinearGradient(
colors: [AppColor.info.withOpacity(0.8), AppColor.info],
),
barWidth: 3,
isStrokeCapRound: true,
dotData: FlDotData(
show: true,
getDotPainter: (spot, percent, barData, index) {
return FlDotCirclePainter(
radius: 4,
color: AppColor.info,
strokeWidth: 2,
strokeColor: AppColor.white,
);
},
),
),
],
);
}
Widget _buildEmptyChart() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
LineIcons.lineChart,
size: 48,
color: AppColor.textSecondary.withOpacity(0.3),
),
const SizedBox(height: 12),
Text(
'Tidak ada data untuk ditampilkan',
style: AppStyle.sm.copyWith(color: AppColor.textSecondary),
),
],
),
);
}
// Helper methods for calculating data
int _calculateTotalCashIn() {
return dailyData.fold(0, (sum, data) => sum + data.revenue);
}
int _calculateTotalCashOut() {
return dailyData.fold(
0,
(sum, data) => sum + data.cost + data.tax + data.discount,
);
}
double _getMaxChartValue() {
if (dailyData.isEmpty) return 30000000;
final maxRevenue = dailyData
.map((e) => e.revenue)
.reduce((a, b) => a > b ? a : b);
final maxCost = dailyData
.map((e) => e.cost + e.tax + e.discount)
.reduce((a, b) => a > b ? a : b);
final maxValue = maxRevenue > maxCost ? maxRevenue : maxCost;
return (maxValue * 1.2).toDouble(); // Add 20% padding
}
double _getMinChartValue() {
if (dailyData.isEmpty) return -5000000;
final minNetProfit = dailyData
.map((e) => e.netProfit)
.reduce((a, b) => a < b ? a : b);
return minNetProfit < 0 ? (minNetProfit * 1.2).toDouble() : 0;
}
List<FlSpot> _buildCashInSpots() {
return dailyData.asMap().entries.map((entry) {
return FlSpot(entry.key.toDouble(), entry.value.revenue.toDouble());
}).toList();
}
List<FlSpot> _buildCashOutSpots() {
return dailyData.asMap().entries.map((entry) {
final totalCost =
entry.value.cost + entry.value.tax + entry.value.discount;
return FlSpot(entry.key.toDouble(), totalCost.toDouble());
}).toList();
}
List<FlSpot> _buildNetFlowSpots() {
return dailyData.asMap().entries.map((entry) {
return FlSpot(entry.key.toDouble(), entry.value.netProfit.toDouble());
}).toList();
}
String _getDayName(int weekday) {
switch (weekday) {
case 1:
return 'Sen';
case 2:
return 'Sel';
case 3:
return 'Rab';
case 4:
return 'Kam';
case 5:
return 'Jum';
case 6:
return 'Sab';
case 7:
return 'Min';
default:
return '';
}
}
String _formatChartValue(double value) {
if (value.abs() >= 1000000) {
return '${(value / 1000000).toStringAsFixed(0)}M';
} else if (value.abs() >= 1000) {
return '${(value / 1000).toStringAsFixed(0)}K';
} else {
return value.toStringAsFixed(0);
}
}
String _formatCurrency(int amount) {
if (amount.abs() >= 1000000000) {
return 'Rp ${(amount / 1000000000).toStringAsFixed(1)}B';
} else if (amount.abs() >= 1000000) {
return 'Rp ${(amount / 1000000).toStringAsFixed(1)}M';
} else if (amount.abs() >= 1000) {
return 'Rp ${(amount / 1000).toStringAsFixed(1)}K';
} else {
return 'Rp ${NumberFormat('#,###', 'id_ID').format(amount)}';
}
}
Widget _buildChartLegend(String label, Color color) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 12,
height: 12,
decoration: BoxDecoration(color: color, shape: BoxShape.circle),
),
const SizedBox(width: 6),
Text(
label,
style: AppStyle.xs.copyWith(
color: AppColor.textSecondary,
fontWeight: FontWeight.w500,
),
),
],
);
}
Widget _buildCashFlowIndicator(
String label,
String amount,
IconData icon,
Color color,
) {
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: color.withOpacity(0.05),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: color.withOpacity(0.2)),
),
child: Column(
children: [
Icon(icon, color: color, size: 20),
const SizedBox(height: 8),
Text(
label,
style: AppStyle.xs.copyWith(color: AppColor.textSecondary),
),
const SizedBox(height: 4),
Text(
amount,
style: AppStyle.md.copyWith(
fontWeight: FontWeight.bold,
color: color,
),
),
],
),
);
}
}
@@ -1,209 +0,0 @@
import 'package:flutter/material.dart';
import 'package:line_icons/line_icons.dart';
import 'package:intl/intl.dart';
import '../../../../common/extension/extension.dart';
import '../../../../common/theme/theme.dart';
import '../../../../domain/analytic/analytic.dart';
import '../../../components/widgets/empty_widget.dart';
class FinanceCategory extends StatelessWidget {
final List<CategoryAnalyticItem> categories;
const FinanceCategory({super.key, required this.categories});
@override
Widget build(BuildContext context) {
final totalRevenue = _calculateTotalRevenue();
final sortedCategories = _sortCategoriesByRevenue();
return Container(
margin: const EdgeInsets.all(16),
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: AppColor.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: AppColor.textLight.withOpacity(0.1),
spreadRadius: 1,
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: AppColor.secondary.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: const Icon(
LineIcons.pieChart,
color: AppColor.secondary,
size: 20,
),
),
const SizedBox(width: 12),
Text(
context.lang.sales_category,
style: AppStyle.lg.copyWith(fontWeight: FontWeight.bold),
),
],
),
const SizedBox(height: 20),
// Show empty state if no categories
if (categories.isEmpty)
_buildEmptyState(context)
else
...sortedCategories.asMap().entries.map(
(entry) => _buildCategoryItem(
context,
entry.value,
_calculatePercentage(entry.value.totalRevenue, totalRevenue),
_getCategoryColor(entry.key),
),
),
],
),
);
}
Widget _buildCategoryItem(
BuildContext context,
CategoryAnalyticItem category,
double percentage,
Color color,
) {
return Container(
margin: const EdgeInsets.only(bottom: 16),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Row(
children: [
Container(
width: 12,
height: 12,
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadius.circular(6),
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
category.categoryName,
style: AppStyle.md.copyWith(
fontWeight: FontWeight.w600,
),
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 2),
Text(
'${category.productCount} ${context.lang.product} • ${category.orderCount} ${context.lang.orders}',
style: AppStyle.xs.copyWith(
color: AppColor.textSecondary,
),
),
],
),
),
],
),
),
const SizedBox(width: 12),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
category.totalRevenue.currencyFormatRp,
style: AppStyle.md.copyWith(
fontWeight: FontWeight.bold,
color: color,
),
),
Text(
'${NumberFormat('#,###', 'id_ID').format(category.totalQuantity)} ${context.lang.unit}',
style: AppStyle.xs.copyWith(color: AppColor.textSecondary),
),
],
),
],
),
const SizedBox(height: 8),
LinearProgressIndicator(
value: percentage / 100,
backgroundColor: AppColor.borderLight,
valueColor: AlwaysStoppedAnimation<Color>(color),
minHeight: 6,
),
const SizedBox(height: 4),
Align(
alignment: Alignment.centerRight,
child: Text(
'${percentage.toStringAsFixed(1)}%',
style: AppStyle.xs.copyWith(color: AppColor.textSecondary),
),
),
],
),
);
}
Widget _buildEmptyState(BuildContext context) {
return EmptyWidget(
title: context.lang.category_no_data,
message: context.lang.category_no_data_desc,
);
}
// Helper methods
int _calculateTotalRevenue() {
return categories.fold(0, (sum, category) => sum + category.totalRevenue);
}
List<CategoryAnalyticItem> _sortCategoriesByRevenue() {
final sorted = List<CategoryAnalyticItem>.from(categories);
sorted.sort((a, b) => b.totalRevenue.compareTo(a.totalRevenue));
return sorted;
}
double _calculatePercentage(int categoryRevenue, int totalRevenue) {
if (totalRevenue == 0) return 0;
return (categoryRevenue / totalRevenue) * 100;
}
Color _getCategoryColor(int index) {
// Predefined color palette for categories
const colors = [
AppColor.primary,
AppColor.secondary,
AppColor.success,
AppColor.warning,
AppColor.error,
AppColor.info,
];
// Generate additional colors if needed
if (index < colors.length) {
return colors[index];
} else {
// Generate colors based on index for unlimited categories
final hue = (index * 137.5) % 360; // Golden angle approximation
return HSLColor.fromAHSL(1.0, hue, 0.7, 0.5).toColor();
}
}
}
@@ -0,0 +1,143 @@
import 'package:flutter/material.dart';
import '../../../../common/extension/extension.dart';
import '../../../../common/theme/theme.dart';
import '../../../../domain/analytic/analytic.dart';
class CostBreakdown extends StatelessWidget {
final ProfitLossPurchasing purchasing;
final int selectedTabIndex;
final DateTime dateFrom;
final DateTime dateTo;
const CostBreakdown({
super.key,
required this.purchasing,
required this.selectedTabIndex,
required this.dateFrom,
required this.dateTo,
});
@override
Widget build(BuildContext context) {
final isToday = selectedTabIndex == 0;
final total = isToday ? purchasing.todayTotal : purchasing.mtdTotal;
return Container(
margin: const EdgeInsets.fromLTRB(16, 0, 16, 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: [
// Title row
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
context.lang.cost_breakdown,
style: AppStyle.lg.copyWith(
fontWeight: FontWeight.w700,
color: AppColor.textPrimary,
),
),
Text(
_formatDateLabel(dateFrom, dateTo),
style: AppStyle.sm.copyWith(
color: AppColor.textSecondary,
fontWeight: FontWeight.w400,
),
),
],
),
const SizedBox(height: 16),
// Item list
...purchasing.items.map((item) => _buildItemRow(item)),
// Total row
const Divider(height: 24, color: AppColor.borderLight),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
context.lang.total_cost,
style: AppStyle.md.copyWith(
fontWeight: FontWeight.w700,
color: AppColor.textPrimary,
),
),
Text(
total.currencyFormatRp,
style: AppStyle.md.copyWith(
fontWeight: FontWeight.w700,
color: AppColor.textPrimary,
),
),
],
),
],
),
);
}
Widget _buildItemRow(ProfitLossPurchasingItem item) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 10),
child: Row(
children: [
Expanded(
child: Text(
item.item,
style: AppStyle.md.copyWith(
color: AppColor.textSecondary,
fontWeight: FontWeight.w400,
),
),
),
Text(
item.nominal.currencyFormatRp,
style: AppStyle.md.copyWith(
color: AppColor.textPrimary,
fontWeight: FontWeight.w600,
),
),
],
),
);
}
String _formatDateLabel(DateTime from, DateTime to) {
const months = [
'Jan',
'Feb',
'Mar',
'Apr',
'Mei',
'Jun',
'Jul',
'Agu',
'Sep',
'Okt',
'Nov',
'Des',
];
if (from.year == to.year && from.month == to.month && from.day == to.day) {
return '${from.day} ${months[from.month - 1]} ${from.year}';
}
return '${from.day} ${months[from.month - 1]} - ${to.day} ${months[to.month - 1]} ${to.year}';
}
}
@@ -1,148 +0,0 @@
import 'package:flutter/material.dart';
import '../../../../common/extension/extension.dart';
import '../../../../common/theme/theme.dart';
import '../../../../domain/analytic/analytic.dart';
class ProfitLossProduct extends StatelessWidget {
final ProfitLossProductData product;
const ProfitLossProduct({super.key, required this.product});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColor.background,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppColor.border.withOpacity(0.5)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Product header
Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
product.productName,
style: AppStyle.md.copyWith(fontWeight: FontWeight.bold),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 2),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 2,
),
decoration: BoxDecoration(
color: AppColor.primary.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Text(
product.categoryName,
style: AppStyle.xs.copyWith(color: AppColor.primary),
),
),
],
),
),
const SizedBox(width: 12),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
'${product.quantitySold} terjual',
style: AppStyle.sm.copyWith(color: AppColor.textSecondary),
),
Text(
'${product.grossProfitMargin.toStringAsFixed(1)}%',
style: AppStyle.md.copyWith(
fontWeight: FontWeight.bold,
color: product.grossProfitMargin > 25
? AppColor.success
: product.grossProfitMargin > 15
? AppColor.warning
: AppColor.error,
),
),
],
),
],
),
const SizedBox(height: 16),
// Financial metrics
Row(
children: [
Expanded(
child: _buildMetricColumn(
context.lang.revenue,
product.revenue.currencyFormatRp,
AppColor.success,
),
),
Expanded(
child: _buildMetricColumn(
context.lang.cost,
product.cost.currencyFormatRp,
AppColor.error,
),
),
Expanded(
child: _buildMetricColumn(
context.lang.gross_profit,
product.grossProfit.currencyFormatRp,
AppColor.info,
),
),
],
),
const SizedBox(height: 12),
// Average metrics
Row(
children: [
Expanded(
child: _buildMetricColumn(
context.lang.average_price,
product.averagePrice.currencyFormatRp,
AppColor.textSecondary,
),
),
Expanded(
child: _buildMetricColumn(
context.lang.profit_per_unit,
product.profitPerUnit.currencyFormatRp,
AppColor.primary,
),
),
const Expanded(child: SizedBox()),
],
),
],
),
);
}
Widget _buildMetricColumn(String label, String value, Color color) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label, style: AppStyle.xs.copyWith(color: AppColor.textSecondary)),
const SizedBox(height: 2),
Text(
value,
style: AppStyle.sm.copyWith(
fontWeight: FontWeight.w600,
color: color,
),
),
],
);
}
}
@@ -1,192 +0,0 @@
import 'package:flutter/material.dart';
import 'package:line_icons/line_icons.dart';
import '../../../../common/extension/extension.dart';
import '../../../../common/theme/theme.dart';
import '../../../../domain/analytic/analytic.dart';
class FinanceProfitLoss extends StatelessWidget {
final ProfitLossSummary data;
const FinanceProfitLoss({super.key, required this.data});
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.all(16),
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: AppColor.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: AppColor.textLight.withOpacity(0.1),
spreadRadius: 1,
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: AppColor.info.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: const Icon(
LineIcons.calculator,
color: AppColor.info,
size: 20,
),
),
const SizedBox(width: 12),
Text(
context.lang.profit_loss_detail,
style: AppStyle.lg.copyWith(fontWeight: FontWeight.bold),
),
],
),
const SizedBox(height: 20),
// Total Revenue (Penjualan Kotor)
_buildPLItem(
context.lang.gross_sales,
data.totalRevenue.currencyFormatRp,
AppColor.success,
true,
),
// Discount (Diskon & Retur)
_buildPLItem(
'${context.lang.discount} & ${context.lang.return_text}',
'- ${data.totalDiscount.currencyFormatRp}',
AppColor.error,
false,
),
const Divider(height: 24),
// Net Sales (Penjualan Bersih = Total Revenue - Discount)
_buildPLItem(
context.lang.net_sales,
(data.totalRevenue - data.totalDiscount).currencyFormatRp,
AppColor.textPrimary,
true,
isHeader: true,
),
const SizedBox(height: 12),
// Cost of Goods Sold (HPP)
_buildPLItem(
'${context.lang.cogs} (${context.lang.cost_of_goods_sold})',
'- ${data.totalCost.currencyFormatRp}',
AppColor.error,
false,
),
const Divider(height: 24),
// Gross Profit (Laba Kotor)
_buildPLItem(
context.lang.gross_profit,
data.grossProfit.currencyFormatRp,
AppColor.success,
true,
isHeader: true,
showPercentage: true,
percentage: '${data.grossProfitMargin.toStringAsFixed(1)}%',
),
const SizedBox(height: 12),
// Operational Cost (Biaya Operasional) - calculated as difference
_buildPLItem(
context.lang.operating_costs,
'- ${_calculateOperationalCost().currencyFormatRp}',
AppColor.error,
false,
),
const Divider(height: 24),
// Net Profit (Laba Bersih)
_buildPLItem(
context.lang.net_profit,
data.netProfit.currencyFormatRp,
AppColor.primary,
true,
isHeader: true,
showPercentage: true,
percentage: '${data.netProfitMargin.round()}%',
),
],
),
);
}
Widget _buildPLItem(
String title,
String amount,
Color color,
bool isPositive, {
bool isHeader = false,
bool showPercentage = false,
String? percentage,
}) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
title,
style: isHeader
? AppStyle.md.copyWith(
fontWeight: FontWeight.bold,
color: color,
)
: AppStyle.md.copyWith(color: AppColor.textSecondary),
),
),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
amount,
style: isHeader
? AppStyle.lg.copyWith(
fontWeight: FontWeight.bold,
color: color,
)
: AppStyle.md.copyWith(
color: color,
fontWeight: FontWeight.w600,
),
),
if (showPercentage && percentage != null)
Text(
percentage,
style: AppStyle.xs.copyWith(
color: AppColor.textSecondary,
fontStyle: FontStyle.italic,
),
),
],
),
],
),
);
}
// Calculate operational cost as the difference between gross profit and net profit
int _calculateOperationalCost() {
return data.grossProfit - data.netProfit - data.totalTax;
}
}
@@ -0,0 +1,389 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:shimmer/shimmer.dart';
import '../../../../application/analytic/profit_loss_loader/profit_loss_loader_bloc.dart';
import '../../../../common/extension/extension.dart';
import '../../../../common/painter/wave_painter.dart';
import '../../../../common/theme/theme.dart';
import '../../../components/spacer/spacer.dart';
class ProfitLossHeader extends StatelessWidget {
final ProfitLossLoaderState state;
final int selectedTabIndex;
final ValueChanged<int> onTabChanged;
const ProfitLossHeader({
super.key,
required this.state,
required this.selectedTabIndex,
required this.onTabChanged,
});
@override
Widget build(BuildContext context) {
final outletLabel = state.profitLoss.outletName.isNotEmpty
? state.profitLoss.outletName
: 'Semua Outlet';
return Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: AppColor.primaryGradient,
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(24),
bottomRight: Radius.circular(24),
),
),
child: Stack(
children: [
// Decorative circles
Positioned(
top: -20,
right: -30,
child: Container(
width: 120,
height: 120,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: AppColor.textWhite.withOpacity(0.08),
),
),
),
Positioned(
top: 30,
right: 20,
child: Container(
width: 60,
height: 60,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: AppColor.textWhite.withOpacity(0.05),
),
),
),
Positioned(
top: 10,
left: -20,
child: Container(
width: 80,
height: 80,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: AppColor.textWhite.withOpacity(0.04),
),
),
),
// Wave pattern
Positioned.fill(
child: CustomPaint(
painter: WavePainter(
animation: 0.0,
color: AppColor.textWhite.withOpacity(0.1),
),
),
),
// Content
SafeArea(
bottom: false,
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Back button + Title row
Row(
children: [
GestureDetector(
onTap: () => context.router.maybePop(),
child: Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: AppColor.textWhite.withOpacity(0.15),
borderRadius: BorderRadius.circular(12),
),
child: const Icon(
Icons.chevron_left_rounded,
color: AppColor.textWhite,
size: 24,
),
),
),
const SpaceWidth(12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
context.lang.profit_loss,
style: AppStyle.xl.copyWith(
color: AppColor.textWhite,
fontWeight: FontWeight.w700,
fontSize: 20,
),
),
const SizedBox(height: 2),
Text(
outletLabel,
style: AppStyle.sm.copyWith(
color: AppColor.textWhite.withOpacity(0.75),
fontWeight: FontWeight.w400,
fontSize: 12,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
),
],
),
const SpaceHeight(20),
// Tab selector (Today / MTD)
_buildTabSelector(context),
const SpaceHeight(24),
// Profit / Loss label with date
Text(
context.lang.profit_loss_date(
_formatDateLabel(state.dateFrom, state.dateTo),
),
style: AppStyle.sm.copyWith(
color: AppColor.textWhite.withOpacity(0.75),
fontWeight: FontWeight.w400,
fontSize: 13,
),
),
const SpaceHeight(4),
// Big profit/loss value
state.isFetching
? _buildHeaderValueShimmer()
: Text(
state.profitLoss.summary.netProfit.currencyFormatRp,
style: AppStyle.h1.copyWith(
color: state.profitLoss.summary.netProfit >= 0
? AppColor.textWhite
: AppColor.textWhite.withOpacity(0.7),
fontWeight: FontWeight.w900,
fontSize: 32,
),
),
const SpaceHeight(16),
// Chips row (Omset + Total Biaya)
state.isFetching
? _buildHeaderChipsShimmer()
: _buildHeaderChips(context),
],
),
),
),
],
),
);
}
Widget _buildTabSelector(BuildContext context) {
final todayLabel = _formatTodayTabLabel(DateTime.now());
final mtdLabel = context.lang.mtd_month(
_getMonthName(DateTime.now().month),
);
return Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: AppColor.textWhite.withOpacity(0.15),
borderRadius: BorderRadius.circular(30),
border: Border.all(color: AppColor.textWhite.withOpacity(0.2)),
),
child: Row(
children: [
Expanded(
child: _buildTab(
label: todayLabel,
isSelected: selectedTabIndex == 0,
onTap: () => onTabChanged(0),
),
),
Expanded(
child: _buildTab(
label: mtdLabel,
isSelected: selectedTabIndex == 1,
onTap: () => onTabChanged(1),
),
),
],
),
);
}
Widget _buildTab({
required String label,
required bool isSelected,
required VoidCallback onTap,
}) {
return GestureDetector(
onTap: onTap,
child: Container(
padding: const EdgeInsets.symmetric(vertical: 10),
decoration: BoxDecoration(
color: isSelected ? AppColor.white : Colors.transparent,
borderRadius: BorderRadius.circular(26),
),
child: Center(
child: Text(
label,
style: AppStyle.md.copyWith(
color: isSelected ? AppColor.textPrimary : AppColor.textWhite,
fontWeight: FontWeight.w600,
),
),
),
),
);
}
Widget _buildHeaderValueShimmer() {
return Shimmer.fromColors(
baseColor: AppColor.textWhite.withOpacity(0.3),
highlightColor: AppColor.textWhite.withOpacity(0.6),
child: Container(
width: 200,
height: 36,
decoration: BoxDecoration(
color: AppColor.textWhite.withOpacity(0.3),
borderRadius: BorderRadius.circular(8),
),
),
);
}
Widget _buildHeaderChipsShimmer() {
return Row(
children: List.generate(
2,
(index) => Padding(
padding: const EdgeInsets.only(right: 8),
child: Shimmer.fromColors(
baseColor: AppColor.textWhite.withOpacity(0.15),
highlightColor: AppColor.textWhite.withOpacity(0.3),
child: Container(
width: 130,
height: 32,
decoration: BoxDecoration(
color: AppColor.textWhite.withOpacity(0.15),
borderRadius: BorderRadius.circular(20),
),
),
),
),
),
);
}
Widget _buildHeaderChips(BuildContext context) {
final summary = state.profitLoss.summary;
return Wrap(
spacing: 8,
runSpacing: 8,
children: [
_buildChip(
'${context.lang.sales} ${summary.totalRevenue.currencyFormatRp}',
),
_buildChip(
'${context.lang.total_cost} ${summary.totalCost.currencyFormatRp}',
),
],
);
}
Widget _buildChip(String label) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
decoration: BoxDecoration(
color: AppColor.textWhite.withOpacity(0.15),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: AppColor.textWhite.withOpacity(0.25)),
),
child: Text(
label,
style: AppStyle.sm.copyWith(
color: AppColor.textWhite,
fontWeight: FontWeight.w600,
fontSize: 12,
),
),
);
}
String _formatTodayTabLabel(DateTime date) {
const months = [
'Januari',
'Februari',
'Maret',
'April',
'Mei',
'Juni',
'Juli',
'Agustus',
'September',
'Oktober',
'November',
'Desember',
];
return '${date.day} ${months[date.month - 1]}';
}
String _formatDateLabel(DateTime from, DateTime to) {
const months = [
'Jan',
'Feb',
'Mar',
'Apr',
'Mei',
'Jun',
'Jul',
'Agu',
'Sep',
'Okt',
'Nov',
'Des',
];
if (from.year == to.year && from.month == to.month && from.day == to.day) {
return '${from.day} ${months[from.month - 1]} ${from.year}';
}
return '${from.day} ${months[from.month - 1]} - ${to.day} ${months[to.month - 1]} ${to.year}';
}
String _getMonthName(int month) {
const months = [
'Januari',
'Februari',
'Maret',
'April',
'Mei',
'Juni',
'Juli',
'Agustus',
'September',
'Oktober',
'November',
'Desember',
];
return months[month - 1];
}
}
@@ -0,0 +1,238 @@
import 'package:flutter/material.dart';
import '../../../../common/extension/extension.dart';
import '../../../../common/theme/theme.dart';
import '../../../../domain/analytic/analytic.dart';
class ProfitLossReport extends StatelessWidget {
final List<ProfitLossMainSummaryItem> mainSummary;
final ProfitLossSummary summary;
final int selectedTabIndex;
const ProfitLossReport({
super.key,
required this.mainSummary,
required this.summary,
required this.selectedTabIndex,
});
@override
Widget build(BuildContext context) {
final isToday = selectedTabIndex == 0;
final marginPct = isToday
? summary.netProfitMargin.round()
: summary.netProfitMargin.round();
return Container(
margin: const EdgeInsets.all(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: [
// Title row
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
context.lang.profit_loss_report,
style: AppStyle.lg.copyWith(
fontWeight: FontWeight.w700,
color: AppColor.textPrimary,
),
),
Text(
'margin $marginPct%',
style: AppStyle.sm.copyWith(
color: AppColor.textSecondary,
fontWeight: FontWeight.w400,
),
),
],
),
const SizedBox(height: 20),
// Main summary items
...mainSummary.map((item) => _buildSummarySection(item, isToday)),
const SizedBox(height: 12),
// Net Profit/Loss footer
_buildNetProfitFooter(context, isToday),
],
),
);
}
Widget _buildSummarySection(ProfitLossMainSummaryItem item, bool isToday) {
final nominal = isToday ? item.todayNominal : item.mtdNominal;
final pct = isToday ? item.todayPct : item.mtdPct;
return Column(
children: [
// Main item row
_buildItemRow(
label: item.label,
nominal: nominal,
pct: pct,
isBold: item.isBold,
isSubItem: false,
),
// Sub items
...item.subItems.map((subItem) {
final subNominal = isToday
? subItem.todayNominal
: subItem.mtdNominal;
final subPct = isToday ? subItem.todayPct : subItem.mtdPct;
return _buildItemRow(
label: subItem.label,
nominal: subNominal,
pct: subPct,
isBold: subItem.isBold,
isSubItem: true,
);
}),
// Divider after section (except for sub-items only sections)
if (item.isBold)
const Padding(
padding: EdgeInsets.symmetric(vertical: 8),
child: Divider(height: 1, color: AppColor.borderLight),
),
],
);
}
Widget _buildItemRow({
required String label,
required int nominal,
required double pct,
required bool isBold,
required bool isSubItem,
}) {
final isNegative = nominal < 0;
final displayNominal = isNegative
? '-${nominal.abs().currencyFormatRp}'
: nominal.currencyFormatRp;
final pctText = '${pct.round()}%';
// Determine color based on context
Color nominalColor;
if (isBold && isNegative) {
nominalColor = AppColor.error;
} else if (isBold) {
nominalColor = AppColor.textPrimary;
} else {
nominalColor = AppColor.textPrimary;
}
return Padding(
padding: EdgeInsets.only(left: isSubItem ? 16 : 0, top: 6, bottom: 6),
child: Row(
children: [
// Label
Expanded(
flex: 5,
child: Text(
label,
style: isBold
? AppStyle.md.copyWith(
fontWeight: FontWeight.w700,
color: AppColor.textPrimary,
)
: AppStyle.md.copyWith(
color: AppColor.textSecondary,
fontWeight: FontWeight.w400,
),
),
),
// Nominal
Expanded(
flex: 3,
child: Text(
displayNominal,
textAlign: TextAlign.right,
style: isBold
? AppStyle.md.copyWith(
fontWeight: FontWeight.w700,
color: nominalColor,
)
: AppStyle.md.copyWith(
color: AppColor.textPrimary,
fontWeight: FontWeight.w500,
),
),
),
// Percentage
SizedBox(
width: 48,
child: Text(
pctText,
textAlign: TextAlign.right,
style: AppStyle.sm.copyWith(
color: AppColor.textSecondary,
fontWeight: FontWeight.w400,
),
),
),
],
),
);
}
Widget _buildNetProfitFooter(BuildContext context, bool isToday) {
final netProfit = summary.netProfit;
final isNegative = netProfit < 0;
final displayValue = isNegative
? '-${netProfit.abs().currencyFormatRp}'
: netProfit.currencyFormatRp;
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
decoration: BoxDecoration(
color: isNegative
? AppColor.error.withOpacity(0.08)
: AppColor.success.withOpacity(0.08),
borderRadius: BorderRadius.circular(12),
),
child: Row(
children: [
Flexible(
child: Text(
context.lang.net_profit_loss,
style: AppStyle.md.copyWith(
fontWeight: FontWeight.w700,
color: isNegative ? AppColor.error : AppColor.success,
),
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(width: 12),
Text(
displayValue,
style: AppStyle.lg.copyWith(
fontWeight: FontWeight.w900,
color: isNegative ? AppColor.error : AppColor.success,
fontSize: 20,
),
),
],
),
);
}
}
@@ -1,70 +0,0 @@
import 'package:flutter/material.dart';
import '../../../../common/theme/theme.dart';
class FinanceSummaryCard extends StatelessWidget {
const FinanceSummaryCard({
super.key,
required this.title,
required this.amount,
required this.icon,
required this.color,
required this.isPositive,
});
final String title;
final String amount;
final IconData icon;
final Color color;
final bool isPositive;
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColor.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: AppColor.textLight.withOpacity(0.1),
spreadRadius: 1,
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(icon, color: color, size: 20),
),
],
),
const SizedBox(height: 12),
Text(
title,
style: AppStyle.sm.copyWith(color: AppColor.textSecondary),
),
const SizedBox(height: 4),
Text(
amount,
style: AppStyle.lg.copyWith(
fontWeight: FontWeight.bold,
color: AppColor.textPrimary,
),
),
],
),
);
}
}
@@ -16,6 +16,7 @@ import 'widgets/feature.dart';
import 'widgets/header.dart';
import 'widgets/home_top_products.dart';
import 'widgets/home_warnings.dart';
import 'widgets/profit_sharing_banner.dart';
import 'widgets/stats.dart';
@RoutePage()
@@ -201,6 +202,7 @@ class _HomePageState extends State<HomePage> with TickerProviderStateMixin {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
HomeFeature(),
HomeProfitSharingBanner(),
HomeWarnings(),
HomeStats(overview: state.dashboard.overview),
HomeTopProducts(
@@ -99,8 +99,8 @@ class _HomeHeaderState extends State<HomeHeader>
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
AppColor.primary,
AppColor.primary.withOpacity(0.9),
AppColor.primaryDark,
AppColor.primaryDark.withOpacity(0.9),
AppColor.primaryLight.withOpacity(0.85),
],
begin: Alignment.topLeft,
@@ -17,8 +17,16 @@ class HeaderDateFilter extends StatelessWidget {
return Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: AppColor.white.withOpacity(0.15),
color: AppColor.primary.withOpacity(0.8),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppColor.white.withOpacity(0.3)),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.15),
blurRadius: 8,
offset: const Offset(0, 3),
),
],
),
child: Row(
children: [
@@ -39,11 +47,22 @@ class HeaderDateFilter extends StatelessWidget {
}
},
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
duration: const Duration(milliseconds: 220),
curve: Curves.easeOutCubic,
padding: const EdgeInsets.symmetric(vertical: 10),
decoration: BoxDecoration(
color: isSelected ? AppColor.white : Colors.transparent,
borderRadius: BorderRadius.circular(10),
boxShadow: isSelected
? [
BoxShadow(
color: Colors.black.withOpacity(0.18),
blurRadius: 8,
spreadRadius: 0,
offset: const Offset(0, 3),
),
]
: null,
),
alignment: Alignment.center,
child: Text(
@@ -18,9 +18,17 @@ class HeaderOutletSelector extends StatelessWidget {
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
color: AppColor.white.withOpacity(0.15),
color: AppColor.primary.withOpacity(0.8),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppColor.white.withOpacity(0.3)),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.15),
blurRadius: 10,
spreadRadius: 0,
offset: const Offset(0, 4),
),
],
),
child: Row(
children: [
@@ -34,85 +34,202 @@ class HeaderSummaryCard extends StatelessWidget {
return GestureDetector(
onTap: onTap,
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColor.white.withOpacity(0.15),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: AppColor.white.withOpacity(0.2)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Top: Icon + Title + Percentage + Chevron
Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: iconColor,
borderRadius: BorderRadius.circular(10),
),
child: Icon(icon, color: Colors.white, size: 20),
),
const SpaceWidth(10),
Text(
title,
style: AppStyle.md.copyWith(
color: AppColor.white,
fontWeight: FontWeight.w600,
),
),
const Spacer(),
if (percentage != null) _buildPercentageBadge(),
const SpaceWidth(6),
Icon(
Icons.chevron_right_rounded,
color: AppColor.white.withOpacity(0.7),
size: 20,
),
],
borderRadius: BorderRadius.circular(18),
gradient: const LinearGradient(
colors: [
Color(0xFFE8000A), // sedikit lebih terang di atas
Color(0xFFC40202), // primary di tengah
Color(0xFF990000), // gelap di bawah
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
stops: [0.0, 0.5, 1.0],
),
boxShadow: [
// Shadow merah di bawah — efek floating/3D
BoxShadow(
color: const Color(0xFFC40202).withOpacity(0.55),
blurRadius: 16,
spreadRadius: 0,
offset: const Offset(0, 8),
),
const SpaceHeight(12),
// Value (hidden or visible)
isValueVisible
? Text(
value.currencyFormatRp,
style: AppStyle.h1.copyWith(
color: AppColor.white,
fontWeight: FontWeight.w900,
fontSize: 26,
),
)
: Text(
'Rp ••••••',
style: AppStyle.h1.copyWith(
color: AppColor.white,
fontWeight: FontWeight.w900,
fontSize: 26,
letterSpacing: 2,
),
),
const SpaceHeight(4),
// Subtitle
Text(
subtitle,
style: AppStyle.xs.copyWith(
color: AppColor.white.withOpacity(0.7),
fontWeight: FontWeight.w400,
fontStyle: FontStyle.italic,
),
// Inner highlight di atas kiri (simulasi cahaya)
BoxShadow(
color: AppColor.white.withOpacity(0.08),
blurRadius: 0,
spreadRadius: -1,
offset: const Offset(0, 1),
),
const Spacer(),
// Mini bar chart
_buildMiniBarChart(),
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(18),
child: Stack(
children: [
// Top highlight strip — efek glossy
Positioned(
top: 0,
left: 0,
right: 0,
child: Container(
height: 1.5,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
AppColor.white.withOpacity(0.0),
AppColor.white.withOpacity(0.35),
AppColor.white.withOpacity(0.0),
],
),
),
),
),
// Decorative circles
Positioned(
top: -30,
right: -30,
child: Container(
width: 120,
height: 120,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: AppColor.white.withOpacity(0.07),
),
),
),
Positioned(
bottom: -20,
right: 20,
child: Container(
width: 70,
height: 70,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: AppColor.white.withOpacity(0.05),
),
),
),
Positioned(
top: 40,
left: -20,
child: Container(
width: 60,
height: 60,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: AppColor.white.withOpacity(0.04),
),
),
),
// Content
Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Top: Icon + Title + Percentage + Chevron
Row(
children: [
// Icon dengan shadow
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [iconColor.withOpacity(0.9), iconColor],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(10),
boxShadow: [
BoxShadow(
color: iconColor.withOpacity(0.5),
blurRadius: 8,
offset: const Offset(0, 4),
),
],
),
child: Icon(icon, color: Colors.white, size: 20),
),
const SpaceWidth(10),
Text(
title,
style: AppStyle.md.copyWith(
color: AppColor.white,
fontWeight: FontWeight.w600,
shadows: [
Shadow(
color: Colors.black.withOpacity(0.2),
blurRadius: 4,
offset: const Offset(0, 1),
),
],
),
),
const Spacer(),
if (percentage != null) _buildPercentageBadge(),
const SpaceWidth(6),
Icon(
Icons.chevron_right_rounded,
color: AppColor.white.withOpacity(0.7),
size: 20,
),
],
),
const SpaceHeight(12),
// Value
isValueVisible
? Text(
value.currencyFormatRp,
style: AppStyle.h1.copyWith(
color: AppColor.white,
fontWeight: FontWeight.w900,
fontSize: 26,
shadows: [
Shadow(
color: Colors.black.withOpacity(0.25),
blurRadius: 6,
offset: const Offset(0, 2),
),
],
),
)
: Text(
'Rp ••••••',
style: AppStyle.h1.copyWith(
color: AppColor.white,
fontWeight: FontWeight.w900,
fontSize: 26,
letterSpacing: 2,
),
),
const SpaceHeight(4),
// Subtitle
Text(
subtitle,
style: AppStyle.xs.copyWith(
color: AppColor.white.withOpacity(0.7),
fontWeight: FontWeight.w400,
fontStyle: FontStyle.italic,
),
),
const Spacer(),
// Mini bar chart
_buildMiniBarChart(),
],
),
),
],
),
),
),
);
}
@@ -126,6 +243,12 @@ class HeaderSummaryCard extends StatelessWidget {
? const Color(0xFF4CAF50).withOpacity(0.25)
: const Color(0xFFE53E3E).withOpacity(0.25),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: isPositive
? const Color(0xFF4CAF50).withOpacity(0.4)
: const Color(0xFFE53E3E).withOpacity(0.4),
width: 0.5,
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
@@ -162,18 +285,31 @@ class HeaderSummaryCard extends StatelessWidget {
.fold<int>(0, (a, b) => a > b ? a : b);
return SizedBox(
height: 24,
height: 28,
child: Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: dailyData.map((d) {
final ratio = maxVal > 0 ? d.totalCost / maxVal : 0.0;
final isMax = maxVal > 0 && d.totalCost == maxVal;
return Expanded(
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 1.5),
height: 6 + (18 * ratio),
height: 6 + (22 * ratio),
decoration: BoxDecoration(
color: AppColor.white.withOpacity(0.4),
// Bar tertinggi lebih terang
color: isMax
? AppColor.white.withOpacity(0.75)
: AppColor.white.withOpacity(0.35),
borderRadius: BorderRadius.circular(3),
boxShadow: isMax
? [
BoxShadow(
color: AppColor.white.withOpacity(0.3),
blurRadius: 4,
offset: const Offset(0, -2),
),
]
: null,
),
),
);
@@ -0,0 +1,89 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:hugeicons/hugeicons.dart';
import '../../../../common/extension/extension.dart';
import '../../../../common/theme/theme.dart';
import '../../../components/spacer/spacer.dart';
import '../../../router/app_router.gr.dart';
/// Pintu masuk ke halaman bagi hasil dari beranda.
class HomeProfitSharingBanner extends StatelessWidget {
const HomeProfitSharingBanner({super.key});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 0),
child: InkWell(
borderRadius: BorderRadius.circular(AppValue.radius),
onTap: () => context.router.push(const ProfitSharingRoute()),
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: AppColor.primaryGradient,
begin: Alignment.centerLeft,
end: Alignment.centerRight,
),
borderRadius: BorderRadius.circular(AppValue.radius),
boxShadow: [
BoxShadow(
color: AppColor.primary.withOpacity(0.25),
blurRadius: 12,
offset: const Offset(0, 4),
),
],
),
child: Row(
children: [
Container(
width: 44,
height: 44,
decoration: BoxDecoration(
color: AppColor.textWhite.withOpacity(0.18),
borderRadius: BorderRadius.circular(12),
),
child: const HugeIcon(
icon: HugeIcons.strokeRoundedPieChart,
color: AppColor.textWhite,
size: 24,
),
),
const SpaceWidth(14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
context.lang.profit_sharing,
style: AppStyle.md.copyWith(
color: AppColor.textWhite,
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: 2),
Text(
context.lang.profit_sharing_desc,
style: AppStyle.xs.copyWith(
color: AppColor.textWhite.withOpacity(0.8),
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
),
),
const SpaceWidth(8),
const Icon(
Icons.chevron_right_rounded,
color: AppColor.textWhite,
size: 24,
),
],
),
),
),
);
}
}
@@ -44,7 +44,7 @@ class HomeStats extends StatelessWidget {
icon: Icons.hexagon_outlined,
iconColor: const Color(0xFF4CAF50),
blobColor: const Color(0xFF4CAF50),
value: '0', // TODO: connect items sold data
value: overview.totalItemSold.toString(),
label: context.lang.items_sold,
),
),
@@ -58,7 +58,7 @@ class HomeStats extends StatelessWidget {
icon: Icons.warning_amber_rounded,
iconColor: const Color(0xFFFF9800),
blobColor: const Color(0xFFFF9800),
value: '0', // TODO: connect low stock data
value: overview.totalLowStock.toString(),
label: context.lang.low_stock_warning,
),
),
@@ -68,7 +68,7 @@ class HomeStats extends StatelessWidget {
icon: Icons.hexagon_outlined,
iconColor: const Color(0xFFE53935),
blobColor: const Color(0xFFE53935),
value: '0', // TODO: connect active products data
value: overview.totalProductActive.toString(),
label: context.lang.active_products,
),
),
@@ -3,17 +3,11 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../application/analytic/inventory_analytic_loader/inventory_analytic_loader_bloc.dart';
import '../../../common/extension/extension.dart';
import '../../../common/theme/theme.dart';
import '../../../domain/analytic/analytic.dart';
import '../../../injection.dart';
import '../../components/appbar/appbar.dart';
import 'widgets/ingredient_tile.dart';
import 'widgets/product_tile.dart';
import 'widgets/stat_card.dart';
import 'widgets/tabbar_delegate.dart';
// Custom SliverPersistentHeaderDelegate untuk TabBar
import '../../components/spacer/spacer.dart';
import 'widgets/inventory_header.dart';
import 'widgets/inventory_stock_report.dart';
@RoutePage()
class InventoryPage extends StatefulWidget implements AutoRouteWrapper {
@@ -32,400 +26,187 @@ class InventoryPage extends StatefulWidget implements AutoRouteWrapper {
}
class _InventoryPageState extends State<InventoryPage>
with TickerProviderStateMixin {
late AnimationController _fadeAnimationController;
late AnimationController _slideAnimationController;
with SingleTickerProviderStateMixin {
late AnimationController _fadeController;
late Animation<double> _fadeAnimation;
late Animation<Offset> _slideAnimation;
late TabController _tabController;
int _selectedTabIndex = 0;
@override
void initState() {
super.initState();
_tabController = TabController(length: 2, vsync: this);
_fadeAnimationController = AnimationController(
_fadeController = AnimationController(
duration: const Duration(milliseconds: 1000),
vsync: this,
);
_slideAnimationController = AnimationController(
duration: const Duration(milliseconds: 800),
vsync: this,
);
_fadeAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(
parent: _fadeAnimationController,
curve: Curves.easeInOut,
),
);
_fadeAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(parent: _fadeController, curve: Curves.easeIn));
_slideAnimation =
Tween<Offset>(begin: const Offset(0.0, 0.3), end: Offset.zero).animate(
CurvedAnimation(
parent: _slideAnimationController,
curve: Curves.easeOutBack,
),
);
_fadeAnimationController.forward();
_slideAnimationController.forward();
_fadeController.forward();
}
@override
void dispose() {
_fadeAnimationController.dispose();
_slideAnimationController.dispose();
_tabController.dispose();
_fadeController.dispose();
super.dispose();
}
Color getStatusColor(String status) {
switch (status) {
case 'available':
return AppColor.success;
case 'low_stock':
return AppColor.warning;
case 'out_of_stock':
return AppColor.error;
default:
return AppColor.textSecondary;
}
}
String getStatusText(String status) {
switch (status) {
case 'available':
return context.lang.available;
case 'low_stock':
return context.lang.low_stock;
case 'out_of_stock':
return context.lang.out_of_stock;
default:
return 'Unknown';
}
}
@override
Widget build(BuildContext context) {
return BlocListener<
InventoryAnalyticLoaderBloc,
InventoryAnalyticLoaderState
>(
listenWhen: (previous, current) =>
previous.dateFrom != current.dateFrom ||
previous.dateTo != current.dateTo,
listener: (context, state) {
context.read<InventoryAnalyticLoaderBloc>().add(
InventoryAnalyticLoaderEvent.fetched(),
);
},
child: Scaffold(
backgroundColor: AppColor.background,
body:
BlocBuilder<
InventoryAnalyticLoaderBloc,
InventoryAnalyticLoaderState
>(
builder: (context, state) {
return FadeTransition(
opacity: _fadeAnimation,
child: SlideTransition(
position: _slideAnimation,
child: NestedScrollView(
headerSliverBuilder: (context, innerBoxIsScrolled) {
return [
_buildSliverAppBar(),
SliverPersistentHeader(
pinned: true,
delegate: InventorySliverTabBarDelegate(
startDate: state.dateFrom,
endDate: state.dateTo,
return Scaffold(
backgroundColor: AppColor.background,
body:
BlocListener<
InventoryAnalyticLoaderBloc,
InventoryAnalyticLoaderState
>(
listenWhen: (previous, current) =>
previous.dateFrom != current.dateFrom ||
previous.dateTo != current.dateTo,
listener: (context, state) {
context.read<InventoryAnalyticLoaderBloc>().add(
InventoryAnalyticLoaderEvent.fetched(),
);
},
child:
BlocBuilder<
InventoryAnalyticLoaderBloc,
InventoryAnalyticLoaderState
>(
builder: (context, state) {
return CustomScrollView(
slivers: [
// Header with gradient background, tabs, and summary
SliverToBoxAdapter(
child: FadeTransition(
opacity: _fadeAnimation,
child: InventoryHeader(
state: state,
selectedTabIndex: _selectedTabIndex,
onTabChanged: (index) {
setState(() {
_selectedTabIndex = index;
});
},
onDateRangeChanged: (startDate, endDate) {
context.read<InventoryAnalyticLoaderBloc>().add(
InventoryAnalyticLoaderEvent.rangeDateChanged(
startDate!,
endDate!,
),
_onDateRangeChanged(
context,
startDate,
endDate,
);
},
tabBar: TabBar(
controller: _tabController,
indicator: BoxDecoration(
gradient: LinearGradient(
colors: AppColor.primaryGradient,
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(25),
boxShadow: [
BoxShadow(
color: AppColor.primary.withOpacity(0.3),
blurRadius: 12,
offset: const Offset(0, 4),
),
],
),
indicatorSize: TabBarIndicatorSize.tab,
indicatorPadding: const EdgeInsets.all(6),
labelColor: AppColor.textWhite,
unselectedLabelColor: AppColor.textSecondary,
labelStyle: const TextStyle(
fontWeight: FontWeight.w700,
fontSize: 13,
),
unselectedLabelStyle: const TextStyle(
fontWeight: FontWeight.w500,
fontSize: 13,
),
dividerColor: Colors.transparent,
splashFactory: NoSplash.splashFactory,
overlayColor: MaterialStateProperty.all(
Colors.transparent,
),
tabs: [
Tab(
height: 40,
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
),
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment:
MainAxisAlignment.center,
children: [
Icon(
Icons.inventory_2_rounded,
size: 16,
),
SizedBox(width: 6),
Text(context.lang.product),
],
),
),
),
Tab(
height: 40,
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
),
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment:
MainAxisAlignment.center,
children: [
Icon(
Icons.restaurant_menu_rounded,
size: 16,
),
SizedBox(width: 6),
Text(context.lang.ingredients),
],
),
),
),
],
),
),
),
];
},
body: TabBarView(
controller: _tabController,
children: [
_buildProductTab(state.inventoryAnalytic),
_buildIngredientTab(state.inventoryAnalytic),
],
),
),
// Stock Report Table
SliverToBoxAdapter(
child: FadeTransition(
opacity: _fadeAnimation,
child: state.isFetching
? _buildLoadingReport()
: InventoryStockReport(
inventoryAnalytic: state.inventoryAnalytic,
selectedTabIndex: _selectedTabIndex,
),
),
),
// Bottom spacing
const SliverToBoxAdapter(child: SpaceHeight(100)),
],
);
},
),
),
);
}
Widget _buildLoadingReport() {
return Container(
margin: const EdgeInsets.all(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(
children: List.generate(
5,
(index) => Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
children: [
Expanded(
flex: 4,
child: Container(
height: 14,
decoration: BoxDecoration(
color: AppColor.borderLight,
borderRadius: BorderRadius.circular(4),
),
),
);
},
),
const SpaceWidth(12),
Expanded(
flex: 2,
child: Container(
height: 14,
decoration: BoxDecoration(
color: AppColor.borderLight,
borderRadius: BorderRadius.circular(4),
),
),
),
const SpaceWidth(12),
Expanded(
flex: 2,
child: Container(
height: 14,
decoration: BoxDecoration(
color: AppColor.borderLight,
borderRadius: BorderRadius.circular(4),
),
),
),
const SpaceWidth(12),
Expanded(
flex: 2,
child: Container(
height: 14,
decoration: BoxDecoration(
color: AppColor.borderLight,
borderRadius: BorderRadius.circular(4),
),
),
),
],
),
),
),
),
);
}
Widget _buildSliverAppBar() {
return SliverAppBar(
expandedHeight: 120,
floating: false,
pinned: true,
elevation: 0,
backgroundColor: AppColor.primary,
flexibleSpace: CustomAppBar(title: context.lang.inventory),
);
}
Widget _buildProductTab(InventoryAnalytic inventoryAnalytic) {
return CustomScrollView(
slivers: [
SliverToBoxAdapter(
child: _buildProductStats(inventoryAnalytic.summary),
),
SliverPadding(
padding: const EdgeInsets.all(16),
sliver: SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) =>
InventoryProductTile(item: inventoryAnalytic.products[index]),
childCount: inventoryAnalytic.products.length,
),
),
),
],
);
}
Widget _buildIngredientTab(InventoryAnalytic inventoryAnalytic) {
return CustomScrollView(
slivers: [
SliverToBoxAdapter(
child: _buildIngredientStats(inventoryAnalytic.summary),
),
SliverPadding(
padding: const EdgeInsets.all(16),
sliver: SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) => InventoryIngredientTile(
item: inventoryAnalytic.ingredients[index],
),
childCount: inventoryAnalytic.ingredients.length,
),
),
),
],
);
}
Widget _buildProductStats(InventorySummary inventory) {
return Container(
margin: const EdgeInsets.all(16),
child: Column(
children: [
Row(
children: [
Expanded(
child: _buildStatCard(
context.lang.total_products,
inventory.totalProducts.toString(),
Icons.inventory_2_rounded,
AppColor.primary,
),
),
const SizedBox(width: 16),
Expanded(
child: _buildStatCard(
context.lang.total_sold,
inventory.totalSoldProducts.toString(),
Icons.check_circle_rounded,
AppColor.success,
),
),
],
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: _buildStatCard(
context.lang.low_stock,
inventory.lowStockProducts.toString(),
Icons.warning_rounded,
AppColor.warning,
),
),
const SizedBox(width: 16),
Expanded(
child: _buildStatCard(
context.lang.zero_stock,
inventory.zeroStockProducts.toString(),
Icons.error_rounded,
AppColor.error,
),
),
],
),
],
),
);
}
Widget _buildIngredientStats(InventorySummary inventory) {
return Container(
margin: const EdgeInsets.all(16),
child: Column(
children: [
Row(
children: [
Expanded(
child: _buildStatCard(
context.lang.total_ingredients,
inventory.totalIngredients.toString(),
Icons.restaurant_menu_rounded,
AppColor.primary,
),
),
const SizedBox(width: 16),
Expanded(
child: _buildStatCard(
context.lang.total_sold,
inventory.totalSoldIngredients.toString(),
Icons.check_circle_rounded,
AppColor.success,
),
),
],
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: _buildStatCard(
context.lang.low_stock,
inventory.lowStockIngredients.toString(),
Icons.warning_rounded,
AppColor.warning,
),
),
const SizedBox(width: 16),
Expanded(
child: _buildStatCard(
context.lang.zero_stock,
inventory.zeroStockIngredients.toString(),
Icons.error_rounded,
AppColor.error,
),
),
],
),
],
),
);
}
Widget _buildStatCard(
String title,
String value,
IconData icon,
Color color,
void _onDateRangeChanged(
BuildContext context,
DateTime startDate,
DateTime endDate,
) {
return TweenAnimationBuilder<double>(
tween: Tween<double>(begin: 0, end: 1),
duration: const Duration(milliseconds: 800),
builder: (context, animationValue, child) {
return Transform.scale(
scale: animationValue,
child: InventoryStatCard(
title: title,
value: value,
icon: icon,
color: color,
),
);
},
context.read<InventoryAnalyticLoaderBloc>().add(
InventoryAnalyticLoaderEvent.rangeDateChanged(startDate, endDate),
);
}
}
@@ -0,0 +1,414 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:shimmer/shimmer.dart';
import '../../../../application/analytic/inventory_analytic_loader/inventory_analytic_loader_bloc.dart';
import '../../../../common/extension/extension.dart';
import '../../../../common/painter/wave_painter.dart';
import '../../../../common/theme/theme.dart';
import '../../../components/bottom_sheet/date_range_bottom_sheet.dart';
import '../../../components/spacer/spacer.dart';
class InventoryHeader extends StatelessWidget {
final InventoryAnalyticLoaderState state;
final int selectedTabIndex;
final ValueChanged<int> onTabChanged;
final void Function(DateTime startDate, DateTime endDate)? onDateRangeChanged;
const InventoryHeader({
super.key,
required this.state,
required this.selectedTabIndex,
required this.onTabChanged,
this.onDateRangeChanged,
});
@override
Widget build(BuildContext context) {
final outletLabel = state.inventoryAnalytic.summary.outletName.isNotEmpty
? state.inventoryAnalytic.summary.outletName
: context.lang.all_outlets;
final dateLabel = _formatDateRange(state.dateFrom, state.dateTo);
return Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: AppColor.primaryGradient,
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(24),
bottomRight: Radius.circular(24),
),
),
child: Stack(
children: [
// Decorative circles
Positioned(
top: -20,
right: -30,
child: Container(
width: 120,
height: 120,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: AppColor.textWhite.withOpacity(0.08),
),
),
),
Positioned(
top: 30,
right: 20,
child: Container(
width: 60,
height: 60,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: AppColor.textWhite.withOpacity(0.05),
),
),
),
Positioned(
top: 10,
left: -20,
child: Container(
width: 80,
height: 80,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: AppColor.textWhite.withOpacity(0.04),
),
),
),
// Wave pattern
Positioned.fill(
child: CustomPaint(
painter: WavePainter(
animation: 0.0,
color: AppColor.textWhite.withOpacity(0.1),
),
),
),
// Content
SafeArea(
bottom: false,
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Back button + Title row + Calendar button
Row(
children: [
if (context.router.canPop()) ...[
GestureDetector(
onTap: () => context.router.maybePop(),
child: Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: AppColor.textWhite.withOpacity(0.15),
borderRadius: BorderRadius.circular(12),
),
child: const Icon(
Icons.chevron_left_rounded,
color: AppColor.textWhite,
size: 24,
),
),
),
const SpaceWidth(12),
],
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
context.lang.inventory,
style: AppStyle.xl.copyWith(
color: AppColor.textWhite,
fontWeight: FontWeight.w700,
fontSize: 20,
),
),
const SizedBox(height: 2),
Text(
'$dateLabel · $outletLabel',
style: AppStyle.sm.copyWith(
color: AppColor.textWhite.withOpacity(0.75),
fontWeight: FontWeight.w400,
fontSize: 12,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
),
const SpaceWidth(8),
// Date filter button
GestureDetector(
onTap: () => _showDatePicker(context),
child: Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: AppColor.textWhite.withOpacity(0.15),
borderRadius: BorderRadius.circular(12),
),
child: const Icon(
Icons.calendar_month_rounded,
color: AppColor.textWhite,
size: 20,
),
),
),
],
),
const SpaceHeight(20),
// Tab selector (Product / Ingredient)
_buildTabSelector(context),
const SpaceHeight(24),
// Total Value label
Text(
context.lang.total_inventory_value,
style: AppStyle.sm.copyWith(
color: AppColor.textWhite.withOpacity(0.75),
fontWeight: FontWeight.w400,
fontSize: 13,
),
),
const SpaceHeight(4),
// Big total value
state.isFetching
? _buildHeaderValueShimmer()
: Text(
state
.inventoryAnalytic
.summary
.totalValue
.currencyFormatRp,
style: AppStyle.h1.copyWith(
color: AppColor.textWhite,
fontWeight: FontWeight.w900,
fontSize: 32,
),
),
const SpaceHeight(16),
// Chips row
state.isFetching
? _buildHeaderChipsShimmer()
: _buildHeaderChips(context),
],
),
),
),
],
),
);
}
Widget _buildTabSelector(BuildContext context) {
return Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: AppColor.textWhite.withOpacity(0.15),
borderRadius: BorderRadius.circular(30),
border: Border.all(color: AppColor.textWhite.withOpacity(0.2)),
),
child: Row(
children: [
Expanded(
child: _buildTab(
icon: Icons.inventory_2_rounded,
label: context.lang.product,
isSelected: selectedTabIndex == 0,
onTap: () => onTabChanged(0),
),
),
Expanded(
child: _buildTab(
icon: Icons.restaurant_menu_rounded,
label: context.lang.ingredients,
isSelected: selectedTabIndex == 1,
onTap: () => onTabChanged(1),
),
),
],
),
);
}
Widget _buildTab({
required IconData icon,
required String label,
required bool isSelected,
required VoidCallback onTap,
}) {
return GestureDetector(
onTap: onTap,
child: Container(
padding: const EdgeInsets.symmetric(vertical: 10),
decoration: BoxDecoration(
color: isSelected ? AppColor.white : Colors.transparent,
borderRadius: BorderRadius.circular(26),
),
child: Center(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
icon,
size: 16,
color: isSelected ? AppColor.textPrimary : AppColor.textWhite,
),
const SizedBox(width: 6),
Text(
label,
style: AppStyle.md.copyWith(
color: isSelected ? AppColor.textPrimary : AppColor.textWhite,
fontWeight: FontWeight.w600,
),
),
],
),
),
),
);
}
void _showDatePicker(BuildContext context) {
DateRangePickerBottomSheet.show(
context: context,
primaryColor: AppColor.primary,
initialStartDate: state.dateFrom,
initialEndDate: state.dateTo,
maxDate: DateTime.now(),
onChanged: (startDate, endDate) {
if (startDate != null && endDate != null) {
onDateRangeChanged?.call(startDate, endDate);
}
},
);
}
Widget _buildHeaderValueShimmer() {
return Shimmer.fromColors(
baseColor: AppColor.textWhite.withOpacity(0.3),
highlightColor: AppColor.textWhite.withOpacity(0.6),
child: Container(
width: 200,
height: 36,
decoration: BoxDecoration(
color: AppColor.textWhite.withOpacity(0.3),
borderRadius: BorderRadius.circular(8),
),
),
);
}
Widget _buildHeaderChipsShimmer() {
return Row(
children: List.generate(
3,
(index) => Padding(
padding: const EdgeInsets.only(right: 8),
child: Shimmer.fromColors(
baseColor: AppColor.textWhite.withOpacity(0.15),
highlightColor: AppColor.textWhite.withOpacity(0.3),
child: Container(
width: 90,
height: 32,
decoration: BoxDecoration(
color: AppColor.textWhite.withOpacity(0.15),
borderRadius: BorderRadius.circular(20),
),
),
),
),
),
);
}
Widget _buildHeaderChips(BuildContext context) {
final summary = state.inventoryAnalytic.summary;
if (selectedTabIndex == 0) {
return Wrap(
spacing: 8,
runSpacing: 8,
children: [
_buildChip('${summary.totalProducts} ${context.lang.product}'),
_buildChip('${summary.lowStockProducts} ${context.lang.low_stock}'),
_buildChip('${summary.zeroStockProducts} ${context.lang.zero_stock}'),
],
);
} else {
return Wrap(
spacing: 8,
runSpacing: 8,
children: [
_buildChip('${summary.totalIngredients} ${context.lang.ingredients}'),
_buildChip(
'${summary.lowStockIngredients} ${context.lang.low_stock}',
),
_buildChip(
'${summary.zeroStockIngredients} ${context.lang.zero_stock}',
),
],
);
}
}
Widget _buildChip(String label) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
decoration: BoxDecoration(
color: AppColor.textWhite.withOpacity(0.15),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: AppColor.textWhite.withOpacity(0.25)),
),
child: Text(
label,
style: AppStyle.sm.copyWith(
color: AppColor.textWhite,
fontWeight: FontWeight.w600,
fontSize: 12,
),
),
);
}
String _formatDateRange(DateTime from, DateTime to) {
const months = [
'Jan',
'Feb',
'Mar',
'Apr',
'Mei',
'Jun',
'Jul',
'Agu',
'Sep',
'Okt',
'Nov',
'Des',
];
if (from.year == to.year && from.month == to.month && from.day == to.day) {
return '${from.day} ${months[from.month - 1]} ${from.year}';
}
return '${from.day} ${months[from.month - 1]} - ${to.day} ${months[to.month - 1]} ${to.year}';
}
}
@@ -0,0 +1,638 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import '../../../../common/extension/extension.dart';
import '../../../../common/theme/theme.dart';
import '../../../../domain/analytic/analytic.dart';
import '../../../components/spacer/spacer.dart';
class InventoryStockReport extends StatelessWidget {
final InventoryAnalytic inventoryAnalytic;
final int selectedTabIndex;
const InventoryStockReport({
super.key,
required this.inventoryAnalytic,
required this.selectedTabIndex,
});
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.all(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: [
// Title row
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
selectedTabIndex == 0
? context.lang.product
: context.lang.ingredients,
style: AppStyle.lg.copyWith(
fontWeight: FontWeight.w700,
color: AppColor.textPrimary,
),
),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 4,
),
decoration: BoxDecoration(
color: AppColor.primary.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Text(
selectedTabIndex == 0
? '${inventoryAnalytic.products.length} item'
: '${inventoryAnalytic.ingredients.length} item',
style: AppStyle.sm.copyWith(
color: AppColor.primary,
fontWeight: FontWeight.w600,
),
),
),
],
),
const SpaceHeight(16),
// Summary stats row
_buildSummaryStats(context),
const SpaceHeight(16),
const Divider(height: 1, color: AppColor.borderLight),
const SpaceHeight(12),
// Table header
_buildTableHeader(context),
const SpaceHeight(8),
// Items list
if (selectedTabIndex == 0)
...inventoryAnalytic.products.map(
(product) => _buildProductRow(context, product),
)
else
...inventoryAnalytic.ingredients.map(
(ingredient) => _buildIngredientRow(context, ingredient),
),
const SpaceHeight(12),
// Footer totals
_buildFooterTotals(context),
],
),
);
}
Widget _buildSummaryStats(BuildContext context) {
final summary = inventoryAnalytic.summary;
if (selectedTabIndex == 0) {
return Row(
children: [
Expanded(
child: _buildMiniStat(
context.lang.total_sold,
summary.totalSoldProducts.toString(),
AppColor.success,
Icons.check_circle_rounded,
),
),
const SpaceWidth(8),
Expanded(
child: _buildMiniStat(
context.lang.low_stock,
summary.lowStockProducts.toString(),
AppColor.warning,
Icons.warning_rounded,
),
),
const SpaceWidth(8),
Expanded(
child: _buildMiniStat(
context.lang.zero_stock,
summary.zeroStockProducts.toString(),
AppColor.error,
Icons.error_rounded,
),
),
],
);
} else {
return Row(
children: [
Expanded(
child: _buildMiniStat(
context.lang.total_sold,
summary.totalSoldIngredients.toString(),
AppColor.success,
Icons.check_circle_rounded,
),
),
const SpaceWidth(8),
Expanded(
child: _buildMiniStat(
context.lang.low_stock,
summary.lowStockIngredients.toString(),
AppColor.warning,
Icons.warning_rounded,
),
),
const SpaceWidth(8),
Expanded(
child: _buildMiniStat(
context.lang.zero_stock,
summary.zeroStockIngredients.toString(),
AppColor.error,
Icons.error_rounded,
),
),
],
);
}
}
Widget _buildMiniStat(
String label,
String value,
Color color,
IconData icon,
) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
decoration: BoxDecoration(
color: color.withOpacity(0.08),
borderRadius: BorderRadius.circular(12),
),
child: Column(
children: [
Icon(icon, size: 18, color: color),
const SpaceHeight(4),
Text(
value,
style: AppStyle.lg.copyWith(
fontWeight: FontWeight.w800,
color: color,
),
),
const SpaceHeight(2),
Text(
label,
style: AppStyle.xs.copyWith(
color: color.withOpacity(0.8),
fontWeight: FontWeight.w500,
fontSize: 10,
),
textAlign: TextAlign.center,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
);
}
Widget _buildTableHeader(BuildContext context) {
if (selectedTabIndex == 0) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
children: [
Expanded(
flex: 4,
child: Text(
context.lang.product,
style: AppStyle.sm.copyWith(
fontWeight: FontWeight.w600,
color: AppColor.textSecondary,
),
),
),
Expanded(
flex: 2,
child: Text(
context.lang.stock,
textAlign: TextAlign.center,
style: AppStyle.sm.copyWith(
fontWeight: FontWeight.w600,
color: AppColor.textSecondary,
),
),
),
Expanded(
flex: 2,
child: Text(
context.lang.in_text,
textAlign: TextAlign.center,
style: AppStyle.sm.copyWith(
fontWeight: FontWeight.w600,
color: AppColor.textSecondary,
),
),
),
Expanded(
flex: 2,
child: Text(
context.lang.out_text,
textAlign: TextAlign.center,
style: AppStyle.sm.copyWith(
fontWeight: FontWeight.w600,
color: AppColor.textSecondary,
),
),
),
],
),
);
} else {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
children: [
Expanded(
flex: 4,
child: Text(
context.lang.ingredients,
style: AppStyle.sm.copyWith(
fontWeight: FontWeight.w600,
color: AppColor.textSecondary,
),
),
),
Expanded(
flex: 2,
child: Text(
context.lang.stock,
textAlign: TextAlign.center,
style: AppStyle.sm.copyWith(
fontWeight: FontWeight.w600,
color: AppColor.textSecondary,
),
),
),
Expanded(
flex: 2,
child: Text(
context.lang.in_text,
textAlign: TextAlign.center,
style: AppStyle.sm.copyWith(
fontWeight: FontWeight.w600,
color: AppColor.textSecondary,
),
),
),
Expanded(
flex: 2,
child: Text(
context.lang.out_text,
textAlign: TextAlign.center,
style: AppStyle.sm.copyWith(
fontWeight: FontWeight.w600,
color: AppColor.textSecondary,
),
),
),
],
),
);
}
}
Widget _buildProductRow(BuildContext context, InventoryProduct product) {
final statusColor = product.isZeroStock
? AppColor.error
: product.isLowStock
? AppColor.warning
: AppColor.textPrimary;
return Container(
padding: const EdgeInsets.symmetric(vertical: 10),
decoration: const BoxDecoration(
border: Border(
bottom: BorderSide(color: AppColor.borderLight, width: 0.5),
),
),
child: Row(
children: [
// Product name + category
Expanded(
flex: 4,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
product.productName,
style: AppStyle.md.copyWith(
fontWeight: FontWeight.w600,
color: AppColor.textPrimary,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SpaceHeight(2),
Row(
children: [
if (product.isZeroStock || product.isLowStock)
Container(
width: 6,
height: 6,
margin: const EdgeInsets.only(right: 4),
decoration: BoxDecoration(
color: statusColor,
shape: BoxShape.circle,
),
),
Flexible(
child: Text(
product.categoryName,
style: AppStyle.xs.copyWith(
color: AppColor.textSecondary,
fontWeight: FontWeight.w400,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
),
],
),
),
// Stock
Expanded(
flex: 2,
child: Text(
NumberFormat('#,###', 'id_ID').format(product.quantity),
textAlign: TextAlign.center,
style: AppStyle.md.copyWith(
fontWeight: FontWeight.w600,
color: statusColor,
),
),
),
// In
Expanded(
flex: 2,
child: Text(
product.totalIn > 0
? '+${NumberFormat('#,###', 'id_ID').format(product.totalIn)}'
: '-',
textAlign: TextAlign.center,
style: AppStyle.md.copyWith(
fontWeight: FontWeight.w500,
color: product.totalIn > 0
? AppColor.success
: AppColor.textSecondary,
),
),
),
// Out
Expanded(
flex: 2,
child: Text(
product.totalOut > 0
? '-${NumberFormat('#,###', 'id_ID').format(product.totalOut)}'
: '-',
textAlign: TextAlign.center,
style: AppStyle.md.copyWith(
fontWeight: FontWeight.w500,
color: product.totalOut > 0
? AppColor.error
: AppColor.textSecondary,
),
),
),
],
),
);
}
Widget _buildIngredientRow(
BuildContext context,
InventoryIngredient ingredient,
) {
final statusColor = ingredient.isZeroStock
? AppColor.error
: ingredient.isLowStock
? AppColor.warning
: AppColor.textPrimary;
return Container(
padding: const EdgeInsets.symmetric(vertical: 10),
decoration: const BoxDecoration(
border: Border(
bottom: BorderSide(color: AppColor.borderLight, width: 0.5),
),
),
child: Row(
children: [
// Ingredient name + unit
Expanded(
flex: 4,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
ingredient.ingredientName,
style: AppStyle.md.copyWith(
fontWeight: FontWeight.w600,
color: AppColor.textPrimary,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SpaceHeight(2),
Row(
children: [
if (ingredient.isZeroStock || ingredient.isLowStock)
Container(
width: 6,
height: 6,
margin: const EdgeInsets.only(right: 4),
decoration: BoxDecoration(
color: statusColor,
shape: BoxShape.circle,
),
),
Flexible(
child: Text(
ingredient.unitName,
style: AppStyle.xs.copyWith(
color: AppColor.textSecondary,
fontWeight: FontWeight.w400,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
),
],
),
),
// Stock
Expanded(
flex: 2,
child: Text(
NumberFormat('#,###', 'id_ID').format(ingredient.quantity),
textAlign: TextAlign.center,
style: AppStyle.md.copyWith(
fontWeight: FontWeight.w600,
color: statusColor,
),
),
),
// In
Expanded(
flex: 2,
child: Text(
ingredient.totalIn > 0
? '+${NumberFormat('#,###', 'id_ID').format(ingredient.totalIn)}'
: '-',
textAlign: TextAlign.center,
style: AppStyle.md.copyWith(
fontWeight: FontWeight.w500,
color: ingredient.totalIn > 0
? AppColor.success
: AppColor.textSecondary,
),
),
),
// Out
Expanded(
flex: 2,
child: Text(
ingredient.totalOut > 0
? '-${NumberFormat('#,###', 'id_ID').format(ingredient.totalOut)}'
: '-',
textAlign: TextAlign.center,
style: AppStyle.md.copyWith(
fontWeight: FontWeight.w500,
color: ingredient.totalOut > 0
? AppColor.error
: AppColor.textSecondary,
),
),
),
],
),
);
}
Widget _buildFooterTotals(BuildContext context) {
int totalStock;
int totalIn;
int totalOut;
if (selectedTabIndex == 0) {
totalStock = inventoryAnalytic.products.fold<int>(
0,
(sum, item) => sum + item.quantity,
);
totalIn = inventoryAnalytic.products.fold<int>(
0,
(sum, item) => sum + item.totalIn,
);
totalOut = inventoryAnalytic.products.fold<int>(
0,
(sum, item) => sum + item.totalOut,
);
} else {
totalStock = inventoryAnalytic.ingredients.fold<int>(
0,
(sum, item) => sum + item.quantity,
);
totalIn = inventoryAnalytic.ingredients.fold<int>(
0,
(sum, item) => sum + item.totalIn,
);
totalOut = inventoryAnalytic.ingredients.fold<int>(
0,
(sum, item) => sum + item.totalOut,
);
}
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 14),
decoration: BoxDecoration(
color: AppColor.primary.withOpacity(0.08),
borderRadius: BorderRadius.circular(12),
),
child: Row(
children: [
Expanded(
flex: 4,
child: Text(
'TOTAL',
style: AppStyle.md.copyWith(
fontWeight: FontWeight.w700,
color: AppColor.primary,
),
),
),
Expanded(
flex: 2,
child: Text(
NumberFormat('#,###', 'id_ID').format(totalStock),
textAlign: TextAlign.center,
style: AppStyle.md.copyWith(
fontWeight: FontWeight.w700,
color: AppColor.primary,
),
),
),
Expanded(
flex: 2,
child: Text(
'+${NumberFormat('#,###', 'id_ID').format(totalIn)}',
textAlign: TextAlign.center,
style: AppStyle.md.copyWith(
fontWeight: FontWeight.w700,
color: AppColor.success,
),
),
),
Expanded(
flex: 2,
child: Text(
'-${NumberFormat('#,###', 'id_ID').format(totalOut)}',
textAlign: TextAlign.center,
style: AppStyle.md.copyWith(
fontWeight: FontWeight.w700,
color: AppColor.error,
),
),
),
],
),
);
}
}
+7 -1
View File
@@ -11,7 +11,13 @@ class MainPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return AutoTabsRouter.pageView(
routes: [HomeRoute(), OrderRoute(), ReportRoute(), ProfileRoute()],
routes: [
HomeRoute(),
OrderRoute(),
ExclusiveSummaryRoute(),
InventoryRoute(),
ProfileRoute(),
],
physics: const NeverScrollableScrollPhysics(),
builder: (context, child, pageController) {
final tabsRouter = AutoTabsRouter.of(context);
@@ -40,6 +40,11 @@ class _MainBottomNavbarState extends State<MainBottomNavbar> {
label: context.lang.report,
tooltip: context.lang.report,
),
BottomNavigationBarItem(
icon: HugeIcon(icon: HugeIcons.strokeRoundedPackage),
label: context.lang.stock,
tooltip: context.lang.stock,
),
BottomNavigationBarItem(
icon: HugeIcon(icon: HugeIcons.strokeRoundedUser),
label: context.lang.profile,
@@ -49,7 +49,7 @@ class _OrderPageState extends State<OrderPage> with TickerProviderStateMixin {
final ScrollController _scrollController = ScrollController();
// Filter state
final List<String> filterOptions = ['All', 'Completed', 'Pending'];
final List<String> filterOptions = ['All', 'Pending', 'Completed'];
@override
void initState() {
@@ -43,23 +43,6 @@ class ProfileAccountInfo extends StatelessWidget {
),
),
ProfileTile(
icon: LineIcons.envelope,
title: context.lang.email,
subtitle: user.email,
showArrow: false,
),
ProfileDivider(),
ProfileTile(
icon: LineIcons.calendarAlt,
title: context.lang.member_since,
subtitle: user.createdAt.toDate,
showArrow: false,
),
ProfileDivider(),
ProfileTile(
icon: LineIcons.userEdit,
title: context.lang.edit_profile,
@@ -46,24 +46,6 @@ class ProfileBusinessSetting extends StatelessWidget {
subtitle: context.lang.outlet_informatio_desc,
onTap: () => context.router.push(OutletInformationRoute()),
),
ProfileDivider(),
ProfileTile(
icon: Icons.people_outline,
title: context.lang.staff_management,
subtitle: context.lang.staff_management_desc,
onTap: () => context.router.push(ComingSoonRoute()),
),
ProfileDivider(),
ProfileTile(
icon: Icons.inventory_2_outlined,
title: context.lang.products,
subtitle: context.lang.manage_your_products,
onTap: () => context.router.push(ProductRoute()),
),
ProfileDivider(),
ProfileTile(
@@ -0,0 +1,238 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:shimmer/shimmer.dart';
import '../../../application/analytic/profit_sharing_detail_loader/profit_sharing_detail_loader_bloc.dart';
import '../../../common/extension/extension.dart';
import '../../../common/theme/theme.dart';
import '../../../domain/analytic/analytic.dart';
import '../../../injection.dart';
import '../../components/spacer/spacer.dart';
import 'widgets/profit_sharing_allocation.dart';
import 'widgets/profit_sharing_detail_header.dart';
import 'widgets/profit_sharing_status.dart';
import 'widgets/profit_sharing_subcategories.dart';
@RoutePage()
class ProfitSharingDetailPage extends StatelessWidget
implements AutoRouteWrapper {
final String parentCategoryId;
final String parentCategoryName;
final DateTime dateFrom;
final DateTime dateTo;
const ProfitSharingDetailPage({
super.key,
required this.parentCategoryId,
required this.parentCategoryName,
required this.dateFrom,
required this.dateTo,
});
@override
Widget wrappedRoute(BuildContext context) => BlocProvider(
create: (_) => getIt<ProfitSharingDetailLoaderBloc>()..add(_fetchEvent()),
child: this,
);
ProfitSharingDetailLoaderEvent _fetchEvent() =>
ProfitSharingDetailLoaderEvent.fetched(
parentCategoryId: parentCategoryId,
dateFrom: dateFrom,
dateTo: dateTo,
);
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppColor.background,
body:
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: 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 _buildShimmer() {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
children: [
_shimmerBox(height: 160),
const SpaceHeight(16),
_shimmerBox(height: 240),
],
),
);
}
Widget _shimmerBox({required double height}) {
return Shimmer.fromColors(
baseColor: Colors.grey[300]!,
highlightColor: Colors.grey[100]!,
child: Container(
height: height,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
),
),
);
}
}
class _SummaryCard extends StatelessWidget {
final ProfitSharingCategory summary;
const _SummaryCard({required this.summary});
@override
Widget build(BuildContext context) {
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.summary,
style: AppStyle.lg.copyWith(
fontWeight: FontWeight.w700,
color: AppColor.textPrimary,
),
),
const SpaceHeight(16),
Row(
children: [
Expanded(
child: ProfitSharingMetric(
label: context.lang.sub_category,
value: '${summary.categoryCount}',
),
),
Expanded(
child: ProfitSharingMetric(
label: context.lang.products,
value: '${summary.productCount}',
),
),
Expanded(
child: ProfitSharingMetric(
label: context.lang.orders,
value: summary.orderCount.thousandFormat,
),
),
],
),
const Padding(
padding: EdgeInsets.symmetric(vertical: 12),
child: Divider(height: 1, color: AppColor.borderLight),
),
Row(
children: [
Expanded(
child: ProfitSharingMetric(
label: context.lang.std_hpp,
value: formatPercent(summary.standardHppPercentage),
),
),
Expanded(
child: ProfitSharingMetric(
label: context.lang.real_hpp,
value: formatPercent(summary.realHppPercentage),
valueColor: statusColor(summary.realHppPercentage),
),
),
Expanded(
child: ProfitSharingMetric(
label: context.lang.gross_profit,
value: summary.grossProfit.currencyFormatRp,
),
),
],
),
],
),
);
}
}
@@ -0,0 +1,203 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
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/theme/theme.dart';
import '../../../injection.dart';
import '../../components/field/date_range_picker_field.dart';
import '../../components/spacer/spacer.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';
@RoutePage()
class ProfitSharingPage extends StatefulWidget implements AutoRouteWrapper {
const ProfitSharingPage({super.key});
@override
State<ProfitSharingPage> createState() => _ProfitSharingPageState();
@override
Widget wrappedRoute(BuildContext context) => BlocProvider(
create: (_) =>
getIt<ProfitSharingLoaderBloc>()
..add(ProfitSharingLoaderEvent.fetched()),
child: this,
);
}
class _ProfitSharingPageState extends State<ProfitSharingPage>
with SingleTickerProviderStateMixin {
late AnimationController _fadeController;
late Animation<double> _fadeAnimation;
@override
void initState() {
super.initState();
_fadeController = AnimationController(
duration: const Duration(milliseconds: 1000),
vsync: this,
);
_fadeAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(parent: _fadeController, curve: Curves.easeIn));
_fadeController.forward();
}
@override
void dispose() {
_fadeController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppColor.background,
body: BlocListener<ProfitSharingLoaderBloc, ProfitSharingLoaderState>(
listenWhen: (previous, current) =>
previous.dateFrom != current.dateFrom ||
previous.dateTo != current.dateTo,
listener: (context, state) {
context.read<ProfitSharingLoaderBloc>().add(
const ProfitSharingLoaderEvent.fetched(),
);
},
child: BlocBuilder<ProfitSharingLoaderBloc, ProfitSharingLoaderState>(
builder: (context, state) {
return RefreshIndicator(
backgroundColor: AppColor.white,
color: AppColor.primary,
onRefresh: () async {
context.read<ProfitSharingLoaderBloc>().add(
const ProfitSharingLoaderEvent.fetched(),
);
await context.read<ProfitSharingLoaderBloc>().stream.firstWhere(
(s) => !s.isFetching,
);
},
child: CustomScrollView(
physics: const AlwaysScrollableScrollPhysics(),
slivers: [
SliverToBoxAdapter(
child: FadeTransition(
opacity: _fadeAnimation,
child: ProfitSharingHeader(state: state),
),
),
SliverToBoxAdapter(
child: FadeTransition(
opacity: _fadeAnimation,
child: Padding(
padding: const EdgeInsets.all(16),
child: DateRangePickerField(
startDate: state.dateFrom,
endDate: state.dateTo,
onChanged: (startDate, endDate) {
if (startDate == null || endDate == null) return;
context.read<ProfitSharingLoaderBloc>().add(
ProfitSharingLoaderEvent.rangeDateChanged(
startDate,
endDate,
),
);
},
),
),
),
),
if (state.isFetching)
SliverToBoxAdapter(child: _buildShimmer())
else ...[
SliverToBoxAdapter(
child: FadeTransition(
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,
),
),
),
),
),
const SliverToBoxAdapter(child: SpaceHeight(16)),
SliverToBoxAdapter(
child: FadeTransition(
opacity: _fadeAnimation,
child: ProfitSharingAllocation(
budget: state.profitSharing.budget,
),
),
),
const SliverToBoxAdapter(child: SpaceHeight(16)),
SliverToBoxAdapter(
child: FadeTransition(
opacity: _fadeAnimation,
child: ProfitSharingPeriods(
state: state,
onPeriodTypeChanged: (type) {
context.read<ProfitSharingLoaderBloc>().add(
ProfitSharingLoaderEvent.periodTypeChanged(type),
);
},
),
),
),
],
const SliverToBoxAdapter(child: SizedBox(height: 100)),
],
),
);
},
),
),
);
}
Widget _buildShimmer() {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
children: [
_shimmerBox(height: 240),
const SpaceHeight(16),
_shimmerBox(height: 300),
],
),
);
}
Widget _shimmerBox({required double height}) {
return Shimmer.fromColors(
baseColor: Colors.grey[300]!,
highlightColor: Colors.grey[100]!,
child: Container(
height: height,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
),
),
);
}
}
@@ -0,0 +1,31 @@
import 'package:intl/intl.dart';
/// Label rentang periode ringkas, contoh: "27 Jul - 2 Agu 2026".
String formatPeriodLabel(DateTime from, DateTime to) {
final dayMonth = DateFormat('d MMM', 'id_ID');
final dayMonthYear = DateFormat('d MMM yyyy', 'id_ID');
if (from.year == to.year && from.month == to.month && from.day == to.day) {
return dayMonthYear.format(from);
}
if (from.year == to.year) {
return '${dayMonth.format(from)} - ${dayMonthYear.format(to)}';
}
return '${dayMonthYear.format(from)} - ${dayMonthYear.format(to)}';
}
/// Label bulan dari format API "2026-08" menjadi "Agustus 2026".
String formatMonthLabel(String month, DateTime fallback) {
final parts = month.split('-');
if (parts.length == 2) {
final year = int.tryParse(parts[0]);
final monthNumber = int.tryParse(parts[1]);
if (year != null && monthNumber != null) {
return DateFormat(
'MMMM yyyy',
'id_ID',
).format(DateTime(year, monthNumber));
}
}
return DateFormat('MMMM yyyy', 'id_ID').format(fallback);
}
@@ -0,0 +1,195 @@
import 'package:flutter/material.dart';
import '../../../../common/extension/extension.dart';
import '../../../../common/theme/theme.dart';
import '../../../../domain/analytic/analytic.dart';
import '../../../components/spacer/spacer.dart';
/// Warna tetap untuk tiap pos bagi hasil supaya konsisten di semua bagian.
class ProfitSharingColor {
static const Color purchase = Color(0xFF2196F3);
static const Color owner = Color(0xFF4CAF50);
static const Color team = Color(0xFFFF9800);
}
class ProfitSharingAllocation extends StatelessWidget {
final ProfitSharingBudget budget;
const ProfitSharingAllocation({super.key, required this.budget});
@override
Widget build(BuildContext context) {
final total = budget.total;
final percentages = budget.percentages;
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.profit_sharing_allocation,
style: AppStyle.lg.copyWith(
fontWeight: FontWeight.w700,
color: AppColor.textPrimary,
),
),
const SpaceHeight(16),
ProfitSharingBar(
purchase: percentages.purchase,
owner: percentages.owner,
team: percentages.team,
),
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,
label: context.lang.share_owner,
percentage: percentages.owner,
amount: total.limitOwner,
),
const Divider(height: 24, color: AppColor.borderLight),
_row(
context: context,
color: ProfitSharingColor.team,
label: context.lang.share_team,
percentage: percentages.team,
amount: total.limitTeam,
),
],
),
);
}
Widget _row({
required BuildContext context,
required Color color,
required String label,
required double percentage,
required int amount,
}) {
return Row(
children: [
Container(
width: 10,
height: 10,
decoration: BoxDecoration(color: color, shape: BoxShape.circle),
),
const SpaceWidth(10),
Text(
label,
style: AppStyle.md.copyWith(
color: AppColor.textPrimary,
fontWeight: FontWeight.w600,
),
),
const SpaceWidth(8),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: color.withOpacity(0.12),
borderRadius: BorderRadius.circular(6),
),
child: Text(
'${percentage.toStringAsFixed(percentage % 1 == 0 ? 0 : 1)}%',
style: AppStyle.xs.copyWith(
color: color,
fontWeight: FontWeight.w700,
),
),
),
const Spacer(),
Flexible(
child: Text(
amount.currencyFormatRp,
textAlign: TextAlign.right,
style: AppStyle.md.copyWith(
color: AppColor.textPrimary,
fontWeight: FontWeight.w700,
),
overflow: TextOverflow.ellipsis,
),
),
],
);
}
}
/// Bar proporsi belanja / owner / tim.
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,
});
@override
Widget build(BuildContext context) {
final total = purchase + owner + team;
if (total <= 0) {
return Container(
height: height,
decoration: BoxDecoration(
color: AppColor.borderLight,
borderRadius: BorderRadius.circular(height),
),
);
}
return ClipRRect(
borderRadius: BorderRadius.circular(height),
child: SizedBox(
height: height,
child: Row(
children: [
_segment(purchase, ProfitSharingColor.purchase),
_segment(owner, ProfitSharingColor.owner),
_segment(team, ProfitSharingColor.team),
].whereType<Widget>().toList(),
),
),
);
}
/// Segmen dilewati saat porsinya 0 supaya Expanded tidak dipakai dengan flex 0.
Widget? _segment(double value, Color color) {
final flex = (value * 100).round();
if (flex <= 0) return null;
return Expanded(
flex: flex,
child: Container(color: color),
);
}
}
@@ -0,0 +1,324 @@
import 'package:flutter/material.dart';
import '../../../../common/extension/extension.dart';
import '../../../../common/theme/theme.dart';
import '../../../../domain/analytic/analytic.dart';
import '../../../components/spacer/spacer.dart';
import 'profit_sharing_status.dart';
/// Ringkasan omzet & HPP per kategori induk (parent category).
/// Tiap kartu bisa diketuk untuk melihat rincian sub kategori & produknya.
class ProfitSharingCategories extends StatelessWidget {
final List<ProfitSharingCategory> categories;
final ValueChanged<ProfitSharingCategory> onCategoryTap;
const ProfitSharingCategories({
super.key,
required this.categories,
required this.onCategoryTap,
});
@override
Widget build(BuildContext context) {
final sorted = [...categories]
..sort((a, b) => b.totalRevenue.compareTo(a.totalRevenue));
final totalRevenue = sorted.fold<int>(0, (sum, e) => sum + e.totalRevenue);
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.parent_category_summary,
style: AppStyle.lg.copyWith(
fontWeight: FontWeight.w700,
color: AppColor.textPrimary,
),
),
const SizedBox(height: 2),
Text(
context.lang.tap_row_for_detail,
style: AppStyle.xs.copyWith(color: AppColor.textSecondary),
),
const SpaceHeight(16),
if (sorted.isEmpty)
Padding(
padding: const EdgeInsets.symmetric(vertical: 24),
child: Center(
child: Text(
context.lang.no_data_yet,
style: AppStyle.sm.copyWith(color: AppColor.textSecondary),
),
),
)
else ...[
...sorted.map(
(category) => Padding(
padding: const EdgeInsets.only(bottom: 12),
child: _CategoryCard(
category: category,
share: totalRevenue == 0
? 0
: category.totalRevenue / totalRevenue,
onTap: () => onCategoryTap(category),
),
),
),
_TotalCard(categories: sorted),
],
],
),
);
}
}
class _CategoryCard extends StatelessWidget {
final ProfitSharingCategory category;
final double share;
final VoidCallback onTap;
const _CategoryCard({
required this.category,
required this.share,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(12),
child: Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: AppColor.background,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppColor.borderLight),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(
category.parentCategoryName,
style: AppStyle.md.copyWith(
color: AppColor.textPrimary,
fontWeight: FontWeight.w700,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
const SpaceWidth(8),
ProfitSharingStatusBadge(
realHppPercentage: category.realHppPercentage,
),
const Icon(
Icons.chevron_right_rounded,
color: AppColor.textSecondary,
size: 20,
),
],
),
const SpaceHeight(8),
Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Expanded(
child: Text(
category.totalRevenue.currencyFormatRp,
style: AppStyle.lg.copyWith(
color: AppColor.primary,
fontWeight: FontWeight.w800,
),
),
),
Text(
formatPercent(share * 100),
style: AppStyle.xs.copyWith(
color: AppColor.textSecondary,
fontWeight: FontWeight.w600,
),
),
],
),
const SpaceHeight(8),
ClipRRect(
borderRadius: BorderRadius.circular(6),
child: LinearProgressIndicator(
value: share.clamp(0.0, 1.0),
minHeight: 6,
backgroundColor: AppColor.borderLight,
valueColor: const AlwaysStoppedAnimation<Color>(
AppColor.primary,
),
),
),
const SpaceHeight(12),
Row(
children: [
Expanded(
child: ProfitSharingMetric(
label: context.lang.qty,
value: category.totalQuantity.thousandFormat,
),
),
Expanded(
child: ProfitSharingMetric(
label: context.lang.products,
value: '${category.productCount}',
),
),
Expanded(
child: ProfitSharingMetric(
label: context.lang.sub_category,
value: '${category.categoryCount}',
),
),
],
),
const Padding(
padding: EdgeInsets.symmetric(vertical: 10),
child: Divider(height: 1, color: AppColor.border),
),
Row(
children: [
Expanded(
child: ProfitSharingMetric(
label: context.lang.std_hpp,
value: formatPercent(category.standardHppPercentage),
),
),
Expanded(
child: ProfitSharingMetric(
label: context.lang.real_hpp,
value: formatPercent(category.realHppPercentage),
valueColor: statusColor(category.realHppPercentage),
),
),
],
),
],
),
),
);
}
}
class _TotalCard extends StatelessWidget {
final List<ProfitSharingCategory> categories;
const _TotalCard({required this.categories});
@override
Widget build(BuildContext context) {
final totalQuantity = categories.fold<int>(
0,
(sum, e) => sum + e.totalQuantity,
);
final totalRevenue = categories.fold<int>(
0,
(sum, e) => sum + e.totalRevenue,
);
final totalStandardHpp = categories.fold<int>(
0,
(sum, e) => sum + e.totalStandardHpp,
);
final totalFifoHpp = categories.fold<int>(
0,
(sum, e) => sum + e.totalFifoHpp,
);
double percentOf(int hpp) =>
totalRevenue == 0 ? 0 : (hpp / totalRevenue) * 100;
return Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: AppColor.primary.withOpacity(0.06),
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(
context.lang.grand_total,
style: AppStyle.md.copyWith(
color: AppColor.textPrimary,
fontWeight: FontWeight.w800,
),
),
),
Text(
totalRevenue.currencyFormatRp,
style: AppStyle.lg.copyWith(
color: AppColor.primary,
fontWeight: FontWeight.w900,
),
),
],
),
const SpaceHeight(12),
Row(
children: [
Expanded(
child: ProfitSharingMetric(
label: context.lang.qty,
value: totalQuantity.thousandFormat,
isBold: true,
),
),
Expanded(
child: ProfitSharingMetric(
label: context.lang.std_hpp,
value: formatPercent(percentOf(totalStandardHpp)),
isBold: true,
),
),
Expanded(
child: ProfitSharingMetric(
label: context.lang.real_hpp,
value: formatPercent(percentOf(totalFifoHpp)),
isBold: true,
valueColor: statusColor(percentOf(totalFifoHpp)),
),
),
],
),
],
),
);
}
}
@@ -0,0 +1,195 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:shimmer/shimmer.dart';
import '../../../../application/analytic/profit_sharing_detail_loader/profit_sharing_detail_loader_bloc.dart';
import '../../../../common/extension/extension.dart';
import '../../../../common/theme/theme.dart';
import '../../../components/spacer/spacer.dart';
import 'period_label.dart';
import 'profit_sharing_status.dart';
class ProfitSharingDetailHeader extends StatelessWidget {
final ProfitSharingDetailLoaderState state;
final String fallbackTitle;
const ProfitSharingDetailHeader({
super.key,
required this.state,
required this.fallbackTitle,
});
@override
Widget build(BuildContext context) {
final detail = state.detail;
final summary = detail.summary;
final title = detail.parentCategoryName.isNotEmpty
? detail.parentCategoryName
: fallbackTitle;
return Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: AppColor.primaryGradient,
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(24),
bottomRight: Radius.circular(24),
),
),
child: SafeArea(
bottom: false,
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
GestureDetector(
onTap: () => context.router.maybePop(),
child: Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: AppColor.textWhite.withOpacity(0.15),
borderRadius: BorderRadius.circular(12),
),
child: const Icon(
Icons.chevron_left_rounded,
color: AppColor.textWhite,
size: 24,
),
),
),
const SpaceWidth(12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: AppStyle.xl.copyWith(
color: AppColor.textWhite,
fontWeight: FontWeight.w700,
fontSize: 20,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 2),
Text(
formatPeriodLabel(state.dateFrom, state.dateTo),
style: AppStyle.sm.copyWith(
color: AppColor.textWhite.withOpacity(0.75),
fontSize: 12,
),
),
],
),
),
],
),
const SpaceHeight(24),
if (state.isFetching)
_shimmerBox(width: 220, height: 36)
else
Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Expanded(
child: Text(
summary.totalRevenue.currencyFormatRp,
style: AppStyle.h1.copyWith(
color: AppColor.textWhite,
fontWeight: FontWeight.w900,
fontSize: 30,
),
),
),
ProfitSharingStatusBadge(
realHppPercentage: summary.realHppPercentage,
onDarkBackground: true,
),
],
),
const SpaceHeight(4),
Text(
context.lang.total_revenue,
style: AppStyle.sm.copyWith(
color: AppColor.textWhite.withOpacity(0.75),
fontSize: 13,
),
),
const SpaceHeight(16),
if (state.isFetching)
Row(
children: [
_shimmerBox(width: 120, height: 32, radius: 20),
const SpaceWidth(8),
_shimmerBox(width: 120, height: 32, radius: 20),
],
)
else
Wrap(
spacing: 8,
runSpacing: 8,
children: [
_chip(
'${context.lang.qty} ${summary.totalQuantity.thousandFormat}',
),
_chip(context.lang.order_count_label(summary.orderCount)),
],
),
],
),
),
),
);
}
Widget _chip(String label) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
decoration: BoxDecoration(
color: AppColor.textWhite.withOpacity(0.15),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: AppColor.textWhite.withOpacity(0.25)),
),
child: Text(
label,
style: AppStyle.sm.copyWith(
color: AppColor.textWhite,
fontWeight: FontWeight.w600,
fontSize: 12,
),
),
);
}
Widget _shimmerBox({
required double width,
required double height,
double radius = 8,
}) {
return Shimmer.fromColors(
baseColor: AppColor.textWhite.withOpacity(0.3),
highlightColor: AppColor.textWhite.withOpacity(0.6),
child: Container(
width: width,
height: height,
decoration: BoxDecoration(
color: AppColor.textWhite.withOpacity(0.3),
borderRadius: BorderRadius.circular(radius),
),
),
);
}
}

Some files were not shown because too many files have changed in this diff Show More