Compare commits

..
42 Commits
Author SHA1 Message Date
Efril b98462ee8c config: logo, app name
Build & Deploy iOS to TestFlight / build-and-deploy (push) Waiting to run
2026-06-23 21:34:41 +07:00
Efril 83f4e065ed feat: update home ui 2026-06-23 21:27:27 +07:00
efrilm e236d811ce feat: exclusive summary
Build & Deploy iOS to TestFlight / build-and-deploy (push) Has been cancelled
2026-06-22 14:44:03 +07:00
efrilm 0b70194d8e feat: report purchasing
Build & Deploy iOS to TestFlight / build-and-deploy (push) Waiting to run
2026-06-22 14:30:38 +07:00
efrilm c1cefb122b build number
Build & Deploy iOS to TestFlight / build-and-deploy (push) Has been cancelled
2026-05-29 22:04:38 +07:00
efrilm ded5516bb1 update
Build & Deploy iOS to TestFlight / build-and-deploy (push) Waiting to run
2026-05-29 21:56:46 +07:00
efrilm 77651e2e95 update 2026-05-29 21:56:29 +07:00
efrilm 0ae599d8f8 update
Build & Deploy iOS to TestFlight / build-and-deploy (push) Waiting to run
2026-05-29 19:45:34 +07:00
efrilm 36ecf95ef2 update
Build & Deploy iOS to TestFlight / build-and-deploy (push) Waiting to run
2026-05-29 19:44:26 +07:00
efrilm 9dbc092313 update version
Build & Deploy iOS to TestFlight / build-and-deploy (push) Waiting to run
2026-05-29 19:17:36 +07:00
efrilm ad11a9e43f update os version
Build & Deploy iOS to TestFlight / build-and-deploy (push) Waiting to run
2026-05-29 19:03:42 +07:00
efrilm 20a9c65229 update via transporter
Build & Deploy iOS to TestFlight / build-and-deploy (push) Waiting to run
2026-05-29 18:54:29 +07:00
efrilm c03cfdfa80 update via transporter
Build & Deploy iOS to TestFlight / build-and-deploy (push) Waiting to run
2026-05-29 18:39:48 +07:00
efrilm c69a84569b update workflow archieve
Build & Deploy iOS to TestFlight / build-and-deploy (push) Waiting to run
2026-05-29 18:30:44 +07:00
efrilm d99dae0ca9 update
Build & Deploy iOS to TestFlight / build-and-deploy (push) Waiting to run
2026-05-29 18:16:57 +07:00
efrilm 5a0f37fd3c update firebase crashlytic
Build & Deploy iOS to TestFlight / build-and-deploy (push) Waiting to run
2026-05-29 18:06:08 +07:00
efrilm e5c92b26c5 update workflow
Build & Deploy iOS to TestFlight / build-and-deploy (push) Waiting to run
2026-05-29 17:56:51 +07:00
efrilm 7955ebb7d1 update flutter futter v
Build & Deploy iOS to TestFlight / build-and-deploy (push) Waiting to run
2026-05-29 17:47:20 +07:00
efrilm 508b72a52e update pubspec
Build & Deploy iOS to TestFlight / build-and-deploy (push) Waiting to run
2026-05-29 17:44:14 +07:00
efrilm 8d982c468f update
Build & Deploy iOS to TestFlight / build-and-deploy (push) Waiting to run
2026-05-29 17:41:38 +07:00
Efril 6ebfae9d5b Merge branch 'main' of https://gits.altru.id/apksel-dev/apskel-owner-flutter
Build & Deploy iOS to TestFlight / build-and-deploy (push) Waiting to run
2026-05-29 17:37:52 +07:00
Efril 926e45170d update workflow 2026-05-29 17:37:25 +07:00
efrilm f9cbd16b28 update 2026-05-29 16:09:48 +07:00
Efril 3ae2e93531 update workflow 2026-05-29 16:00:38 +07:00
Efril a182a3504c Merge branch 'main' of https://gits.altru.id/apksel-dev/apskel-owner-flutter 2026-05-29 15:51:43 +07:00
Efril 88972b28b5 update pod 2026-05-29 15:51:02 +07:00
Efril 2c8e9a3fb4 update 2026-05-29 15:12:05 +07:00
efrilm 086c17a217 add crashlytic 2026-05-25 19:54:04 +07:00
efrilm 9a7681680d update apns 2026-05-25 19:32:38 +07:00
Efril 823e009121 purchasing 2026-05-19 23:33:12 +07:00
Efril 602647ff26 Merge branch 'main' of https://gits.altru.id/apksel-dev/apskel-owner-flutter 2026-05-19 23:08:20 +07:00
Efril d9a553708b purchasing analytic repo 2026-05-19 23:08:01 +07:00
efrilm aaa8eba31b update workflow 2026-05-15 13:44:06 +07:00
efrilm 31a82e14cb update workflows 2026-05-15 13:36:13 +07:00
Efril e88eff28f0 update 2026-05-15 13:26:53 +07:00
Efril 720c63b6c6 Merge branch 'main' of https://github.com/efrilm/apskel-owner 2026-05-15 13:21:09 +07:00
Efril ca2cc8bf6b update ios 2026-05-15 13:19:20 +07:00
efrilm d56c3f89db update workflow 2026-05-15 13:08:05 +07:00
efrilm f7f5682924 update workflow 2026-05-15 13:02:41 +07:00
efrilm b51749a01b add github actions ios build 2026-05-15 12:58:24 +07:00
aefril 5fd870c507 Merge pull request 'git push' (#2) from dev into main
Reviewed-on: #2
2026-05-15 05:53:32 +00:00
aefril d8d8fd9d16 Merge pull request 'dev' (#1) from dev into main
Reviewed-on: #1
2026-05-15 05:52:25 +00:00
123 changed files with 15332 additions and 2296 deletions
+168
View File
@@ -0,0 +1,168 @@
name: Build & Deploy iOS to TestFlight
on:
push:
branches:
- main
workflow_dispatch:
jobs:
build-and-deploy:
runs-on: macos-26
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
channel: "stable"
cache: true
- name: Install Dependencies
run: flutter pub get
# ── Code Signing Setup ──────────────────────────────────────────────────
- name: Install Apple Certificate
env:
BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }}
P12_PASSWORD: ${{ secrets.P12_PASSWORD }}
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
run: |
CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12
echo -n "$BUILD_CERTIFICATE_BASE64" | base64 --decode -o $CERTIFICATE_PATH
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security import $CERTIFICATE_PATH -P "$P12_PASSWORD" -A \
-t cert -f pkcs12 -k $KEYCHAIN_PATH
security set-key-partition-list -S apple-tool:,apple: \
-s -k "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security list-keychain -d user -s $KEYCHAIN_PATH
- name: Install Provisioning Profile
env:
BUILD_PROVISION_PROFILE_BASE64: ${{ secrets.BUILD_PROVISION_PROFILE_BASE64 }}
run: |
PP_PATH=$RUNNER_TEMP/build_pp.mobileprovision
echo -n "$BUILD_PROVISION_PROFILE_BASE64" | base64 --decode -o $PP_PATH
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
# Xcode requires the filename to be the profile's UUID
UUID=$(security cms -D -i $PP_PATH | plutil -extract UUID raw -)
cp $PP_PATH ~/Library/MobileDevice/Provisioning\ Profiles/$UUID.mobileprovision
echo "Installed provisioning profile UUID: $UUID"
# ── Disable SPM ─────────────────────────────────────────────────────────
- name: Disable Swift Package Manager
run: flutter config --no-enable-swift-package-manager
# ── CocoaPods ───────────────────────────────────────────────────────────
- name: Install CocoaPods Dependencies
run: |
cd ios
rm -f Podfile.lock
pod install --repo-update
# ── Build & Archive ─────────────────────────────────────────────────────
- name: Build iOS (no codesign)
run: flutter build ios --release --no-codesign
- name: Re-run pod install to apply Podfile signing settings
run: |
cd ios
pod install
- name: Archive with Xcode
env:
TEAM_ID: ${{ secrets.TEAM_ID }}
run: |
cat > $RUNNER_TEMP/signing.xcconfig << 'XCCONFIG'
CODE_SIGN_STYLE = Manual
CODE_SIGN_IDENTITY = iPhone Distribution
PROVISIONING_PROFILE_SPECIFIER = Enaklo Owner App Store
CODE_SIGNING_REQUIRED = YES
XCCONFIG
xcodebuild archive \
-workspace ios/Runner.xcworkspace \
-scheme Runner \
-configuration Release \
-archivePath $RUNNER_TEMP/Runner.xcarchive \
-destination "generic/platform=iOS" \
-xcconfig $RUNNER_TEMP/signing.xcconfig \
DEVELOPMENT_TEAM="$TEAM_ID"
- name: Export IPA
run: |
xcodebuild -exportArchive \
-archivePath $RUNNER_TEMP/Runner.xcarchive \
-exportPath $RUNNER_TEMP/export \
-exportOptionsPlist ios/ExportOptions.plist
mkdir -p build/ios/ipa
cp $RUNNER_TEMP/export/*.ipa build/ios/ipa/
- name: Upload dSYMs to Crashlytics
env:
GOOGLE_SERVICE_INFO_PLIST: ${{ secrets.GOOGLE_SERVICE_INFO_PLIST }}
run: |
# Find the Crashlytics upload-symbols binary from Pods
UPLOAD_SYMBOLS=$(find ios/Pods -name "upload-symbols" -type f | head -1)
if [ -z "$UPLOAD_SYMBOLS" ]; then
echo "upload-symbols binary not found, skipping dSYM upload"
exit 0
fi
# Write GoogleService-Info.plist from secret
echo -n "$GOOGLE_SERVICE_INFO_PLIST" | base64 --decode > $RUNNER_TEMP/GoogleService-Info.plist
# Upload dSYMs from the xcarchive
"$UPLOAD_SYMBOLS" \
-gsp $RUNNER_TEMP/GoogleService-Info.plist \
-p ios \
$RUNNER_TEMP/Runner.xcarchive/dSYMs
# ── Upload to TestFlight ────────────────────────────────────────────────
- name: Upload to TestFlight via Transporter
env:
APPLE_ID: ${{ secrets.APPLE_ID }}
APP_SPECIFIC_PASSWORD: ${{ secrets.APP_SPECIFIC_PASSWORD }}
run: |
IPA_PATH=$(find build/ios/ipa -name "*.ipa" | head -1)
echo "Uploading: $IPA_PATH"
xcrun altool --upload-app \
--type ios \
--file "$IPA_PATH" \
--username "$APPLE_ID" \
--password "$APP_SPECIFIC_PASSWORD" \
--verbose
# ── Cleanup ─────────────────────────────────────────────────────────────
- name: Clean Up Keychain and Provisioning Profile
if: ${{ always() }}
run: |
security delete-keychain $RUNNER_TEMP/app-signing.keychain-db || true
rm -rf ~/Library/MobileDevice/Provisioning\ Profiles/*.mobileprovision || true
# ── Artifact ────────────────────────────────────────────────────────────
- name: Upload IPA as Artifact
if: always()
uses: actions/upload-artifact@v4
with:
name: ios-ipa
path: build/ios/ipa/*.ipa
retention-days: 7
+1 -1
View File
@@ -13,7 +13,7 @@
<uses-permission android:name="android.permission.WAKE_LOCK"/> <uses-permission android:name="android.permission.WAKE_LOCK"/>
<application <application
android:label="Enaklo Owner" android:label="Grow Food"
android:name="${applicationName}" android:name="${applicationName}"
android:icon="@mipmap/launcher_icon"> android:icon="@mipmap/launcher_icon">
<activity <activity
Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 203 KiB

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 27 KiB

+4
View File
@@ -1,3 +1,7 @@
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true android.useAndroidX=true
android.enableJetifier=true android.enableJetifier=true
# This builtInKotlin flag was added automatically by Flutter migrator
android.builtInKotlin=false
# This newDsl flag was added automatically by Flutter migrator
android.newDsl=false
Binary file not shown.

After

Width:  |  Height:  |  Size: 498 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 952 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

After

Width:  |  Height:  |  Size: 2.0 MiB

+36
View File
@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<!-- Ganti "app-store-connect" jika perlu: ad-hoc, enterprise, development -->
<key>method</key>
<string>app-store-connect</string>
<!-- Team ID dari Apple Developer account -->
<key>teamID</key>
<string>5TRC3M8UZG</string>
<!-- Signing style: manual (pakai provisioning profile eksplisit) -->
<key>signingStyle</key>
<string>manual</string>
<!-- Provisioning profiles: key = bundle ID, value = nama profile -->
<key>provisioningProfiles</key>
<dict>
<key>com.apskel.enaklo</key>
<string>Enaklo Owner App Store</string>
</dict>
<key>stripSwiftSymbols</key>
<true/>
<key>uploadBitcode</key>
<false/>
<key>uploadSymbols</key>
<true/>
<key>compileBitcode</key>
<false/>
</dict>
</plist>
+8
View File
@@ -39,5 +39,13 @@ end
post_install do |installer| post_install do |installer|
installer.pods_project.targets.each do |target| installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target) flutter_additional_ios_build_settings(target)
target.build_configurations.each do |config|
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '14.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'
config.build_settings['CODE_SIGNING_REQUIRED'] = 'NO'
config.build_settings['CODE_SIGN_IDENTITY'] = ''
end
end end
end end
+45 -1
View File
@@ -5,12 +5,19 @@ PODS:
- Flutter - Flutter
- Firebase/CoreOnly (10.25.0): - Firebase/CoreOnly (10.25.0):
- FirebaseCore (= 10.25.0) - FirebaseCore (= 10.25.0)
- Firebase/Crashlytics (10.25.0):
- Firebase/CoreOnly
- FirebaseCrashlytics (~> 10.25.0)
- Firebase/Messaging (10.25.0): - Firebase/Messaging (10.25.0):
- Firebase/CoreOnly - Firebase/CoreOnly
- FirebaseMessaging (~> 10.25.0) - FirebaseMessaging (~> 10.25.0)
- firebase_core (2.32.0): - firebase_core (2.32.0):
- Firebase/CoreOnly (= 10.25.0) - Firebase/CoreOnly (= 10.25.0)
- Flutter - Flutter
- firebase_crashlytics (3.5.7):
- Firebase/Crashlytics (= 10.25.0)
- firebase_core
- Flutter
- firebase_messaging (14.7.10): - firebase_messaging (14.7.10):
- Firebase/Messaging (= 10.25.0) - Firebase/Messaging (= 10.25.0)
- firebase_core - firebase_core
@@ -19,8 +26,19 @@ PODS:
- FirebaseCoreInternal (~> 10.0) - FirebaseCoreInternal (~> 10.0)
- GoogleUtilities/Environment (~> 7.12) - GoogleUtilities/Environment (~> 7.12)
- GoogleUtilities/Logger (~> 7.12) - GoogleUtilities/Logger (~> 7.12)
- FirebaseCoreExtension (10.29.0):
- FirebaseCore (~> 10.0)
- FirebaseCoreInternal (10.29.0): - FirebaseCoreInternal (10.29.0):
- "GoogleUtilities/NSData+zlib (~> 7.8)" - "GoogleUtilities/NSData+zlib (~> 7.8)"
- FirebaseCrashlytics (10.25.0):
- FirebaseCore (~> 10.5)
- FirebaseInstallations (~> 10.0)
- FirebaseRemoteConfigInterop (~> 10.23)
- FirebaseSessions (~> 10.5)
- GoogleDataTransport (~> 9.2)
- GoogleUtilities/Environment (~> 7.8)
- nanopb (< 2.30911.0, >= 2.30908.0)
- PromisesObjC (~> 2.1)
- FirebaseInstallations (10.29.0): - FirebaseInstallations (10.29.0):
- FirebaseCore (~> 10.0) - FirebaseCore (~> 10.0)
- GoogleUtilities/Environment (~> 7.8) - GoogleUtilities/Environment (~> 7.8)
@@ -35,6 +53,16 @@ PODS:
- GoogleUtilities/Reachability (~> 7.8) - GoogleUtilities/Reachability (~> 7.8)
- GoogleUtilities/UserDefaults (~> 7.8) - GoogleUtilities/UserDefaults (~> 7.8)
- nanopb (< 2.30911.0, >= 2.30908.0) - nanopb (< 2.30911.0, >= 2.30908.0)
- FirebaseRemoteConfigInterop (10.29.0)
- FirebaseSessions (10.29.0):
- FirebaseCore (~> 10.5)
- FirebaseCoreExtension (~> 10.0)
- FirebaseInstallations (~> 10.0)
- GoogleDataTransport (~> 9.2)
- GoogleUtilities/Environment (~> 7.13)
- GoogleUtilities/UserDefaults (~> 7.13)
- nanopb (< 2.30911.0, >= 2.30908.0)
- PromisesSwift (~> 2.1)
- Flutter (1.0.0) - Flutter (1.0.0)
- flutter_local_notifications (0.0.1): - flutter_local_notifications (0.0.1):
- Flutter - Flutter
@@ -84,6 +112,8 @@ PODS:
- permission_handler_apple (9.3.0): - permission_handler_apple (9.3.0):
- Flutter - Flutter
- PromisesObjC (2.4.0) - PromisesObjC (2.4.0)
- PromisesSwift (2.4.0):
- PromisesObjC (= 2.4.0)
- shared_preferences_foundation (0.0.1): - shared_preferences_foundation (0.0.1):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
@@ -97,6 +127,7 @@ DEPENDENCIES:
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`) - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
- firebase_core (from `.symlinks/plugins/firebase_core/ios`) - firebase_core (from `.symlinks/plugins/firebase_core/ios`)
- firebase_crashlytics (from `.symlinks/plugins/firebase_crashlytics/ios`)
- firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`) - firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`)
- Flutter (from `Flutter`) - Flutter (from `Flutter`)
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`) - flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
@@ -113,13 +144,18 @@ SPEC REPOS:
trunk: trunk:
- Firebase - Firebase
- FirebaseCore - FirebaseCore
- FirebaseCoreExtension
- FirebaseCoreInternal - FirebaseCoreInternal
- FirebaseCrashlytics
- FirebaseInstallations - FirebaseInstallations
- FirebaseMessaging - FirebaseMessaging
- FirebaseRemoteConfigInterop
- FirebaseSessions
- GoogleDataTransport - GoogleDataTransport
- GoogleUtilities - GoogleUtilities
- nanopb - nanopb
- PromisesObjC - PromisesObjC
- PromisesSwift
EXTERNAL SOURCES: EXTERNAL SOURCES:
connectivity_plus: connectivity_plus:
@@ -128,6 +164,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/device_info_plus/ios" :path: ".symlinks/plugins/device_info_plus/ios"
firebase_core: firebase_core:
:path: ".symlinks/plugins/firebase_core/ios" :path: ".symlinks/plugins/firebase_core/ios"
firebase_crashlytics:
:path: ".symlinks/plugins/firebase_crashlytics/ios"
firebase_messaging: firebase_messaging:
:path: ".symlinks/plugins/firebase_messaging/ios" :path: ".symlinks/plugins/firebase_messaging/ios"
Flutter: Flutter:
@@ -156,11 +194,16 @@ SPEC CHECKSUMS:
device_info_plus: bf2e3232933866d73fe290f2942f2156cdd10342 device_info_plus: bf2e3232933866d73fe290f2942f2156cdd10342
Firebase: 0312a2352584f782ea56f66d91606891d4607f06 Firebase: 0312a2352584f782ea56f66d91606891d4607f06
firebase_core: a626d00494efa398e7c54f25f1454a64c8abf197 firebase_core: a626d00494efa398e7c54f25f1454a64c8abf197
firebase_crashlytics: 17e856fabec68d993662abaf2f6fe2413f0abece
firebase_messaging: 1541105e2a2a6ef8bd869bcc44157d31e82f3a50 firebase_messaging: 1541105e2a2a6ef8bd869bcc44157d31e82f3a50
FirebaseCore: 7ec4d0484817f12c3373955bc87762d96842d483 FirebaseCore: 7ec4d0484817f12c3373955bc87762d96842d483
FirebaseCoreExtension: 705ca5b14bf71d2564a0ddc677df1fc86ffa600f
FirebaseCoreInternal: df84dd300b561c27d5571684f389bf60b0a5c934 FirebaseCoreInternal: df84dd300b561c27d5571684f389bf60b0a5c934
FirebaseCrashlytics: 4b96efb0ce73b38b2a85e8b8bd1bd8f63f09d015
FirebaseInstallations: 913cf60d0400ebd5d6b63a28b290372ab44590dd FirebaseInstallations: 913cf60d0400ebd5d6b63a28b290372ab44590dd
FirebaseMessaging: 88950ba9485052891ebe26f6c43a52bb62248952 FirebaseMessaging: 88950ba9485052891ebe26f6c43a52bb62248952
FirebaseRemoteConfigInterop: 6efda51fb5e2f15b16585197e26eaa09574e8a4d
FirebaseSessions: dbd14adac65ce996228652c1fc3a3f576bdf3ecc
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
flutter_local_notifications: df98d66e515e1ca797af436137b4459b160ad8c9 flutter_local_notifications: df98d66e515e1ca797af436137b4459b160ad8c9
GoogleDataTransport: 6c09b596d841063d76d4288cc2d2f42cc36e1e2a GoogleDataTransport: 6c09b596d841063d76d4288cc2d2f42cc36e1e2a
@@ -172,10 +215,11 @@ SPEC CHECKSUMS:
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2 permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe
PODFILE CHECKSUM: e30f02f9d1c72c47bb6344a0a748c9d268180865 PODFILE CHECKSUM: 5f0fa675e57bf6c9b78950d2f469725f8fefc4a3
COCOAPODS: 1.16.2 COCOAPODS: 1.16.2
+11 -3
View File
@@ -1,6 +1,7 @@
import Flutter import Flutter
import UIKit import UIKit
import UserNotifications import UserNotifications
import Firebase
@main @main
@objc class AppDelegate: FlutterAppDelegate { @objc class AppDelegate: FlutterAppDelegate {
@@ -8,11 +9,18 @@ import UserNotifications
_ application: UIApplication, _ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool { ) -> Bool {
FirebaseApp.configure()
// Set notification delegate so notifications show in foreground & background // Set notification delegate so notifications show in foreground & background
UNUserNotificationCenter.current().delegate = self UNUserNotificationCenter.current().delegate = self
GeneratedPluginRegistrant.register(with: self) GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
return super.application(
application,
didFinishLaunchingWithOptions: launchOptions
)
} }
// Called when a notification is delivered while app is in foreground // Called when a notification is delivered while app is in foreground
@@ -24,7 +32,7 @@ import UserNotifications
completionHandler([.banner, .badge, .sound]) completionHandler([.banner, .badge, .sound])
} }
// Called when user taps a notification (foreground or background) // Called when user taps a notification
override func userNotificationCenter( override func userNotificationCenter(
_ center: UNUserNotificationCenter, _ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse, didReceive response: UNNotificationResponse,
@@ -32,4 +40,4 @@ import UserNotifications
) { ) {
completionHandler() completionHandler()
} }
} }
Binary file not shown.

Before

Width:  |  Height:  |  Size: 654 KiB

After

Width:  |  Height:  |  Size: 652 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 775 B

After

Width:  |  Height:  |  Size: 622 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 20 KiB

+1 -1
View File
@@ -5,7 +5,7 @@
<key>CFBundleDevelopmentRegion</key> <key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string> <string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key> <key>CFBundleDisplayName</key>
<string>Enaklo Owner</string> <string>Grow Food</string>
<key>CFBundleExecutable</key> <key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string> <string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key> <key>CFBundleIdentifier</key>
+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>aps-environment</key>
<string>development</string>
</dict>
</plist>
+1
View File
@@ -0,0 +1 @@
2edd7a931624573eff7b4280d22a7f4e
+4 -4
View File
@@ -3,15 +3,15 @@
flutter_launcher_icons: flutter_launcher_icons:
android: "launcher_icon" android: "launcher_icon"
ios: true ios: true
image_path: "assets/images/logo.png" image_path: "assets/images/ic_launcher.png"
remove_alpha_ios: true remove_alpha_ios: true
min_sdk_android: 21 # android min sdk min:16, default 21 min_sdk_android: 21 # android min sdk min:16, default 21
adaptive_icon_background: "#ffffff" adaptive_icon_background: "#ffffff"
adaptive_icon_foreground: "assets/images/logo.png" adaptive_icon_foreground: "assets/images/ic_launcher.png"
web: web:
generate: true generate: true
image_path: "assets/images/logo.png" image_path: "assets/images/ic_launcher.png"
windows: windows:
generate: true generate: true
image_path: "assets/images/logo.png" image_path: "assets/images/ic_launcher.png"
icon_size: 48 icon_size: 48
@@ -0,0 +1,53 @@
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 'exclusive_summary_loader_event.dart';
part 'exclusive_summary_loader_state.dart';
part 'exclusive_summary_loader_bloc.freezed.dart';
@injectable
class ExclusiveSummaryLoaderBloc
extends Bloc<ExclusiveSummaryLoaderEvent, ExclusiveSummaryLoaderState> {
final IAnalyticRepository _repository;
ExclusiveSummaryLoaderBloc(this._repository)
: super(ExclusiveSummaryLoaderState.initial()) {
on<ExclusiveSummaryLoaderEvent>(_onEvent);
}
Future<void> _onEvent(
ExclusiveSummaryLoaderEvent event,
Emitter<ExclusiveSummaryLoaderState> emit,
) {
return event.map(
rangeDateChanged: (e) async {
emit(state.copyWith(dateFrom: e.dateFrom, dateTo: e.dateTo));
},
fetched: (e) async {
emit(
state.copyWith(
isFetching: true,
failureOption: none(),
),
);
final result = await _repository.getExclusiveSummary(
dateFrom: state.dateFrom,
dateTo: state.dateTo,
);
final data = result.fold(
(f) => state.copyWith(failureOption: optionOf(f)),
(summary) => state.copyWith(exclusiveSummary: summary),
);
emit(data.copyWith(isFetching: false));
},
);
}
}
@@ -0,0 +1,617 @@
// 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 'exclusive_summary_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 _$ExclusiveSummaryLoaderEvent {
@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;
}
/// @nodoc
abstract class $ExclusiveSummaryLoaderEventCopyWith<$Res> {
factory $ExclusiveSummaryLoaderEventCopyWith(
ExclusiveSummaryLoaderEvent value,
$Res Function(ExclusiveSummaryLoaderEvent) then,
) =
_$ExclusiveSummaryLoaderEventCopyWithImpl<
$Res,
ExclusiveSummaryLoaderEvent
>;
}
/// @nodoc
class _$ExclusiveSummaryLoaderEventCopyWithImpl<
$Res,
$Val extends ExclusiveSummaryLoaderEvent
>
implements $ExclusiveSummaryLoaderEventCopyWith<$Res> {
_$ExclusiveSummaryLoaderEventCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of ExclusiveSummaryLoaderEvent
/// 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
_$ExclusiveSummaryLoaderEventCopyWithImpl<$Res, _$RangeDateChangedImpl>
implements _$$RangeDateChangedImplCopyWith<$Res> {
__$$RangeDateChangedImplCopyWithImpl(
_$RangeDateChangedImpl _value,
$Res Function(_$RangeDateChangedImpl) _then,
) : super(_value, _then);
/// Create a copy of ExclusiveSummaryLoaderEvent
/// 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 'ExclusiveSummaryLoaderEvent.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 ExclusiveSummaryLoaderEvent
/// 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 ExclusiveSummaryLoaderEvent {
const factory _RangeDateChanged(
final DateTime dateFrom,
final DateTime dateTo,
) = _$RangeDateChangedImpl;
DateTime get dateFrom;
DateTime get dateTo;
/// Create a copy of ExclusiveSummaryLoaderEvent
/// 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(
_$FetchedImpl value,
$Res Function(_$FetchedImpl) then,
) = __$$FetchedImplCopyWithImpl<$Res>;
}
/// @nodoc
class __$$FetchedImplCopyWithImpl<$Res>
extends _$ExclusiveSummaryLoaderEventCopyWithImpl<$Res, _$FetchedImpl>
implements _$$FetchedImplCopyWith<$Res> {
__$$FetchedImplCopyWithImpl(
_$FetchedImpl _value,
$Res Function(_$FetchedImpl) _then,
) : super(_value, _then);
/// Create a copy of ExclusiveSummaryLoaderEvent
/// with the given fields replaced by the non-null parameter values.
}
/// @nodoc
class _$FetchedImpl implements _Fetched {
const _$FetchedImpl();
@override
String toString() {
return 'ExclusiveSummaryLoaderEvent.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() fetched,
}) {
return fetched();
}
@override
@optionalTypeArgs
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(),
}) {
if (fetched != null) {
return fetched();
}
return orElse();
}
@override
@optionalTypeArgs
TResult map<TResult extends Object?>({
required TResult Function(_RangeDateChanged value) rangeDateChanged,
required TResult Function(_Fetched value) fetched,
}) {
return fetched(this);
}
@override
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult? Function(_RangeDateChanged value)? rangeDateChanged,
TResult? Function(_Fetched value)? fetched,
}) {
return fetched?.call(this);
}
@override
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
TResult Function(_RangeDateChanged value)? rangeDateChanged,
TResult Function(_Fetched value)? fetched,
required TResult orElse(),
}) {
if (fetched != null) {
return fetched(this);
}
return orElse();
}
}
abstract class _Fetched implements ExclusiveSummaryLoaderEvent {
const factory _Fetched() = _$FetchedImpl;
}
/// @nodoc
mixin _$ExclusiveSummaryLoaderState {
ExclusiveSummary get exclusiveSummary => throw _privateConstructorUsedError;
Option<AnalyticFailure> get failureOption =>
throw _privateConstructorUsedError;
bool get isFetching => throw _privateConstructorUsedError;
DateTime get dateFrom => throw _privateConstructorUsedError;
DateTime get dateTo => throw _privateConstructorUsedError;
/// Create a copy of ExclusiveSummaryLoaderState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$ExclusiveSummaryLoaderStateCopyWith<ExclusiveSummaryLoaderState>
get copyWith => throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $ExclusiveSummaryLoaderStateCopyWith<$Res> {
factory $ExclusiveSummaryLoaderStateCopyWith(
ExclusiveSummaryLoaderState value,
$Res Function(ExclusiveSummaryLoaderState) then,
) =
_$ExclusiveSummaryLoaderStateCopyWithImpl<
$Res,
ExclusiveSummaryLoaderState
>;
@useResult
$Res call({
ExclusiveSummary exclusiveSummary,
Option<AnalyticFailure> failureOption,
bool isFetching,
DateTime dateFrom,
DateTime dateTo,
});
$ExclusiveSummaryCopyWith<$Res> get exclusiveSummary;
}
/// @nodoc
class _$ExclusiveSummaryLoaderStateCopyWithImpl<
$Res,
$Val extends ExclusiveSummaryLoaderState
>
implements $ExclusiveSummaryLoaderStateCopyWith<$Res> {
_$ExclusiveSummaryLoaderStateCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of ExclusiveSummaryLoaderState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? exclusiveSummary = null,
Object? failureOption = null,
Object? isFetching = null,
Object? dateFrom = null,
Object? dateTo = null,
}) {
return _then(
_value.copyWith(
exclusiveSummary: null == exclusiveSummary
? _value.exclusiveSummary
: exclusiveSummary // ignore: cast_nullable_to_non_nullable
as ExclusiveSummary,
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,
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 ExclusiveSummaryLoaderState
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$ExclusiveSummaryCopyWith<$Res> get exclusiveSummary {
return $ExclusiveSummaryCopyWith<$Res>(_value.exclusiveSummary, (value) {
return _then(_value.copyWith(exclusiveSummary: value) as $Val);
});
}
}
/// @nodoc
abstract class _$$ExclusiveSummaryLoaderStateImplCopyWith<$Res>
implements $ExclusiveSummaryLoaderStateCopyWith<$Res> {
factory _$$ExclusiveSummaryLoaderStateImplCopyWith(
_$ExclusiveSummaryLoaderStateImpl value,
$Res Function(_$ExclusiveSummaryLoaderStateImpl) then,
) = __$$ExclusiveSummaryLoaderStateImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({
ExclusiveSummary exclusiveSummary,
Option<AnalyticFailure> failureOption,
bool isFetching,
DateTime dateFrom,
DateTime dateTo,
});
@override
$ExclusiveSummaryCopyWith<$Res> get exclusiveSummary;
}
/// @nodoc
class __$$ExclusiveSummaryLoaderStateImplCopyWithImpl<$Res>
extends
_$ExclusiveSummaryLoaderStateCopyWithImpl<
$Res,
_$ExclusiveSummaryLoaderStateImpl
>
implements _$$ExclusiveSummaryLoaderStateImplCopyWith<$Res> {
__$$ExclusiveSummaryLoaderStateImplCopyWithImpl(
_$ExclusiveSummaryLoaderStateImpl _value,
$Res Function(_$ExclusiveSummaryLoaderStateImpl) _then,
) : super(_value, _then);
/// Create a copy of ExclusiveSummaryLoaderState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? exclusiveSummary = null,
Object? failureOption = null,
Object? isFetching = null,
Object? dateFrom = null,
Object? dateTo = null,
}) {
return _then(
_$ExclusiveSummaryLoaderStateImpl(
exclusiveSummary: null == exclusiveSummary
? _value.exclusiveSummary
: exclusiveSummary // ignore: cast_nullable_to_non_nullable
as ExclusiveSummary,
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,
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 _$ExclusiveSummaryLoaderStateImpl
implements _ExclusiveSummaryLoaderState {
const _$ExclusiveSummaryLoaderStateImpl({
required this.exclusiveSummary,
required this.failureOption,
this.isFetching = false,
required this.dateFrom,
required this.dateTo,
});
@override
final ExclusiveSummary exclusiveSummary;
@override
final Option<AnalyticFailure> failureOption;
@override
@JsonKey()
final bool isFetching;
@override
final DateTime dateFrom;
@override
final DateTime dateTo;
@override
String toString() {
return 'ExclusiveSummaryLoaderState(exclusiveSummary: $exclusiveSummary, failureOption: $failureOption, isFetching: $isFetching, dateFrom: $dateFrom, dateTo: $dateTo)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$ExclusiveSummaryLoaderStateImpl &&
(identical(other.exclusiveSummary, exclusiveSummary) ||
other.exclusiveSummary == exclusiveSummary) &&
(identical(other.failureOption, failureOption) ||
other.failureOption == failureOption) &&
(identical(other.isFetching, isFetching) ||
other.isFetching == isFetching) &&
(identical(other.dateFrom, dateFrom) ||
other.dateFrom == dateFrom) &&
(identical(other.dateTo, dateTo) || other.dateTo == dateTo));
}
@override
int get hashCode => Object.hash(
runtimeType,
exclusiveSummary,
failureOption,
isFetching,
dateFrom,
dateTo,
);
/// Create a copy of ExclusiveSummaryLoaderState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$ExclusiveSummaryLoaderStateImplCopyWith<_$ExclusiveSummaryLoaderStateImpl>
get copyWith =>
__$$ExclusiveSummaryLoaderStateImplCopyWithImpl<
_$ExclusiveSummaryLoaderStateImpl
>(this, _$identity);
}
abstract class _ExclusiveSummaryLoaderState
implements ExclusiveSummaryLoaderState {
const factory _ExclusiveSummaryLoaderState({
required final ExclusiveSummary exclusiveSummary,
required final Option<AnalyticFailure> failureOption,
final bool isFetching,
required final DateTime dateFrom,
required final DateTime dateTo,
}) = _$ExclusiveSummaryLoaderStateImpl;
@override
ExclusiveSummary get exclusiveSummary;
@override
Option<AnalyticFailure> get failureOption;
@override
bool get isFetching;
@override
DateTime get dateFrom;
@override
DateTime get dateTo;
/// Create a copy of ExclusiveSummaryLoaderState
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$ExclusiveSummaryLoaderStateImplCopyWith<_$ExclusiveSummaryLoaderStateImpl>
get copyWith => throw _privateConstructorUsedError;
}
@@ -0,0 +1,11 @@
part of 'exclusive_summary_loader_bloc.dart';
@freezed
class ExclusiveSummaryLoaderEvent with _$ExclusiveSummaryLoaderEvent {
const factory ExclusiveSummaryLoaderEvent.rangeDateChanged(
DateTime dateFrom,
DateTime dateTo,
) = _RangeDateChanged;
const factory ExclusiveSummaryLoaderEvent.fetched() = _Fetched;
}
@@ -0,0 +1,22 @@
part of 'exclusive_summary_loader_bloc.dart';
@freezed
class ExclusiveSummaryLoaderState with _$ExclusiveSummaryLoaderState {
const factory ExclusiveSummaryLoaderState({
required ExclusiveSummary exclusiveSummary,
required Option<AnalyticFailure> failureOption,
@Default(false) bool isFetching,
required DateTime dateFrom,
required DateTime dateTo,
}) = _ExclusiveSummaryLoaderState;
factory ExclusiveSummaryLoaderState.initial() {
final now = DateTime.now();
return ExclusiveSummaryLoaderState(
exclusiveSummary: ExclusiveSummary.empty(),
failureOption: none(),
dateFrom: DateTime(now.year, now.month, 1),
dateTo: DateTime(now.year, now.month + 1, 0),
);
}
}
@@ -0,0 +1,56 @@
import 'package:bloc/bloc.dart';
import 'package:dartz/dartz.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 'purchasing_analytic_loader_event.dart';
part 'purchasing_analytic_loader_state.dart';
part 'purchasing_analytic_loader_bloc.freezed.dart';
@injectable
class PurchasingAnalyticLoaderBloc
extends Bloc<PurchasingAnalyticLoaderEvent, PurchasingAnalyticLoaderState> {
final IAnalyticRepository _analyticRepository;
PurchasingAnalyticLoaderBloc(this._analyticRepository)
: super(PurchasingAnalyticLoaderState.initial()) {
on<PurchasingAnalyticLoaderEvent>(_onEvent);
}
Future<void> _onEvent(
PurchasingAnalyticLoaderEvent event,
Emitter<PurchasingAnalyticLoaderState> emit,
) {
return event.map(
rangeDateChanged: (e) async {
emit(state.copyWith(dateFrom: e.dateFrom, dateTo: e.dateTo));
},
outletChanged: (e) async {
emit(state.copyWith(outletId: e.outletId));
},
fetched: (e) async {
emit(state.copyWith(
isFetching: true,
failureOptionPurchasing: none(),
));
final result = await _analyticRepository.getPurchasing(
dateFrom: state.dateFrom,
dateTo: state.dateTo,
outletId: state.outletId,
groupBy: state.groupBy,
);
final newState = result.fold(
(f) => state.copyWith(failureOptionPurchasing: optionOf(f)),
(purchasing) => state.copyWith(purchasing: purchasing),
);
emit(newState.copyWith(isFetching: false));
},
);
}
}
@@ -0,0 +1,840 @@
// 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 'purchasing_analytic_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 _$PurchasingAnalyticLoaderEvent {
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function(DateTime dateFrom, DateTime dateTo)
rangeDateChanged,
required TResult Function(String? outletId) outletChanged,
required TResult Function() fetched,
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function(DateTime dateFrom, DateTime dateTo)? rangeDateChanged,
TResult? Function(String? outletId)? outletChanged,
TResult? Function()? fetched,
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function(DateTime dateFrom, DateTime dateTo)? rangeDateChanged,
TResult Function(String? outletId)? outletChanged,
TResult Function()? fetched,
required TResult orElse(),
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult map<TResult extends Object?>({
required TResult Function(_RangeDateChanged value) rangeDateChanged,
required TResult Function(_OutletChanged value) outletChanged,
required TResult Function(_Fetched value) fetched,
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult? Function(_RangeDateChanged value)? rangeDateChanged,
TResult? Function(_OutletChanged value)? outletChanged,
TResult? Function(_Fetched value)? fetched,
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
TResult Function(_RangeDateChanged value)? rangeDateChanged,
TResult Function(_OutletChanged value)? outletChanged,
TResult Function(_Fetched value)? fetched,
required TResult orElse(),
}) => throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $PurchasingAnalyticLoaderEventCopyWith<$Res> {
factory $PurchasingAnalyticLoaderEventCopyWith(
PurchasingAnalyticLoaderEvent value,
$Res Function(PurchasingAnalyticLoaderEvent) then,
) =
_$PurchasingAnalyticLoaderEventCopyWithImpl<
$Res,
PurchasingAnalyticLoaderEvent
>;
}
/// @nodoc
class _$PurchasingAnalyticLoaderEventCopyWithImpl<
$Res,
$Val extends PurchasingAnalyticLoaderEvent
>
implements $PurchasingAnalyticLoaderEventCopyWith<$Res> {
_$PurchasingAnalyticLoaderEventCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of PurchasingAnalyticLoaderEvent
/// 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
_$PurchasingAnalyticLoaderEventCopyWithImpl<
$Res,
_$RangeDateChangedImpl
>
implements _$$RangeDateChangedImplCopyWith<$Res> {
__$$RangeDateChangedImplCopyWithImpl(
_$RangeDateChangedImpl _value,
$Res Function(_$RangeDateChangedImpl) _then,
) : super(_value, _then);
/// Create a copy of PurchasingAnalyticLoaderEvent
/// 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 'PurchasingAnalyticLoaderEvent.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 PurchasingAnalyticLoaderEvent
/// 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(String? outletId) outletChanged,
required TResult Function() fetched,
}) {
return rangeDateChanged(dateFrom, dateTo);
}
@override
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function(DateTime dateFrom, DateTime dateTo)? rangeDateChanged,
TResult? Function(String? outletId)? outletChanged,
TResult? Function()? fetched,
}) {
return rangeDateChanged?.call(dateFrom, dateTo);
}
@override
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function(DateTime dateFrom, DateTime dateTo)? rangeDateChanged,
TResult Function(String? outletId)? outletChanged,
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(_OutletChanged value) outletChanged,
required TResult Function(_Fetched value) fetched,
}) {
return rangeDateChanged(this);
}
@override
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult? Function(_RangeDateChanged value)? rangeDateChanged,
TResult? Function(_OutletChanged value)? outletChanged,
TResult? Function(_Fetched value)? fetched,
}) {
return rangeDateChanged?.call(this);
}
@override
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
TResult Function(_RangeDateChanged value)? rangeDateChanged,
TResult Function(_OutletChanged value)? outletChanged,
TResult Function(_Fetched value)? fetched,
required TResult orElse(),
}) {
if (rangeDateChanged != null) {
return rangeDateChanged(this);
}
return orElse();
}
}
abstract class _RangeDateChanged implements PurchasingAnalyticLoaderEvent {
const factory _RangeDateChanged(
final DateTime dateFrom,
final DateTime dateTo,
) = _$RangeDateChangedImpl;
DateTime get dateFrom;
DateTime get dateTo;
/// Create a copy of PurchasingAnalyticLoaderEvent
/// 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 _$$OutletChangedImplCopyWith<$Res> {
factory _$$OutletChangedImplCopyWith(
_$OutletChangedImpl value,
$Res Function(_$OutletChangedImpl) then,
) = __$$OutletChangedImplCopyWithImpl<$Res>;
@useResult
$Res call({String? outletId});
}
/// @nodoc
class __$$OutletChangedImplCopyWithImpl<$Res>
extends
_$PurchasingAnalyticLoaderEventCopyWithImpl<$Res, _$OutletChangedImpl>
implements _$$OutletChangedImplCopyWith<$Res> {
__$$OutletChangedImplCopyWithImpl(
_$OutletChangedImpl _value,
$Res Function(_$OutletChangedImpl) _then,
) : super(_value, _then);
/// Create a copy of PurchasingAnalyticLoaderEvent
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({Object? outletId = freezed}) {
return _then(
_$OutletChangedImpl(
freezed == outletId
? _value.outletId
: outletId // ignore: cast_nullable_to_non_nullable
as String?,
),
);
}
}
/// @nodoc
class _$OutletChangedImpl implements _OutletChanged {
const _$OutletChangedImpl(this.outletId);
@override
final String? outletId;
@override
String toString() {
return 'PurchasingAnalyticLoaderEvent.outletChanged(outletId: $outletId)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$OutletChangedImpl &&
(identical(other.outletId, outletId) ||
other.outletId == outletId));
}
@override
int get hashCode => Object.hash(runtimeType, outletId);
/// Create a copy of PurchasingAnalyticLoaderEvent
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$OutletChangedImplCopyWith<_$OutletChangedImpl> get copyWith =>
__$$OutletChangedImplCopyWithImpl<_$OutletChangedImpl>(this, _$identity);
@override
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function(DateTime dateFrom, DateTime dateTo)
rangeDateChanged,
required TResult Function(String? outletId) outletChanged,
required TResult Function() fetched,
}) {
return outletChanged(outletId);
}
@override
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function(DateTime dateFrom, DateTime dateTo)? rangeDateChanged,
TResult? Function(String? outletId)? outletChanged,
TResult? Function()? fetched,
}) {
return outletChanged?.call(outletId);
}
@override
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function(DateTime dateFrom, DateTime dateTo)? rangeDateChanged,
TResult Function(String? outletId)? outletChanged,
TResult Function()? fetched,
required TResult orElse(),
}) {
if (outletChanged != null) {
return outletChanged(outletId);
}
return orElse();
}
@override
@optionalTypeArgs
TResult map<TResult extends Object?>({
required TResult Function(_RangeDateChanged value) rangeDateChanged,
required TResult Function(_OutletChanged value) outletChanged,
required TResult Function(_Fetched value) fetched,
}) {
return outletChanged(this);
}
@override
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult? Function(_RangeDateChanged value)? rangeDateChanged,
TResult? Function(_OutletChanged value)? outletChanged,
TResult? Function(_Fetched value)? fetched,
}) {
return outletChanged?.call(this);
}
@override
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
TResult Function(_RangeDateChanged value)? rangeDateChanged,
TResult Function(_OutletChanged value)? outletChanged,
TResult Function(_Fetched value)? fetched,
required TResult orElse(),
}) {
if (outletChanged != null) {
return outletChanged(this);
}
return orElse();
}
}
abstract class _OutletChanged implements PurchasingAnalyticLoaderEvent {
const factory _OutletChanged(final String? outletId) = _$OutletChangedImpl;
String? get outletId;
/// Create a copy of PurchasingAnalyticLoaderEvent
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
_$$OutletChangedImplCopyWith<_$OutletChangedImpl> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class _$$FetchedImplCopyWith<$Res> {
factory _$$FetchedImplCopyWith(
_$FetchedImpl value,
$Res Function(_$FetchedImpl) then,
) = __$$FetchedImplCopyWithImpl<$Res>;
}
/// @nodoc
class __$$FetchedImplCopyWithImpl<$Res>
extends _$PurchasingAnalyticLoaderEventCopyWithImpl<$Res, _$FetchedImpl>
implements _$$FetchedImplCopyWith<$Res> {
__$$FetchedImplCopyWithImpl(
_$FetchedImpl _value,
$Res Function(_$FetchedImpl) _then,
) : super(_value, _then);
/// Create a copy of PurchasingAnalyticLoaderEvent
/// with the given fields replaced by the non-null parameter values.
}
/// @nodoc
class _$FetchedImpl implements _Fetched {
const _$FetchedImpl();
@override
String toString() {
return 'PurchasingAnalyticLoaderEvent.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(String? outletId) outletChanged,
required TResult Function() fetched,
}) {
return fetched();
}
@override
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function(DateTime dateFrom, DateTime dateTo)? rangeDateChanged,
TResult? Function(String? outletId)? outletChanged,
TResult? Function()? fetched,
}) {
return fetched?.call();
}
@override
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function(DateTime dateFrom, DateTime dateTo)? rangeDateChanged,
TResult Function(String? outletId)? outletChanged,
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(_OutletChanged value) outletChanged,
required TResult Function(_Fetched value) fetched,
}) {
return fetched(this);
}
@override
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult? Function(_RangeDateChanged value)? rangeDateChanged,
TResult? Function(_OutletChanged value)? outletChanged,
TResult? Function(_Fetched value)? fetched,
}) {
return fetched?.call(this);
}
@override
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
TResult Function(_RangeDateChanged value)? rangeDateChanged,
TResult Function(_OutletChanged value)? outletChanged,
TResult Function(_Fetched value)? fetched,
required TResult orElse(),
}) {
if (fetched != null) {
return fetched(this);
}
return orElse();
}
}
abstract class _Fetched implements PurchasingAnalyticLoaderEvent {
const factory _Fetched() = _$FetchedImpl;
}
/// @nodoc
mixin _$PurchasingAnalyticLoaderState {
PurchasingAnalytic get purchasing => throw _privateConstructorUsedError;
Option<AnalyticFailure> get failureOptionPurchasing =>
throw _privateConstructorUsedError;
bool get isFetching => throw _privateConstructorUsedError;
DateTime get dateFrom => throw _privateConstructorUsedError;
DateTime get dateTo => throw _privateConstructorUsedError;
String? get outletId => throw _privateConstructorUsedError;
String get groupBy => throw _privateConstructorUsedError;
/// Create a copy of PurchasingAnalyticLoaderState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$PurchasingAnalyticLoaderStateCopyWith<PurchasingAnalyticLoaderState>
get copyWith => throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $PurchasingAnalyticLoaderStateCopyWith<$Res> {
factory $PurchasingAnalyticLoaderStateCopyWith(
PurchasingAnalyticLoaderState value,
$Res Function(PurchasingAnalyticLoaderState) then,
) =
_$PurchasingAnalyticLoaderStateCopyWithImpl<
$Res,
PurchasingAnalyticLoaderState
>;
@useResult
$Res call({
PurchasingAnalytic purchasing,
Option<AnalyticFailure> failureOptionPurchasing,
bool isFetching,
DateTime dateFrom,
DateTime dateTo,
String? outletId,
String groupBy,
});
$PurchasingAnalyticCopyWith<$Res> get purchasing;
}
/// @nodoc
class _$PurchasingAnalyticLoaderStateCopyWithImpl<
$Res,
$Val extends PurchasingAnalyticLoaderState
>
implements $PurchasingAnalyticLoaderStateCopyWith<$Res> {
_$PurchasingAnalyticLoaderStateCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of PurchasingAnalyticLoaderState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? purchasing = null,
Object? failureOptionPurchasing = null,
Object? isFetching = null,
Object? dateFrom = null,
Object? dateTo = null,
Object? outletId = freezed,
Object? groupBy = null,
}) {
return _then(
_value.copyWith(
purchasing: null == purchasing
? _value.purchasing
: purchasing // ignore: cast_nullable_to_non_nullable
as PurchasingAnalytic,
failureOptionPurchasing: null == failureOptionPurchasing
? _value.failureOptionPurchasing
: failureOptionPurchasing // ignore: cast_nullable_to_non_nullable
as Option<AnalyticFailure>,
isFetching: null == isFetching
? _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,
outletId: freezed == outletId
? _value.outletId
: outletId // ignore: cast_nullable_to_non_nullable
as String?,
groupBy: null == groupBy
? _value.groupBy
: groupBy // ignore: cast_nullable_to_non_nullable
as String,
)
as $Val,
);
}
/// Create a copy of PurchasingAnalyticLoaderState
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$PurchasingAnalyticCopyWith<$Res> get purchasing {
return $PurchasingAnalyticCopyWith<$Res>(_value.purchasing, (value) {
return _then(_value.copyWith(purchasing: value) as $Val);
});
}
}
/// @nodoc
abstract class _$$PurchasingAnalyticLoaderStateImplCopyWith<$Res>
implements $PurchasingAnalyticLoaderStateCopyWith<$Res> {
factory _$$PurchasingAnalyticLoaderStateImplCopyWith(
_$PurchasingAnalyticLoaderStateImpl value,
$Res Function(_$PurchasingAnalyticLoaderStateImpl) then,
) = __$$PurchasingAnalyticLoaderStateImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({
PurchasingAnalytic purchasing,
Option<AnalyticFailure> failureOptionPurchasing,
bool isFetching,
DateTime dateFrom,
DateTime dateTo,
String? outletId,
String groupBy,
});
@override
$PurchasingAnalyticCopyWith<$Res> get purchasing;
}
/// @nodoc
class __$$PurchasingAnalyticLoaderStateImplCopyWithImpl<$Res>
extends
_$PurchasingAnalyticLoaderStateCopyWithImpl<
$Res,
_$PurchasingAnalyticLoaderStateImpl
>
implements _$$PurchasingAnalyticLoaderStateImplCopyWith<$Res> {
__$$PurchasingAnalyticLoaderStateImplCopyWithImpl(
_$PurchasingAnalyticLoaderStateImpl _value,
$Res Function(_$PurchasingAnalyticLoaderStateImpl) _then,
) : super(_value, _then);
/// Create a copy of PurchasingAnalyticLoaderState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? purchasing = null,
Object? failureOptionPurchasing = null,
Object? isFetching = null,
Object? dateFrom = null,
Object? dateTo = null,
Object? outletId = freezed,
Object? groupBy = null,
}) {
return _then(
_$PurchasingAnalyticLoaderStateImpl(
purchasing: null == purchasing
? _value.purchasing
: purchasing // ignore: cast_nullable_to_non_nullable
as PurchasingAnalytic,
failureOptionPurchasing: null == failureOptionPurchasing
? _value.failureOptionPurchasing
: failureOptionPurchasing // ignore: cast_nullable_to_non_nullable
as Option<AnalyticFailure>,
isFetching: null == isFetching
? _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,
outletId: freezed == outletId
? _value.outletId
: outletId // ignore: cast_nullable_to_non_nullable
as String?,
groupBy: null == groupBy
? _value.groupBy
: groupBy // ignore: cast_nullable_to_non_nullable
as String,
),
);
}
}
/// @nodoc
class _$PurchasingAnalyticLoaderStateImpl
implements _PurchasingAnalyticLoaderState {
const _$PurchasingAnalyticLoaderStateImpl({
required this.purchasing,
required this.failureOptionPurchasing,
this.isFetching = false,
required this.dateFrom,
required this.dateTo,
this.outletId,
this.groupBy = 'day',
});
@override
final PurchasingAnalytic purchasing;
@override
final Option<AnalyticFailure> failureOptionPurchasing;
@override
@JsonKey()
final bool isFetching;
@override
final DateTime dateFrom;
@override
final DateTime dateTo;
@override
final String? outletId;
@override
@JsonKey()
final String groupBy;
@override
String toString() {
return 'PurchasingAnalyticLoaderState(purchasing: $purchasing, failureOptionPurchasing: $failureOptionPurchasing, isFetching: $isFetching, dateFrom: $dateFrom, dateTo: $dateTo, outletId: $outletId, groupBy: $groupBy)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$PurchasingAnalyticLoaderStateImpl &&
(identical(other.purchasing, purchasing) ||
other.purchasing == purchasing) &&
(identical(
other.failureOptionPurchasing,
failureOptionPurchasing,
) ||
other.failureOptionPurchasing == failureOptionPurchasing) &&
(identical(other.isFetching, isFetching) ||
other.isFetching == isFetching) &&
(identical(other.dateFrom, dateFrom) ||
other.dateFrom == dateFrom) &&
(identical(other.dateTo, dateTo) || other.dateTo == dateTo) &&
(identical(other.outletId, outletId) ||
other.outletId == outletId) &&
(identical(other.groupBy, groupBy) || other.groupBy == groupBy));
}
@override
int get hashCode => Object.hash(
runtimeType,
purchasing,
failureOptionPurchasing,
isFetching,
dateFrom,
dateTo,
outletId,
groupBy,
);
/// Create a copy of PurchasingAnalyticLoaderState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$PurchasingAnalyticLoaderStateImplCopyWith<
_$PurchasingAnalyticLoaderStateImpl
>
get copyWith =>
__$$PurchasingAnalyticLoaderStateImplCopyWithImpl<
_$PurchasingAnalyticLoaderStateImpl
>(this, _$identity);
}
abstract class _PurchasingAnalyticLoaderState
implements PurchasingAnalyticLoaderState {
const factory _PurchasingAnalyticLoaderState({
required final PurchasingAnalytic purchasing,
required final Option<AnalyticFailure> failureOptionPurchasing,
final bool isFetching,
required final DateTime dateFrom,
required final DateTime dateTo,
final String? outletId,
final String groupBy,
}) = _$PurchasingAnalyticLoaderStateImpl;
@override
PurchasingAnalytic get purchasing;
@override
Option<AnalyticFailure> get failureOptionPurchasing;
@override
bool get isFetching;
@override
DateTime get dateFrom;
@override
DateTime get dateTo;
@override
String? get outletId;
@override
String get groupBy;
/// Create a copy of PurchasingAnalyticLoaderState
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$PurchasingAnalyticLoaderStateImplCopyWith<
_$PurchasingAnalyticLoaderStateImpl
>
get copyWith => throw _privateConstructorUsedError;
}
@@ -0,0 +1,15 @@
part of 'purchasing_analytic_loader_bloc.dart';
@freezed
class PurchasingAnalyticLoaderEvent with _$PurchasingAnalyticLoaderEvent {
const factory PurchasingAnalyticLoaderEvent.rangeDateChanged(
DateTime dateFrom,
DateTime dateTo,
) = _RangeDateChanged;
const factory PurchasingAnalyticLoaderEvent.outletChanged(
String? outletId,
) = _OutletChanged;
const factory PurchasingAnalyticLoaderEvent.fetched() = _Fetched;
}
@@ -0,0 +1,22 @@
part of 'purchasing_analytic_loader_bloc.dart';
@freezed
class PurchasingAnalyticLoaderState with _$PurchasingAnalyticLoaderState {
const factory PurchasingAnalyticLoaderState({
required PurchasingAnalytic purchasing,
required Option<AnalyticFailure> failureOptionPurchasing,
@Default(false) bool isFetching,
required DateTime dateFrom,
required DateTime dateTo,
String? outletId,
@Default('day') String groupBy,
}) = _PurchasingAnalyticLoaderState;
factory PurchasingAnalyticLoaderState.initial() =>
PurchasingAnalyticLoaderState(
purchasing: PurchasingAnalytic.empty(),
failureOptionPurchasing: none(),
dateFrom: DateTime.now().subtract(const Duration(days: 30)),
dateTo: DateTime.now(),
);
}
@@ -43,13 +43,8 @@ class LoginFormBloc extends Bloc<LoginFormEvent, LoginFormState> {
if (emailValid && passwordValid) { if (emailValid && passwordValid) {
// Ambil device info dan FCM token secara paralel // Ambil device info dan FCM token secara paralel
final results = await Future.wait([ final deviceInfo = await _deviceInfoService.getDeviceInfo();
_deviceInfoService.getDeviceInfo(), final fcmToken = await _fcmService.getToken();
_fcmService.getToken(),
]);
final deviceInfo = results[0] as DeviceInfo;
final fcmToken = results[1] as String?;
failureOrAuth = await _repository.login( failureOrAuth = await _repository.login(
email: state.email, email: state.email,
+3
View File
@@ -65,5 +65,8 @@ class ThemeApp {
), ),
iconTheme: const IconThemeData(color: AppColor.white), iconTheme: const IconThemeData(color: AppColor.white),
), ),
bottomSheetTheme: BottomSheetThemeData(
backgroundColor: AppColor.white,
)
); );
} }
+3
View File
@@ -11,6 +11,9 @@ class ApiPath {
static const String productAnalytic = '/api/v1/analytics/products'; static const String productAnalytic = '/api/v1/analytics/products';
static const String paymentMethodAnalytic = static const String paymentMethodAnalytic =
'/api/v1/analytics/paymentMethods'; '/api/v1/analytics/paymentMethods';
static const String purchasingAnalytic = '/api/v1/analytics/purchasing';
static const String exclusiveSummaryAnalytic =
'/api/v1/analytics/exclusive-summary/period';
// Inventory // Inventory
static const String inventoryReportDetail = static const String inventoryReportDetail =
+26 -9
View File
@@ -101,17 +101,26 @@ class FcmService {
} }
// 7. Print FCM token for debugging // 7. Print FCM token for debugging
final token = await getToken(); try {
debugPrint('[FCM] Token: $token'); final token = await getToken();
debugPrint('[FCM] Token: $token');
} catch (e) {
// Simulator atau APNs belum dikonfigurasi — skip token fetch
debugPrint('[FCM] Token unavailable: $e');
}
} }
Future<void> _requestPermission() async { Future<void> _requestPermission() async {
final settings = await _messaging.requestPermission( try {
alert: true, final settings = await _messaging.requestPermission(
badge: true, alert: true,
sound: true, badge: true,
); sound: true,
debugPrint('[FCM] Permission status: ${settings.authorizationStatus}'); );
debugPrint('[FCM] Permission status: ${settings.authorizationStatus}');
} catch (e) {
debugPrint('[FCM] Permission request failed: $e');
}
} }
Future<void> _setupLocalNotifications() async { Future<void> _setupLocalNotifications() async {
@@ -185,7 +194,15 @@ class FcmService {
} }
/// Returns the FCM registration token for this device. /// Returns the FCM registration token for this device.
Future<String?> getToken() => _messaging.getToken(); /// Returns null if token is unavailable (e.g. simulator, APNs not ready).
Future<String?> getToken() async {
try {
return await _messaging.getToken();
} catch (e) {
debugPrint('[FCM] getToken failed: $e');
return null;
}
}
/// Subscribe to a topic (e.g. 'all', 'promo'). /// Subscribe to a topic (e.g. 'all', 'promo').
Future<void> subscribeToTopic(String topic) => Future<void> subscribeToTopic(String topic) =>
+2
View File
@@ -11,4 +11,6 @@ part 'entities/inventory_analytic_entity.dart';
part 'entities/dashboard_analytic_entity.dart'; part 'entities/dashboard_analytic_entity.dart';
part 'entities/product_analytic_entity.dart'; part 'entities/product_analytic_entity.dart';
part 'entities/payment_method_analytic_entity.dart'; part 'entities/payment_method_analytic_entity.dart';
part 'entities/purchasing_analytic_entity.dart';
part 'entities/exclusive_summary_entity.dart';
part 'failures/analytic_failure.dart'; part 'failures/analytic_failure.dart';
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,119 @@
part of '../analytic.dart';
@freezed
class ExclusiveSummary with _$ExclusiveSummary {
const factory ExclusiveSummary({
required String organizationId,
required String outletId,
required ExclusiveSummaryPeriod period,
required ExclusiveSummarySummary summary,
required ExclusiveSummaryReimburse reimburse,
required List<ExclusiveSummaryBreakdown> hppBreakdown,
required List<ExclusiveSummaryBreakdown> operationalExpenseBreakdown,
required List<ExclusiveSummaryDaily> dailySummary,
required List<ExclusiveSummaryTransaction> dailyTransactions,
}) = _ExclusiveSummary;
factory ExclusiveSummary.empty() => ExclusiveSummary(
organizationId: '',
outletId: '',
period: ExclusiveSummaryPeriod.empty(),
summary: ExclusiveSummarySummary.empty(),
reimburse: ExclusiveSummaryReimburse.empty(),
hppBreakdown: [],
operationalExpenseBreakdown: [],
dailySummary: [],
dailyTransactions: [],
);
}
@freezed
class ExclusiveSummaryPeriod with _$ExclusiveSummaryPeriod {
const factory ExclusiveSummaryPeriod({
required DateTime dateFrom,
required DateTime dateTo,
}) = _ExclusiveSummaryPeriod;
factory ExclusiveSummaryPeriod.empty() => ExclusiveSummaryPeriod(
dateFrom: DateTime.fromMillisecondsSinceEpoch(0),
dateTo: DateTime.fromMillisecondsSinceEpoch(0),
);
}
@freezed
class ExclusiveSummarySummary with _$ExclusiveSummarySummary {
const factory ExclusiveSummarySummary({
required int sales,
required int hpp,
required int grossProfit,
required int salaryTotal,
required int salaryDw,
required int salaryStaff,
required int salaryOther,
required int otherOperationalExpenses,
required int operationalExpensesTotal,
required int totalCost,
required int netProfit,
}) = _ExclusiveSummarySummary;
factory ExclusiveSummarySummary.empty() => const ExclusiveSummarySummary(
sales: 0,
hpp: 0,
grossProfit: 0,
salaryTotal: 0,
salaryDw: 0,
salaryStaff: 0,
salaryOther: 0,
otherOperationalExpenses: 0,
operationalExpensesTotal: 0,
totalCost: 0,
netProfit: 0,
);
}
@freezed
class ExclusiveSummaryReimburse with _$ExclusiveSummaryReimburse {
const factory ExclusiveSummaryReimburse({
required int totalCost,
required int excludedSalaryStaff,
required int totalReimburse,
}) = _ExclusiveSummaryReimburse;
factory ExclusiveSummaryReimburse.empty() =>
const ExclusiveSummaryReimburse(
totalCost: 0,
excludedSalaryStaff: 0,
totalReimburse: 0,
);
}
@freezed
class ExclusiveSummaryBreakdown with _$ExclusiveSummaryBreakdown {
const factory ExclusiveSummaryBreakdown({
required String categoryCode,
required String categoryName,
required int amount,
required double percentage,
}) = _ExclusiveSummaryBreakdown;
}
@freezed
class ExclusiveSummaryDaily with _$ExclusiveSummaryDaily {
const factory ExclusiveSummaryDaily({
required DateTime date,
required int transactionCount,
required int totalCost,
}) = _ExclusiveSummaryDaily;
}
@freezed
class ExclusiveSummaryTransaction with _$ExclusiveSummaryTransaction {
const factory ExclusiveSummaryTransaction({
required DateTime date,
required String categoryCode,
required String categoryName,
required String description,
required int amount,
required String source,
}) = _ExclusiveSummaryTransaction;
}
@@ -0,0 +1,132 @@
part of '../analytic.dart';
@freezed
class PurchasingAnalytic with _$PurchasingAnalytic {
const factory PurchasingAnalytic({
required String organizationId,
required String outletId,
required String outletName,
required DateTime dateFrom,
required DateTime dateTo,
required String groupBy,
required PurchasingAnalyticSummary summary,
required List<PurchasingAnalyticData> data,
required List<PurchasingIngredientData> ingredientData,
required List<PurchasingVendorData> vendorData,
}) = _PurchasingAnalytic;
factory PurchasingAnalytic.empty() => PurchasingAnalytic(
organizationId: '',
outletId: '',
outletName: '',
dateFrom: DateTime.fromMillisecondsSinceEpoch(0),
dateTo: DateTime.fromMillisecondsSinceEpoch(0),
groupBy: '',
summary: PurchasingAnalyticSummary.empty(),
data: [],
ingredientData: [],
vendorData: [],
);
}
@freezed
class PurchasingAnalyticSummary with _$PurchasingAnalyticSummary {
const factory PurchasingAnalyticSummary({
required int totalPurchases,
required int rawMaterialPurchases,
required int expensePurchases,
required int totalPurchaseOrders,
required int rawMaterialPurchaseOrders,
required int expenseCount,
required int totalQuantity,
required double averagePurchaseOrderValue,
required int totalIngredients,
required int totalVendors,
}) = _PurchasingAnalyticSummary;
factory PurchasingAnalyticSummary.empty() =>
const PurchasingAnalyticSummary(
totalPurchases: 0,
rawMaterialPurchases: 0,
expensePurchases: 0,
totalPurchaseOrders: 0,
rawMaterialPurchaseOrders: 0,
expenseCount: 0,
totalQuantity: 0,
averagePurchaseOrderValue: 0,
totalIngredients: 0,
totalVendors: 0,
);
}
@freezed
class PurchasingAnalyticData with _$PurchasingAnalyticData {
const factory PurchasingAnalyticData({
required DateTime date,
required int purchases,
required int rawMaterialPurchases,
required int expensePurchases,
required int purchaseOrders,
required int rawMaterialPurchaseOrders,
required int expenseCount,
required int quantity,
required int ingredients,
required int vendors,
}) = _PurchasingAnalyticData;
factory PurchasingAnalyticData.empty() => PurchasingAnalyticData(
date: DateTime.fromMillisecondsSinceEpoch(0),
purchases: 0,
rawMaterialPurchases: 0,
expensePurchases: 0,
purchaseOrders: 0,
rawMaterialPurchaseOrders: 0,
expenseCount: 0,
quantity: 0,
ingredients: 0,
vendors: 0,
);
}
@freezed
class PurchasingIngredientData with _$PurchasingIngredientData {
const factory PurchasingIngredientData({
required String ingredientId,
required String ingredientName,
required int quantity,
required int totalCost,
required double averageUnitCost,
required int purchaseOrderCount,
}) = _PurchasingIngredientData;
factory PurchasingIngredientData.empty() =>
const PurchasingIngredientData(
ingredientId: '',
ingredientName: '',
quantity: 0,
totalCost: 0,
averageUnitCost: 0,
purchaseOrderCount: 0,
);
}
@freezed
class PurchasingVendorData with _$PurchasingVendorData {
const factory PurchasingVendorData({
required String vendorId,
required String vendorName,
required int totalCost,
required int purchaseOrderCount,
required int ingredientCount,
required int quantity,
}) = _PurchasingVendorData;
factory PurchasingVendorData.empty() => const PurchasingVendorData(
vendorId: '',
vendorName: '',
totalCost: 0,
purchaseOrderCount: 0,
ingredientCount: 0,
quantity: 0,
);
}
@@ -44,4 +44,17 @@ abstract class IAnalyticRepository {
required DateTime dateTo, required DateTime dateTo,
String? outletId, String? outletId,
}); });
Future<Either<AnalyticFailure, PurchasingAnalytic>> getPurchasing({
required DateTime dateFrom,
required DateTime dateTo,
String? outletId,
String groupBy = 'day',
});
Future<Either<AnalyticFailure, ExclusiveSummary>> getExclusiveSummary({
required DateTime dateFrom,
required DateTime dateTo,
String? outletId,
});
} }
@@ -12,3 +12,5 @@ part 'dto/inventory_analytic_dto.dart';
part 'dto/dashboard_analytic_dto.dart'; part 'dto/dashboard_analytic_dto.dart';
part 'dto/product_analytic_dto.dart'; part 'dto/product_analytic_dto.dart';
part 'dto/payment_method_analytic_dto.dart'; part 'dto/payment_method_analytic_dto.dart';
part 'dto/purchasing_analytic_dto.dart';
part 'dto/exclusive_summary_dto.dart';
File diff suppressed because it is too large Load Diff
@@ -637,3 +637,337 @@ Map<String, dynamic> _$$PaymentMethodSummaryDtoImplToJson(
'total_payments': instance.totalPayments, 'total_payments': instance.totalPayments,
'average_order_value': instance.averageOrderValue, 'average_order_value': instance.averageOrderValue,
}; };
_$PurchasingAnalyticDtoImpl _$$PurchasingAnalyticDtoImplFromJson(
Map<String, dynamic> json,
) => _$PurchasingAnalyticDtoImpl(
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),
groupBy: json['group_by'] as String?,
summary: json['summary'] == null
? null
: PurchasingAnalyticSummaryDto.fromJson(
json['summary'] as Map<String, dynamic>,
),
data: (json['data'] as List<dynamic>?)
?.map(
(e) => PurchasingAnalyticDataDto.fromJson(e as Map<String, dynamic>),
)
.toList(),
ingredientData: (json['ingredient_data'] as List<dynamic>?)
?.map(
(e) => PurchasingIngredientDataDto.fromJson(e as Map<String, dynamic>),
)
.toList(),
vendorData: (json['vendor_data'] as List<dynamic>?)
?.map((e) => PurchasingVendorDataDto.fromJson(e as Map<String, dynamic>))
.toList(),
);
Map<String, dynamic> _$$PurchasingAnalyticDtoImplToJson(
_$PurchasingAnalyticDtoImpl 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(),
'group_by': instance.groupBy,
'summary': instance.summary,
'data': instance.data,
'ingredient_data': instance.ingredientData,
'vendor_data': instance.vendorData,
};
_$PurchasingAnalyticSummaryDtoImpl _$$PurchasingAnalyticSummaryDtoImplFromJson(
Map<String, dynamic> json,
) => _$PurchasingAnalyticSummaryDtoImpl(
totalPurchases: json['total_purchases'] as num?,
rawMaterialPurchases: json['raw_material_purchases'] as num?,
expensePurchases: json['expense_purchases'] as num?,
totalPurchaseOrders: json['total_purchase_orders'] as num?,
rawMaterialPurchaseOrders: json['raw_material_purchase_orders'] as num?,
expenseCount: json['expense_count'] as num?,
totalQuantity: json['total_quantity'] as num?,
averagePurchaseOrderValue: json['average_purchase_order_value'] as num?,
totalIngredients: json['total_ingredients'] as num?,
totalVendors: json['total_vendors'] as num?,
);
Map<String, dynamic> _$$PurchasingAnalyticSummaryDtoImplToJson(
_$PurchasingAnalyticSummaryDtoImpl instance,
) => <String, dynamic>{
'total_purchases': instance.totalPurchases,
'raw_material_purchases': instance.rawMaterialPurchases,
'expense_purchases': instance.expensePurchases,
'total_purchase_orders': instance.totalPurchaseOrders,
'raw_material_purchase_orders': instance.rawMaterialPurchaseOrders,
'expense_count': instance.expenseCount,
'total_quantity': instance.totalQuantity,
'average_purchase_order_value': instance.averagePurchaseOrderValue,
'total_ingredients': instance.totalIngredients,
'total_vendors': instance.totalVendors,
};
_$PurchasingAnalyticDataDtoImpl _$$PurchasingAnalyticDataDtoImplFromJson(
Map<String, dynamic> json,
) => _$PurchasingAnalyticDataDtoImpl(
date: json['date'] == null ? null : DateTime.parse(json['date'] as String),
purchases: json['purchases'] as num?,
rawMaterialPurchases: json['raw_material_purchases'] as num?,
expensePurchases: json['expense_purchases'] as num?,
purchaseOrders: json['purchase_orders'] as num?,
rawMaterialPurchaseOrders: json['raw_material_purchase_orders'] as num?,
expenseCount: json['expense_count'] as num?,
quantity: json['quantity'] as num?,
ingredients: json['ingredients'] as num?,
vendors: json['vendors'] as num?,
);
Map<String, dynamic> _$$PurchasingAnalyticDataDtoImplToJson(
_$PurchasingAnalyticDataDtoImpl instance,
) => <String, dynamic>{
'date': instance.date?.toIso8601String(),
'purchases': instance.purchases,
'raw_material_purchases': instance.rawMaterialPurchases,
'expense_purchases': instance.expensePurchases,
'purchase_orders': instance.purchaseOrders,
'raw_material_purchase_orders': instance.rawMaterialPurchaseOrders,
'expense_count': instance.expenseCount,
'quantity': instance.quantity,
'ingredients': instance.ingredients,
'vendors': instance.vendors,
};
_$PurchasingIngredientDataDtoImpl _$$PurchasingIngredientDataDtoImplFromJson(
Map<String, dynamic> json,
) => _$PurchasingIngredientDataDtoImpl(
ingredientId: json['ingredient_id'] as String?,
ingredientName: json['ingredient_name'] as String?,
quantity: json['quantity'] as num?,
totalCost: json['total_cost'] as num?,
averageUnitCost: json['average_unit_cost'] as num?,
purchaseOrderCount: json['purchase_order_count'] as num?,
);
Map<String, dynamic> _$$PurchasingIngredientDataDtoImplToJson(
_$PurchasingIngredientDataDtoImpl instance,
) => <String, dynamic>{
'ingredient_id': instance.ingredientId,
'ingredient_name': instance.ingredientName,
'quantity': instance.quantity,
'total_cost': instance.totalCost,
'average_unit_cost': instance.averageUnitCost,
'purchase_order_count': instance.purchaseOrderCount,
};
_$PurchasingVendorDataDtoImpl _$$PurchasingVendorDataDtoImplFromJson(
Map<String, dynamic> json,
) => _$PurchasingVendorDataDtoImpl(
vendorId: json['vendor_id'] as String?,
vendorName: json['vendor_name'] as String?,
totalCost: json['total_cost'] as num?,
purchaseOrderCount: json['purchase_order_count'] as num?,
ingredientCount: json['ingredient_count'] as num?,
quantity: json['quantity'] as num?,
);
Map<String, dynamic> _$$PurchasingVendorDataDtoImplToJson(
_$PurchasingVendorDataDtoImpl instance,
) => <String, dynamic>{
'vendor_id': instance.vendorId,
'vendor_name': instance.vendorName,
'total_cost': instance.totalCost,
'purchase_order_count': instance.purchaseOrderCount,
'ingredient_count': instance.ingredientCount,
'quantity': instance.quantity,
};
_$ExclusiveSummaryDtoImpl _$$ExclusiveSummaryDtoImplFromJson(
Map<String, dynamic> json,
) => _$ExclusiveSummaryDtoImpl(
organizationId: json['organization_id'] as String?,
outletId: json['outlet_id'] as String?,
period: json['period'] == null
? null
: ExclusiveSummaryPeriodDto.fromJson(
json['period'] as Map<String, dynamic>,
),
summary: json['summary'] == null
? null
: ExclusiveSummarySummaryDto.fromJson(
json['summary'] as Map<String, dynamic>,
),
reimburse: json['reimburse'] == null
? null
: ExclusiveSummaryReimburseDto.fromJson(
json['reimburse'] as Map<String, dynamic>,
),
hppBreakdown: (json['hpp_breakdown'] as List<dynamic>?)
?.map(
(e) => ExclusiveSummaryBreakdownDto.fromJson(e as Map<String, dynamic>),
)
.toList(),
operationalExpenseBreakdown:
(json['operational_expense_breakdown'] as List<dynamic>?)
?.map(
(e) => ExclusiveSummaryBreakdownDto.fromJson(
e as Map<String, dynamic>,
),
)
.toList(),
dailySummary: (json['daily_summary'] as List<dynamic>?)
?.map((e) => ExclusiveSummaryDailyDto.fromJson(e as Map<String, dynamic>))
.toList(),
dailyTransactions: (json['daily_transactions'] as List<dynamic>?)
?.map(
(e) =>
ExclusiveSummaryTransactionDto.fromJson(e as Map<String, dynamic>),
)
.toList(),
);
Map<String, dynamic> _$$ExclusiveSummaryDtoImplToJson(
_$ExclusiveSummaryDtoImpl instance,
) => <String, dynamic>{
'organization_id': instance.organizationId,
'outlet_id': instance.outletId,
'period': instance.period,
'summary': instance.summary,
'reimburse': instance.reimburse,
'hpp_breakdown': instance.hppBreakdown,
'operational_expense_breakdown': instance.operationalExpenseBreakdown,
'daily_summary': instance.dailySummary,
'daily_transactions': instance.dailyTransactions,
};
_$ExclusiveSummaryPeriodDtoImpl _$$ExclusiveSummaryPeriodDtoImplFromJson(
Map<String, dynamic> json,
) => _$ExclusiveSummaryPeriodDtoImpl(
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),
);
Map<String, dynamic> _$$ExclusiveSummaryPeriodDtoImplToJson(
_$ExclusiveSummaryPeriodDtoImpl instance,
) => <String, dynamic>{
'date_from': instance.dateFrom?.toIso8601String(),
'date_to': instance.dateTo?.toIso8601String(),
};
_$ExclusiveSummarySummaryDtoImpl _$$ExclusiveSummarySummaryDtoImplFromJson(
Map<String, dynamic> json,
) => _$ExclusiveSummarySummaryDtoImpl(
sales: json['sales'] as num?,
hpp: json['hpp'] as num?,
grossProfit: json['gross_profit'] as num?,
salaryTotal: json['salary_total'] as num?,
salaryDw: json['salary_dw'] as num?,
salaryStaff: json['salary_staff'] as num?,
salaryOther: json['salary_other'] as num?,
otherOperationalExpenses: json['other_operational_expenses'] as num?,
operationalExpensesTotal: json['operational_expenses_total'] as num?,
totalCost: json['total_cost'] as num?,
netProfit: json['net_profit'] as num?,
);
Map<String, dynamic> _$$ExclusiveSummarySummaryDtoImplToJson(
_$ExclusiveSummarySummaryDtoImpl instance,
) => <String, dynamic>{
'sales': instance.sales,
'hpp': instance.hpp,
'gross_profit': instance.grossProfit,
'salary_total': instance.salaryTotal,
'salary_dw': instance.salaryDw,
'salary_staff': instance.salaryStaff,
'salary_other': instance.salaryOther,
'other_operational_expenses': instance.otherOperationalExpenses,
'operational_expenses_total': instance.operationalExpensesTotal,
'total_cost': instance.totalCost,
'net_profit': instance.netProfit,
};
_$ExclusiveSummaryReimburseDtoImpl _$$ExclusiveSummaryReimburseDtoImplFromJson(
Map<String, dynamic> json,
) => _$ExclusiveSummaryReimburseDtoImpl(
totalCost: json['total_cost'] as num?,
excludedSalaryStaff: json['excluded_salary_staff'] as num?,
totalReimburse: json['total_reimburse'] as num?,
);
Map<String, dynamic> _$$ExclusiveSummaryReimburseDtoImplToJson(
_$ExclusiveSummaryReimburseDtoImpl instance,
) => <String, dynamic>{
'total_cost': instance.totalCost,
'excluded_salary_staff': instance.excludedSalaryStaff,
'total_reimburse': instance.totalReimburse,
};
_$ExclusiveSummaryBreakdownDtoImpl _$$ExclusiveSummaryBreakdownDtoImplFromJson(
Map<String, dynamic> json,
) => _$ExclusiveSummaryBreakdownDtoImpl(
categoryCode: json['category_code'] as String?,
categoryName: json['category_name'] as String?,
amount: json['amount'] as num?,
percentage: json['percentage'] as num?,
);
Map<String, dynamic> _$$ExclusiveSummaryBreakdownDtoImplToJson(
_$ExclusiveSummaryBreakdownDtoImpl instance,
) => <String, dynamic>{
'category_code': instance.categoryCode,
'category_name': instance.categoryName,
'amount': instance.amount,
'percentage': instance.percentage,
};
_$ExclusiveSummaryDailyDtoImpl _$$ExclusiveSummaryDailyDtoImplFromJson(
Map<String, dynamic> json,
) => _$ExclusiveSummaryDailyDtoImpl(
date: json['date'] == null ? null : DateTime.parse(json['date'] as String),
transactionCount: json['transaction_count'] as num?,
totalCost: json['total_cost'] as num?,
);
Map<String, dynamic> _$$ExclusiveSummaryDailyDtoImplToJson(
_$ExclusiveSummaryDailyDtoImpl instance,
) => <String, dynamic>{
'date': instance.date?.toIso8601String(),
'transaction_count': instance.transactionCount,
'total_cost': instance.totalCost,
};
_$ExclusiveSummaryTransactionDtoImpl
_$$ExclusiveSummaryTransactionDtoImplFromJson(Map<String, dynamic> json) =>
_$ExclusiveSummaryTransactionDtoImpl(
date: json['date'] == null
? null
: DateTime.parse(json['date'] as String),
categoryCode: json['category_code'] as String?,
categoryName: json['category_name'] as String?,
description: json['description'] as String?,
amount: json['amount'] as num?,
source: json['source'] as String?,
);
Map<String, dynamic> _$$ExclusiveSummaryTransactionDtoImplToJson(
_$ExclusiveSummaryTransactionDtoImpl instance,
) => <String, dynamic>{
'date': instance.date?.toIso8601String(),
'category_code': instance.categoryCode,
'category_name': instance.categoryName,
'description': instance.description,
'amount': instance.amount,
'source': instance.source,
};
@@ -231,4 +231,68 @@ class AnalyticRemoteDataProvider {
return DC.error(AnalyticFailure.serverError(e)); return DC.error(AnalyticFailure.serverError(e));
} }
} }
Future<DC<AnalyticFailure, PurchasingAnalyticDto>> fetchPurchasing({
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.purchasingAnalytic,
params: params,
headers: getAuthorizationHeader(),
);
if (response.data['data'] == null) {
return DC.error(AnalyticFailure.empty());
}
final dto = PurchasingAnalyticDto.fromJson(response.data['data']);
return DC.data(dto);
} on ApiFailure catch (e, s) {
log('fetchPurchasingError', name: _logName, error: e, stackTrace: s);
return DC.error(AnalyticFailure.serverError(e));
}
}
Future<DC<AnalyticFailure, ExclusiveSummaryDto>> fetchExclusiveSummary({
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.exclusiveSummaryAnalytic,
params: params,
headers: getAuthorizationHeader(),
);
if (response.data['data'] == null) {
return DC.error(AnalyticFailure.empty());
}
final dto = ExclusiveSummaryDto.fromJson(response.data['data']);
return DC.data(dto);
} on ApiFailure catch (e, s) {
log('fetchExclusiveSummaryError', name: _logName, error: e, stackTrace: s);
return DC.error(AnalyticFailure.serverError(e));
}
}
} }
@@ -0,0 +1,181 @@
part of '../analytic_dtos.dart';
@freezed
class ExclusiveSummaryDto with _$ExclusiveSummaryDto {
const ExclusiveSummaryDto._();
const factory ExclusiveSummaryDto({
@JsonKey(name: 'organization_id') String? organizationId,
@JsonKey(name: 'outlet_id') String? outletId,
@JsonKey(name: 'period') ExclusiveSummaryPeriodDto? period,
@JsonKey(name: 'summary') ExclusiveSummarySummaryDto? summary,
@JsonKey(name: 'reimburse') ExclusiveSummaryReimburseDto? reimburse,
@JsonKey(name: 'hpp_breakdown')
List<ExclusiveSummaryBreakdownDto>? hppBreakdown,
@JsonKey(name: 'operational_expense_breakdown')
List<ExclusiveSummaryBreakdownDto>? operationalExpenseBreakdown,
@JsonKey(name: 'daily_summary')
List<ExclusiveSummaryDailyDto>? dailySummary,
@JsonKey(name: 'daily_transactions')
List<ExclusiveSummaryTransactionDto>? dailyTransactions,
}) = _ExclusiveSummaryDto;
factory ExclusiveSummaryDto.fromJson(Map<String, dynamic> json) =>
_$ExclusiveSummaryDtoFromJson(json);
ExclusiveSummary toDomain() => ExclusiveSummary(
organizationId: organizationId ?? '',
outletId: outletId ?? '',
period: period?.toDomain() ?? ExclusiveSummaryPeriod.empty(),
summary: summary?.toDomain() ?? ExclusiveSummarySummary.empty(),
reimburse: reimburse?.toDomain() ?? ExclusiveSummaryReimburse.empty(),
hppBreakdown: hppBreakdown?.map((e) => e.toDomain()).toList() ?? [],
operationalExpenseBreakdown:
operationalExpenseBreakdown?.map((e) => e.toDomain()).toList() ?? [],
dailySummary: dailySummary?.map((e) => e.toDomain()).toList() ?? [],
dailyTransactions:
dailyTransactions?.map((e) => e.toDomain()).toList() ?? [],
);
}
@freezed
class ExclusiveSummaryPeriodDto with _$ExclusiveSummaryPeriodDto {
const ExclusiveSummaryPeriodDto._();
const factory ExclusiveSummaryPeriodDto({
@JsonKey(name: 'date_from') DateTime? dateFrom,
@JsonKey(name: 'date_to') DateTime? dateTo,
}) = _ExclusiveSummaryPeriodDto;
factory ExclusiveSummaryPeriodDto.fromJson(Map<String, dynamic> json) =>
_$ExclusiveSummaryPeriodDtoFromJson(json);
ExclusiveSummaryPeriod toDomain() => ExclusiveSummaryPeriod(
dateFrom: dateFrom ?? DateTime.fromMillisecondsSinceEpoch(0),
dateTo: dateTo ?? DateTime.fromMillisecondsSinceEpoch(0),
);
}
@freezed
class ExclusiveSummarySummaryDto with _$ExclusiveSummarySummaryDto {
const ExclusiveSummarySummaryDto._();
const factory ExclusiveSummarySummaryDto({
@JsonKey(name: 'sales') num? sales,
@JsonKey(name: 'hpp') num? hpp,
@JsonKey(name: 'gross_profit') num? grossProfit,
@JsonKey(name: 'salary_total') num? salaryTotal,
@JsonKey(name: 'salary_dw') num? salaryDw,
@JsonKey(name: 'salary_staff') num? salaryStaff,
@JsonKey(name: 'salary_other') num? salaryOther,
@JsonKey(name: 'other_operational_expenses') num? otherOperationalExpenses,
@JsonKey(name: 'operational_expenses_total') num? operationalExpensesTotal,
@JsonKey(name: 'total_cost') num? totalCost,
@JsonKey(name: 'net_profit') num? netProfit,
}) = _ExclusiveSummarySummaryDto;
factory ExclusiveSummarySummaryDto.fromJson(Map<String, dynamic> json) =>
_$ExclusiveSummarySummaryDtoFromJson(json);
ExclusiveSummarySummary toDomain() => ExclusiveSummarySummary(
sales: sales?.toInt() ?? 0,
hpp: hpp?.toInt() ?? 0,
grossProfit: grossProfit?.toInt() ?? 0,
salaryTotal: salaryTotal?.toInt() ?? 0,
salaryDw: salaryDw?.toInt() ?? 0,
salaryStaff: salaryStaff?.toInt() ?? 0,
salaryOther: salaryOther?.toInt() ?? 0,
otherOperationalExpenses: otherOperationalExpenses?.toInt() ?? 0,
operationalExpensesTotal: operationalExpensesTotal?.toInt() ?? 0,
totalCost: totalCost?.toInt() ?? 0,
netProfit: netProfit?.toInt() ?? 0,
);
}
@freezed
class ExclusiveSummaryReimburseDto with _$ExclusiveSummaryReimburseDto {
const ExclusiveSummaryReimburseDto._();
const factory ExclusiveSummaryReimburseDto({
@JsonKey(name: 'total_cost') num? totalCost,
@JsonKey(name: 'excluded_salary_staff') num? excludedSalaryStaff,
@JsonKey(name: 'total_reimburse') num? totalReimburse,
}) = _ExclusiveSummaryReimburseDto;
factory ExclusiveSummaryReimburseDto.fromJson(Map<String, dynamic> json) =>
_$ExclusiveSummaryReimburseDtoFromJson(json);
ExclusiveSummaryReimburse toDomain() => ExclusiveSummaryReimburse(
totalCost: totalCost?.toInt() ?? 0,
excludedSalaryStaff: excludedSalaryStaff?.toInt() ?? 0,
totalReimburse: totalReimburse?.toInt() ?? 0,
);
}
@freezed
class ExclusiveSummaryBreakdownDto with _$ExclusiveSummaryBreakdownDto {
const ExclusiveSummaryBreakdownDto._();
const factory ExclusiveSummaryBreakdownDto({
@JsonKey(name: 'category_code') String? categoryCode,
@JsonKey(name: 'category_name') String? categoryName,
@JsonKey(name: 'amount') num? amount,
@JsonKey(name: 'percentage') num? percentage,
}) = _ExclusiveSummaryBreakdownDto;
factory ExclusiveSummaryBreakdownDto.fromJson(Map<String, dynamic> json) =>
_$ExclusiveSummaryBreakdownDtoFromJson(json);
ExclusiveSummaryBreakdown toDomain() => ExclusiveSummaryBreakdown(
categoryCode: categoryCode ?? '',
categoryName: categoryName ?? '',
amount: amount?.toInt() ?? 0,
percentage: percentage?.toDouble() ?? 0,
);
}
@freezed
class ExclusiveSummaryDailyDto with _$ExclusiveSummaryDailyDto {
const ExclusiveSummaryDailyDto._();
const factory ExclusiveSummaryDailyDto({
@JsonKey(name: 'date') DateTime? date,
@JsonKey(name: 'transaction_count') num? transactionCount,
@JsonKey(name: 'total_cost') num? totalCost,
}) = _ExclusiveSummaryDailyDto;
factory ExclusiveSummaryDailyDto.fromJson(Map<String, dynamic> json) =>
_$ExclusiveSummaryDailyDtoFromJson(json);
ExclusiveSummaryDaily toDomain() => ExclusiveSummaryDaily(
date: date ?? DateTime.fromMillisecondsSinceEpoch(0),
transactionCount: transactionCount?.toInt() ?? 0,
totalCost: totalCost?.toInt() ?? 0,
);
}
@freezed
class ExclusiveSummaryTransactionDto with _$ExclusiveSummaryTransactionDto {
const ExclusiveSummaryTransactionDto._();
const factory ExclusiveSummaryTransactionDto({
@JsonKey(name: 'date') DateTime? date,
@JsonKey(name: 'category_code') String? categoryCode,
@JsonKey(name: 'category_name') String? categoryName,
@JsonKey(name: 'description') String? description,
@JsonKey(name: 'amount') num? amount,
@JsonKey(name: 'source') String? source,
}) = _ExclusiveSummaryTransactionDto;
factory ExclusiveSummaryTransactionDto.fromJson(Map<String, dynamic> json) =>
_$ExclusiveSummaryTransactionDtoFromJson(json);
ExclusiveSummaryTransaction toDomain() => ExclusiveSummaryTransaction(
date: date ?? DateTime.fromMillisecondsSinceEpoch(0),
categoryCode: categoryCode ?? '',
categoryName: categoryName ?? '',
description: description ?? '',
amount: amount?.toInt() ?? 0,
source: source ?? '',
);
}
@@ -0,0 +1,159 @@
part of '../analytic_dtos.dart';
@freezed
class PurchasingAnalyticDto with _$PurchasingAnalyticDto {
const PurchasingAnalyticDto._();
const factory PurchasingAnalyticDto({
@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,
@JsonKey(name: 'summary') PurchasingAnalyticSummaryDto? summary,
@JsonKey(name: 'data') List<PurchasingAnalyticDataDto>? data,
@JsonKey(name: 'ingredient_data')
List<PurchasingIngredientDataDto>? ingredientData,
@JsonKey(name: 'vendor_data') List<PurchasingVendorDataDto>? vendorData,
}) = _PurchasingAnalyticDto;
factory PurchasingAnalyticDto.fromJson(Map<String, dynamic> json) =>
_$PurchasingAnalyticDtoFromJson(json);
PurchasingAnalytic toDomain() => PurchasingAnalytic(
organizationId: organizationId ?? '',
outletId: outletId ?? '',
outletName: outletName ?? '',
dateFrom: dateFrom ?? DateTime.fromMillisecondsSinceEpoch(0),
dateTo: dateTo ?? DateTime.fromMillisecondsSinceEpoch(0),
groupBy: groupBy ?? '',
summary: summary?.toDomain() ?? PurchasingAnalyticSummary.empty(),
data: data?.map((e) => e.toDomain()).toList() ?? [],
ingredientData:
ingredientData?.map((e) => e.toDomain()).toList() ?? [],
vendorData: vendorData?.map((e) => e.toDomain()).toList() ?? [],
);
}
@freezed
class PurchasingAnalyticSummaryDto with _$PurchasingAnalyticSummaryDto {
const PurchasingAnalyticSummaryDto._();
const factory PurchasingAnalyticSummaryDto({
@JsonKey(name: 'total_purchases') num? totalPurchases,
@JsonKey(name: 'raw_material_purchases') num? rawMaterialPurchases,
@JsonKey(name: 'expense_purchases') num? expensePurchases,
@JsonKey(name: 'total_purchase_orders') num? totalPurchaseOrders,
@JsonKey(name: 'raw_material_purchase_orders') num? rawMaterialPurchaseOrders,
@JsonKey(name: 'expense_count') num? expenseCount,
@JsonKey(name: 'total_quantity') num? totalQuantity,
@JsonKey(name: 'average_purchase_order_value')
num? averagePurchaseOrderValue,
@JsonKey(name: 'total_ingredients') num? totalIngredients,
@JsonKey(name: 'total_vendors') num? totalVendors,
}) = _PurchasingAnalyticSummaryDto;
factory PurchasingAnalyticSummaryDto.fromJson(Map<String, dynamic> json) =>
_$PurchasingAnalyticSummaryDtoFromJson(json);
PurchasingAnalyticSummary toDomain() => PurchasingAnalyticSummary(
totalPurchases: totalPurchases?.toInt() ?? 0,
rawMaterialPurchases: rawMaterialPurchases?.toInt() ?? 0,
expensePurchases: expensePurchases?.toInt() ?? 0,
totalPurchaseOrders: totalPurchaseOrders?.toInt() ?? 0,
rawMaterialPurchaseOrders: rawMaterialPurchaseOrders?.toInt() ?? 0,
expenseCount: expenseCount?.toInt() ?? 0,
totalQuantity: totalQuantity?.toInt() ?? 0,
averagePurchaseOrderValue:
averagePurchaseOrderValue?.toDouble() ?? 0,
totalIngredients: totalIngredients?.toInt() ?? 0,
totalVendors: totalVendors?.toInt() ?? 0,
);
}
@freezed
class PurchasingAnalyticDataDto with _$PurchasingAnalyticDataDto {
const PurchasingAnalyticDataDto._();
const factory PurchasingAnalyticDataDto({
@JsonKey(name: 'date') DateTime? date,
@JsonKey(name: 'purchases') num? purchases,
@JsonKey(name: 'raw_material_purchases') num? rawMaterialPurchases,
@JsonKey(name: 'expense_purchases') num? expensePurchases,
@JsonKey(name: 'purchase_orders') num? purchaseOrders,
@JsonKey(name: 'raw_material_purchase_orders') num? rawMaterialPurchaseOrders,
@JsonKey(name: 'expense_count') num? expenseCount,
@JsonKey(name: 'quantity') num? quantity,
@JsonKey(name: 'ingredients') num? ingredients,
@JsonKey(name: 'vendors') num? vendors,
}) = _PurchasingAnalyticDataDto;
factory PurchasingAnalyticDataDto.fromJson(Map<String, dynamic> json) =>
_$PurchasingAnalyticDataDtoFromJson(json);
PurchasingAnalyticData toDomain() => PurchasingAnalyticData(
date: date ?? DateTime.fromMillisecondsSinceEpoch(0),
purchases: purchases?.toInt() ?? 0,
rawMaterialPurchases: rawMaterialPurchases?.toInt() ?? 0,
expensePurchases: expensePurchases?.toInt() ?? 0,
purchaseOrders: purchaseOrders?.toInt() ?? 0,
rawMaterialPurchaseOrders: rawMaterialPurchaseOrders?.toInt() ?? 0,
expenseCount: expenseCount?.toInt() ?? 0,
quantity: quantity?.toInt() ?? 0,
ingredients: ingredients?.toInt() ?? 0,
vendors: vendors?.toInt() ?? 0,
);
}
@freezed
class PurchasingIngredientDataDto with _$PurchasingIngredientDataDto {
const PurchasingIngredientDataDto._();
const factory PurchasingIngredientDataDto({
@JsonKey(name: 'ingredient_id') String? ingredientId,
@JsonKey(name: 'ingredient_name') String? ingredientName,
@JsonKey(name: 'quantity') num? quantity,
@JsonKey(name: 'total_cost') num? totalCost,
@JsonKey(name: 'average_unit_cost') num? averageUnitCost,
@JsonKey(name: 'purchase_order_count') num? purchaseOrderCount,
}) = _PurchasingIngredientDataDto;
factory PurchasingIngredientDataDto.fromJson(Map<String, dynamic> json) =>
_$PurchasingIngredientDataDtoFromJson(json);
PurchasingIngredientData toDomain() => PurchasingIngredientData(
ingredientId: ingredientId ?? '',
ingredientName: ingredientName ?? '',
quantity: quantity?.toInt() ?? 0,
totalCost: totalCost?.toInt() ?? 0,
averageUnitCost: averageUnitCost?.toDouble() ?? 0,
purchaseOrderCount: purchaseOrderCount?.toInt() ?? 0,
);
}
@freezed
class PurchasingVendorDataDto with _$PurchasingVendorDataDto {
const PurchasingVendorDataDto._();
const factory PurchasingVendorDataDto({
@JsonKey(name: 'vendor_id') String? vendorId,
@JsonKey(name: 'vendor_name') String? vendorName,
@JsonKey(name: 'total_cost') num? totalCost,
@JsonKey(name: 'purchase_order_count') num? purchaseOrderCount,
@JsonKey(name: 'ingredient_count') num? ingredientCount,
@JsonKey(name: 'quantity') num? quantity,
}) = _PurchasingVendorDataDto;
factory PurchasingVendorDataDto.fromJson(Map<String, dynamic> json) =>
_$PurchasingVendorDataDtoFromJson(json);
PurchasingVendorData toDomain() => PurchasingVendorData(
vendorId: vendorId ?? '',
vendorName: vendorName ?? '',
totalCost: totalCost?.toInt() ?? 0,
purchaseOrderCount: purchaseOrderCount?.toInt() ?? 0,
ingredientCount: ingredientCount?.toInt() ?? 0,
quantity: quantity?.toInt() ?? 0,
);
}
@@ -176,4 +176,48 @@ class AnalyticRepository implements IAnalyticRepository {
return left(const AnalyticFailure.unexpectedError()); return left(const AnalyticFailure.unexpectedError());
} }
} }
@override
Future<Either<AnalyticFailure, PurchasingAnalytic>> getPurchasing({
required DateTime dateFrom,
required DateTime dateTo,
String? outletId,
String groupBy = 'day',
}) async {
try {
final result = await _dataProvider.fetchPurchasing(
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('getPurchasingError', name: _logName, error: e, stackTrace: s);
return left(const AnalyticFailure.unexpectedError());
}
}
@override
Future<Either<AnalyticFailure, ExclusiveSummary>> getExclusiveSummary({
required DateTime dateFrom,
required DateTime dateTo,
String? outletId,
}) async {
try {
final result = await _dataProvider.fetchExclusiveSummary(
dateFrom: dateFrom,
dateTo: dateTo,
outletId: _resolveOutletId(outletId),
);
if (result.hasError) return left(result.error!);
return right(result.data!.toDomain());
} catch (e, s) {
log('getExclusiveSummaryError', name: _logName, error: e, stackTrace: s);
return left(const AnalyticFailure.unexpectedError());
}
}
} }
+10
View File
@@ -13,6 +13,8 @@ import 'package:apskel_owner_flutter/application/analytic/category_analytic_load
as _i1038; as _i1038;
import 'package:apskel_owner_flutter/application/analytic/dashboard_analytic_loader/dashboard_analytic_loader_bloc.dart' import 'package:apskel_owner_flutter/application/analytic/dashboard_analytic_loader/dashboard_analytic_loader_bloc.dart'
as _i516; as _i516;
import 'package:apskel_owner_flutter/application/analytic/exclusive_summary_loader/exclusive_summary_loader_bloc.dart'
as _i702;
import 'package:apskel_owner_flutter/application/analytic/inventory_analytic_loader/inventory_analytic_loader_bloc.dart' import 'package:apskel_owner_flutter/application/analytic/inventory_analytic_loader/inventory_analytic_loader_bloc.dart'
as _i785; as _i785;
import 'package:apskel_owner_flutter/application/analytic/payment_method_analytic_loader/payment_method_analytic_loader_bloc.dart' import 'package:apskel_owner_flutter/application/analytic/payment_method_analytic_loader/payment_method_analytic_loader_bloc.dart'
@@ -21,6 +23,8 @@ import 'package:apskel_owner_flutter/application/analytic/product_analytic_loade
as _i221; as _i221;
import 'package:apskel_owner_flutter/application/analytic/profit_loss_loader/profit_loss_loader_bloc.dart' import 'package:apskel_owner_flutter/application/analytic/profit_loss_loader/profit_loss_loader_bloc.dart'
as _i11; as _i11;
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' import 'package:apskel_owner_flutter/application/analytic/sales_loader/sales_loader_bloc.dart'
as _i889; as _i889;
import 'package:apskel_owner_flutter/application/auth/auth_bloc.dart' as _i945; import 'package:apskel_owner_flutter/application/auth/auth_bloc.dart' as _i945;
@@ -248,6 +252,9 @@ extension GetItInjectableX on _i174.GetIt {
gh<_i850.OutletLocalDataProvider>(), gh<_i850.OutletLocalDataProvider>(),
), ),
); );
gh.factory<_i755.PurchasingAnalyticLoaderBloc>(
() => _i755.PurchasingAnalyticLoaderBloc(gh<_i477.IAnalyticRepository>()),
);
gh.factory<_i889.SalesLoaderBloc>( gh.factory<_i889.SalesLoaderBloc>(
() => _i889.SalesLoaderBloc(gh<_i477.IAnalyticRepository>()), () => _i889.SalesLoaderBloc(gh<_i477.IAnalyticRepository>()),
); );
@@ -266,6 +273,9 @@ extension GetItInjectableX on _i174.GetIt {
gh.factory<_i516.DashboardAnalyticLoaderBloc>( gh.factory<_i516.DashboardAnalyticLoaderBloc>(
() => _i516.DashboardAnalyticLoaderBloc(gh<_i477.IAnalyticRepository>()), () => _i516.DashboardAnalyticLoaderBloc(gh<_i477.IAnalyticRepository>()),
); );
gh.factory<_i702.ExclusiveSummaryLoaderBloc>(
() => _i702.ExclusiveSummaryLoaderBloc(gh<_i477.IAnalyticRepository>()),
);
gh.factory<_i785.InventoryAnalyticLoaderBloc>( gh.factory<_i785.InventoryAnalyticLoaderBloc>(
() => _i785.InventoryAnalyticLoaderBloc(gh<_i477.IAnalyticRepository>()), () => _i785.InventoryAnalyticLoaderBloc(gh<_i477.IAnalyticRepository>()),
); );
+66 -1
View File
@@ -431,5 +431,70 @@
"device": "Device", "device": "Device",
"@device": {}, "@device": {},
"profit_loss": "Laba Rugi", "profit_loss": "Laba Rugi",
"@profit_loss": {} "@profit_loss": {},
"exclusive_summary": "Exclusive Summary",
"@exclusive_summary": {},
"hpp_breakdown": "HPP Breakdown",
"@hpp_breakdown": {},
"operational_expense_breakdown": "Operational Expense Breakdown",
"@operational_expense_breakdown": {},
"daily_summary": "Daily Summary",
"@daily_summary": {},
"daily_transactions": "Daily Transactions",
"@daily_transactions": {},
"reimburse_summary": "Reimburse Summary",
"@reimburse_summary": {},
"total_reimburse": "Total Reimburse",
"@total_reimburse": {},
"excluded_salary_staff": "Excluded Salary Staff",
"@excluded_salary_staff": {},
"hpp": "HPP",
"@hpp": {},
"salary_total": "Total Salary",
"@salary_total": {},
"operational_expenses": "Operational Expenses",
"@operational_expenses": {},
"total_cost": "Total Cost",
"@total_cost": {},
"warning_title": "Warnings",
"@warning_title": {},
"warning_desc": "Activities deviating from standards — needs review.",
"@warning_desc": {},
"no_warning": "No warnings",
"@no_warning": {},
"no_warning_desc": "All activities are running normally.",
"@no_warning_desc": {},
"severity_high": "High",
"@severity_high": {},
"severity_medium": "Medium",
"@severity_medium": {},
"compared_to_previous_period": "Compared to previous period",
"@compared_to_previous_period": {},
"summary_today": "Today's Summary",
"@summary_today": {},
"summary_mtd": "MTD Summary",
"@summary_mtd": {},
"total_sales_label": "Total sales",
"@total_sales_label": {},
"total_raw_material": "Total raw material cost",
"@total_raw_material": {},
"net_profit_label": "Net profit",
"@net_profit_label": {},
"items_sold": "Items Sold",
"@items_sold": {},
"low_stock_warning": "Low Stock",
"@low_stock_warning": {},
"active_products": "Active Products",
"@active_products": {},
"today_condition": "Today's Condition",
"@today_condition": {},
"portion_sold": "{count} portions sold",
"@portion_sold": {
"placeholders": {
"count": {
"type": "int",
"example": "48"
}
}
}
} }
+69 -4
View File
@@ -48,11 +48,11 @@
"@reports": {}, "@reports": {},
"profile": "Profil", "profile": "Profil",
"@profile": {}, "@profile": {},
"sales_today": "Penjualan hari ini", "sales_today": "Omset hari ini",
"@sales_today": {}, "@sales_today": {},
"order": "Pesanan", "order": "Pesanan",
"@order": {}, "@order": {},
"sales": "Penjualan", "sales": "Omset",
"@sales": {}, "@sales": {},
"finance": "Keuangan", "finance": "Keuangan",
"@finance": {}, "@finance": {},
@@ -62,7 +62,7 @@
"@form": {}, "@form": {},
"schedule": "Jadwal", "schedule": "Jadwal",
"@schedule": {}, "@schedule": {},
"inventory": "Inventaris", "inventory": "Stok",
"@inventory": {}, "@inventory": {},
"customer": "Pelanggan", "customer": "Pelanggan",
"@customer": {}, "@customer": {},
@@ -431,5 +431,70 @@
"device": "Perangkat", "device": "Perangkat",
"@device": {}, "@device": {},
"profit_loss": "Laba Rugi", "profit_loss": "Laba Rugi",
"@profit_loss": {} "@profit_loss": {},
"exclusive_summary": "Ringkasan Eksklusif",
"@exclusive_summary": {},
"hpp_breakdown": "Rincian HPP",
"@hpp_breakdown": {},
"operational_expense_breakdown": "Rincian Biaya Operasional",
"@operational_expense_breakdown": {},
"daily_summary": "Ringkasan Harian",
"@daily_summary": {},
"daily_transactions": "Transaksi Harian",
"@daily_transactions": {},
"reimburse_summary": "Ringkasan Reimburse",
"@reimburse_summary": {},
"total_reimburse": "Total Reimburse",
"@total_reimburse": {},
"excluded_salary_staff": "Gaji Staf (Dikecualikan)",
"@excluded_salary_staff": {},
"hpp": "HPP",
"@hpp": {},
"salary_total": "Total Gaji",
"@salary_total": {},
"operational_expenses": "Biaya Operasional",
"@operational_expenses": {},
"total_cost": "Total Biaya",
"@total_cost": {},
"warning_title": "Peringatan",
"@warning_title": {},
"warning_desc": "Aktivitas yang menyimpang dari standar — perlu ditinjau.",
"@warning_desc": {},
"no_warning": "Tidak ada peringatan",
"@no_warning": {},
"no_warning_desc": "Semua aktivitas berjalan normal.",
"@no_warning_desc": {},
"severity_high": "Tinggi",
"@severity_high": {},
"severity_medium": "Sedang",
"@severity_medium": {},
"compared_to_previous_period": "Dibanding periode lalu",
"@compared_to_previous_period": {},
"summary_today": "Ringkasan Hari Ini",
"@summary_today": {},
"summary_mtd": "Ringkasan MTD",
"@summary_mtd": {},
"total_sales_label": "Total penjualan",
"@total_sales_label": {},
"total_raw_material": "Total biaya bahan baku",
"@total_raw_material": {},
"net_profit_label": "Laba bersih",
"@net_profit_label": {},
"items_sold": "Item Terjual",
"@items_sold": {},
"low_stock_warning": "Stok Menipis",
"@low_stock_warning": {},
"active_products": "Produk Aktif",
"@active_products": {},
"today_condition": "Kondisi Hari Ini",
"@today_condition": {},
"portion_sold": "{count} porsi terjual",
"@portion_sold": {
"placeholders": {
"count": {
"type": "int",
"example": "48"
}
}
}
} }
+174
View File
@@ -1306,6 +1306,180 @@ abstract class AppLocalizations {
/// In en, this message translates to: /// In en, this message translates to:
/// **'Laba Rugi'** /// **'Laba Rugi'**
String get profit_loss; String get profit_loss;
/// No description provided for @exclusive_summary.
///
/// In en, this message translates to:
/// **'Exclusive Summary'**
String get exclusive_summary;
/// No description provided for @hpp_breakdown.
///
/// In en, this message translates to:
/// **'HPP Breakdown'**
String get hpp_breakdown;
/// No description provided for @operational_expense_breakdown.
///
/// In en, this message translates to:
/// **'Operational Expense Breakdown'**
String get operational_expense_breakdown;
/// No description provided for @daily_summary.
///
/// In en, this message translates to:
/// **'Daily Summary'**
String get daily_summary;
/// No description provided for @daily_transactions.
///
/// In en, this message translates to:
/// **'Daily Transactions'**
String get daily_transactions;
/// No description provided for @reimburse_summary.
///
/// In en, this message translates to:
/// **'Reimburse Summary'**
String get reimburse_summary;
/// No description provided for @total_reimburse.
///
/// In en, this message translates to:
/// **'Total Reimburse'**
String get total_reimburse;
/// No description provided for @excluded_salary_staff.
///
/// In en, this message translates to:
/// **'Excluded Salary Staff'**
String get excluded_salary_staff;
/// No description provided for @hpp.
///
/// In en, this message translates to:
/// **'HPP'**
String get hpp;
/// No description provided for @salary_total.
///
/// In en, this message translates to:
/// **'Total Salary'**
String get salary_total;
/// No description provided for @operational_expenses.
///
/// In en, this message translates to:
/// **'Operational Expenses'**
String get operational_expenses;
/// No description provided for @total_cost.
///
/// In en, this message translates to:
/// **'Total Cost'**
String get total_cost;
/// No description provided for @warning_title.
///
/// In en, this message translates to:
/// **'Warnings'**
String get warning_title;
/// No description provided for @warning_desc.
///
/// In en, this message translates to:
/// **'Activities deviating from standards — needs review.'**
String get warning_desc;
/// No description provided for @no_warning.
///
/// In en, this message translates to:
/// **'No warnings'**
String get no_warning;
/// No description provided for @no_warning_desc.
///
/// In en, this message translates to:
/// **'All activities are running normally.'**
String get no_warning_desc;
/// No description provided for @severity_high.
///
/// In en, this message translates to:
/// **'High'**
String get severity_high;
/// No description provided for @severity_medium.
///
/// In en, this message translates to:
/// **'Medium'**
String get severity_medium;
/// No description provided for @compared_to_previous_period.
///
/// In en, this message translates to:
/// **'Compared to previous period'**
String get compared_to_previous_period;
/// No description provided for @summary_today.
///
/// In en, this message translates to:
/// **'Today\'s Summary'**
String get summary_today;
/// No description provided for @summary_mtd.
///
/// In en, this message translates to:
/// **'MTD Summary'**
String get summary_mtd;
/// No description provided for @total_sales_label.
///
/// In en, this message translates to:
/// **'Total sales'**
String get total_sales_label;
/// No description provided for @total_raw_material.
///
/// In en, this message translates to:
/// **'Total raw material cost'**
String get total_raw_material;
/// No description provided for @net_profit_label.
///
/// In en, this message translates to:
/// **'Net profit'**
String get net_profit_label;
/// No description provided for @items_sold.
///
/// In en, this message translates to:
/// **'Items Sold'**
String get items_sold;
/// No description provided for @low_stock_warning.
///
/// In en, this message translates to:
/// **'Low Stock'**
String get low_stock_warning;
/// No description provided for @active_products.
///
/// In en, this message translates to:
/// **'Active Products'**
String get active_products;
/// No description provided for @today_condition.
///
/// In en, this message translates to:
/// **'Today\'s Condition'**
String get today_condition;
/// No description provided for @portion_sold.
///
/// In en, this message translates to:
/// **'{count} portions sold'**
String portion_sold(int count);
} }
class _AppLocalizationsDelegate extends LocalizationsDelegate<AppLocalizations> { class _AppLocalizationsDelegate extends LocalizationsDelegate<AppLocalizations> {
+89
View File
@@ -621,4 +621,93 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get profit_loss => 'Laba Rugi'; String get profit_loss => 'Laba Rugi';
@override
String get exclusive_summary => 'Exclusive Summary';
@override
String get hpp_breakdown => 'HPP Breakdown';
@override
String get operational_expense_breakdown => 'Operational Expense Breakdown';
@override
String get daily_summary => 'Daily Summary';
@override
String get daily_transactions => 'Daily Transactions';
@override
String get reimburse_summary => 'Reimburse Summary';
@override
String get total_reimburse => 'Total Reimburse';
@override
String get excluded_salary_staff => 'Excluded Salary Staff';
@override
String get hpp => 'HPP';
@override
String get salary_total => 'Total Salary';
@override
String get operational_expenses => 'Operational Expenses';
@override
String get total_cost => 'Total Cost';
@override
String get warning_title => 'Warnings';
@override
String get warning_desc => 'Activities deviating from standards — needs review.';
@override
String get no_warning => 'No warnings';
@override
String get no_warning_desc => 'All activities are running normally.';
@override
String get severity_high => 'High';
@override
String get severity_medium => 'Medium';
@override
String get compared_to_previous_period => 'Compared to previous period';
@override
String get summary_today => 'Today\'s Summary';
@override
String get summary_mtd => 'MTD Summary';
@override
String get total_sales_label => 'Total sales';
@override
String get total_raw_material => 'Total raw material cost';
@override
String get net_profit_label => 'Net profit';
@override
String get items_sold => 'Items Sold';
@override
String get low_stock_warning => 'Low Stock';
@override
String get active_products => 'Active Products';
@override
String get today_condition => 'Today\'s Condition';
@override
String portion_sold(int count) {
return '$count portions sold';
}
} }
+92 -3
View File
@@ -81,13 +81,13 @@ class AppLocalizationsId extends AppLocalizations {
String get profile => 'Profil'; String get profile => 'Profil';
@override @override
String get sales_today => 'Penjualan hari ini'; String get sales_today => 'Omset hari ini';
@override @override
String get order => 'Pesanan'; String get order => 'Pesanan';
@override @override
String get sales => 'Penjualan'; String get sales => 'Omset';
@override @override
String get finance => 'Keuangan'; String get finance => 'Keuangan';
@@ -102,7 +102,7 @@ class AppLocalizationsId extends AppLocalizations {
String get schedule => 'Jadwal'; String get schedule => 'Jadwal';
@override @override
String get inventory => 'Inventaris'; String get inventory => 'Stok';
@override @override
String get customer => 'Pelanggan'; String get customer => 'Pelanggan';
@@ -621,4 +621,93 @@ class AppLocalizationsId extends AppLocalizations {
@override @override
String get profit_loss => 'Laba Rugi'; String get profit_loss => 'Laba Rugi';
@override
String get exclusive_summary => 'Ringkasan Eksklusif';
@override
String get hpp_breakdown => 'Rincian HPP';
@override
String get operational_expense_breakdown => 'Rincian Biaya Operasional';
@override
String get daily_summary => 'Ringkasan Harian';
@override
String get daily_transactions => 'Transaksi Harian';
@override
String get reimburse_summary => 'Ringkasan Reimburse';
@override
String get total_reimburse => 'Total Reimburse';
@override
String get excluded_salary_staff => 'Gaji Staf (Dikecualikan)';
@override
String get hpp => 'HPP';
@override
String get salary_total => 'Total Gaji';
@override
String get operational_expenses => 'Biaya Operasional';
@override
String get total_cost => 'Total Biaya';
@override
String get warning_title => 'Peringatan';
@override
String get warning_desc => 'Aktivitas yang menyimpang dari standar — perlu ditinjau.';
@override
String get no_warning => 'Tidak ada peringatan';
@override
String get no_warning_desc => 'Semua aktivitas berjalan normal.';
@override
String get severity_high => 'Tinggi';
@override
String get severity_medium => 'Sedang';
@override
String get compared_to_previous_period => 'Dibanding periode lalu';
@override
String get summary_today => 'Ringkasan Hari Ini';
@override
String get summary_mtd => 'Ringkasan MTD';
@override
String get total_sales_label => 'Total penjualan';
@override
String get total_raw_material => 'Total biaya bahan baku';
@override
String get net_profit_label => 'Laba bersih';
@override
String get items_sold => 'Item Terjual';
@override
String get low_stock_warning => 'Stok Menipis';
@override
String get active_products => 'Produk Aktif';
@override
String get today_condition => 'Kondisi Hari Ini';
@override
String portion_sold(int count) {
return '$count porsi terjual';
}
} }
+17 -5
View File
@@ -1,3 +1,4 @@
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
@@ -25,12 +26,23 @@ void main() async {
kReleaseMode ? Environment.prod : Environment.dev, kReleaseMode ? Environment.prod : Environment.dev,
); );
// Setup Crashlytics
FlutterError.onError = FirebaseCrashlytics.instance.recordFlutterFatalError;
PlatformDispatcher.instance.onError = (error, stack) {
FirebaseCrashlytics.instance.recordError(error, stack, fatal: true);
return true;
};
// Initialize FCM after dependencies are ready // Initialize FCM after dependencies are ready
await getIt<FcmService>().initialize( try {
onMessageTap: (message) { await getIt<FcmService>().initialize(
debugPrint('[FCM] Navigate based on: ${message.data}'); onMessageTap: (message) {
}, debugPrint('[FCM] Navigate based on: ${message.data}');
); },
);
} catch (e) {
debugPrint('[FCM] Initialization failed: $e');
}
runApp(const AppWidget()); runApp(const AppWidget());
} }
@@ -14,6 +14,10 @@ import 'package:flutter/widgets.dart';
class $AssetsIconsGen { class $AssetsIconsGen {
const $AssetsIconsGen(); const $AssetsIconsGen();
/// File path: assets/icons/ic-report-exclusive-summary.png
AssetGenImage get icReportExclusiveSummary =>
const AssetGenImage('assets/icons/ic-report-exclusive-summary.png');
/// File path: assets/icons/ic-report-product.png /// File path: assets/icons/ic-report-product.png
AssetGenImage get icReportProduct => AssetGenImage get icReportProduct =>
const AssetGenImage('assets/icons/ic-report-product.png'); const AssetGenImage('assets/icons/ic-report-product.png');
@@ -30,12 +34,18 @@ class $AssetsIconsGen {
AssetGenImage get icReportSales => AssetGenImage get icReportSales =>
const AssetGenImage('assets/icons/ic-report-sales.png'); const AssetGenImage('assets/icons/ic-report-sales.png');
/// File path: assets/icons/ic-report-stock.png
AssetGenImage get icReportStock =>
const AssetGenImage('assets/icons/ic-report-stock.png');
/// List of all assets /// List of all assets
List<AssetGenImage> get values => [ List<AssetGenImage> get values => [
icReportExclusiveSummary,
icReportProduct, icReportProduct,
icReportProfitLoss, icReportProfitLoss,
icReportPurchase, icReportPurchase,
icReportSales, icReportSales,
icReportStock,
]; ];
} }
@@ -1 +0,0 @@
// TODO: define your code
@@ -235,7 +235,7 @@ class DateRangePickerFieldOutlined extends StatefulWidget {
final String? errorText; final String? errorText;
const DateRangePickerFieldOutlined({ const DateRangePickerFieldOutlined({
Key? key, super.key,
this.label, this.label,
this.placeholder = 'Pilih rentang tanggal', this.placeholder = 'Pilih rentang tanggal',
this.startDate, this.startDate,
@@ -246,7 +246,7 @@ class DateRangePickerFieldOutlined extends StatefulWidget {
this.primaryColor = AppColor.primary, this.primaryColor = AppColor.primary,
this.enabled = true, this.enabled = true,
this.errorText, this.errorText,
}) : super(key: key); });
@override @override
State<DateRangePickerFieldOutlined> createState() => State<DateRangePickerFieldOutlined> createState() =>
@@ -7,8 +7,7 @@ class EmptySearchWidget extends StatelessWidget {
final String? searchQuery; final String? searchQuery;
final VoidCallback? onClear; final VoidCallback? onClear;
const EmptySearchWidget({Key? key, this.searchQuery, this.onClear}) const EmptySearchWidget({super.key, this.searchQuery, this.onClear});
: super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -1,3 +1,4 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:package_info_plus/package_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart';
@@ -75,7 +76,9 @@ class _AboutAppPageState extends State<AboutAppPage>
deviceInfo = device; deviceInfo = device;
}); });
} catch (e) { } catch (e) {
print('Error loading app info: $e'); if (kDebugMode) {
print('Error loading app info: $e');
}
} }
} }
+70 -188
View File
@@ -16,86 +16,12 @@ import 'widgets/email_field.dart';
import 'widgets/password_field.dart'; import 'widgets/password_field.dart';
@RoutePage() @RoutePage()
class LoginPage extends StatefulWidget implements AutoRouteWrapper { class LoginPage extends StatelessWidget implements AutoRouteWrapper {
const LoginPage({super.key}); const LoginPage({super.key});
@override
State<LoginPage> createState() => _LoginPageState();
@override @override
Widget wrappedRoute(BuildContext context) => Widget wrappedRoute(BuildContext context) =>
BlocProvider(create: (_) => getIt<LoginFormBloc>(), child: this); BlocProvider(create: (_) => getIt<LoginFormBloc>(), child: this);
}
class _LoginPageState extends State<LoginPage> with TickerProviderStateMixin {
late AnimationController _fadeController;
late AnimationController _slideController;
late AnimationController _backgroundController;
late AnimationController _floatingController;
late Animation<double> _fadeAnimation;
late Animation<Offset> _slideAnimation;
late Animation<double> _backgroundAnimation;
late Animation<double> _floatingAnimation;
@override
void initState() {
super.initState();
_fadeController = AnimationController(
duration: const Duration(milliseconds: 1500),
vsync: this,
);
_slideController = AnimationController(
duration: const Duration(milliseconds: 1200),
vsync: this,
);
_backgroundController = AnimationController(
duration: const Duration(seconds: 10),
vsync: this,
)..repeat();
_floatingController = AnimationController(
duration: const Duration(seconds: 6),
vsync: this,
)..repeat(reverse: true);
_fadeAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(parent: _fadeController, curve: Curves.easeInOut),
);
_slideAnimation =
Tween<Offset>(begin: const Offset(0, 0.3), end: Offset.zero).animate(
CurvedAnimation(parent: _slideController, curve: Curves.easeOutCubic),
);
_backgroundAnimation = Tween<double>(
begin: 0.0,
end: 2 * math.pi,
).animate(_backgroundController);
_floatingAnimation = Tween<double>(begin: -20.0, end: 20.0).animate(
CurvedAnimation(parent: _floatingController, curve: Curves.easeInOut),
);
_fadeController.forward();
_slideController.forward();
}
@override
void dispose() {
_fadeController.dispose();
_slideController.dispose();
_backgroundController.dispose();
_floatingController.dispose();
super.dispose();
}
Future<void> _handleLogin() async {
context.read<LoginFormBloc>().add(LoginFormEvent.submitted());
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -117,83 +43,63 @@ class _LoginPageState extends State<LoginPage> with TickerProviderStateMixin {
); );
}, },
child: Scaffold( child: Scaffold(
body: AnimatedBuilder( body: Container(
animation: Listenable.merge([ decoration: const BoxDecoration(
_backgroundController, gradient: LinearGradient(
_floatingController, begin: Alignment.topLeft,
]), end: Alignment.bottomRight,
builder: (context, child) { colors: AppColor.primaryGradient,
return Container( ),
decoration: const BoxDecoration( ),
gradient: LinearGradient( child: Stack(
begin: Alignment.topLeft, children: [
end: Alignment.bottomRight, _buildStaticBackground(context),
colors: AppColor.primaryGradient, SafeArea(
), child: Center(
), child: SingleChildScrollView(
child: Stack( padding: EdgeInsets.symmetric(horizontal: AppValue.padding),
children: [ child: BlocBuilder<LoginFormBloc, LoginFormState>(
// Animated background elements builder: (context, state) {
_buildAnimatedBackground(), return Column(
mainAxisAlignment: MainAxisAlignment.center,
// Main content children: [
SafeArea( _buildLogo(context),
child: Center( const SpaceHeight(48),
child: SingleChildScrollView( _buildLoginCard(
padding: EdgeInsets.symmetric( context,
horizontal: AppValue.padding, state.isSubmitting,
), state.showErrorMessages,
child: FadeTransition(
opacity: _fadeAnimation,
child: SlideTransition(
position: _slideAnimation,
child: BlocBuilder<LoginFormBloc, LoginFormState>(
builder: (context, state) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_buildLogo(context),
SpaceHeight(48),
_buildLoginCard(
context,
state.isSubmitting,
state.showErrorMessages,
),
],
);
},
), ),
), ],
), );
), },
), ),
), ),
], ),
), ),
); ],
}, ),
), ),
), ),
); );
} }
Widget _buildAnimatedBackground() { Widget _buildStaticBackground(BuildContext context) {
final size = MediaQuery.of(context).size;
return Stack( return Stack(
children: [ children: [
// Floating circles // Static circles
...List.generate(6, (index) { ...List.generate(6, (index) {
final double size = 80 + (index * 40); final double circleSize = 80 + (index * 40);
final double left = final double left = (index * 60.0) % size.width;
(index * 60.0) % MediaQuery.of(context).size.width; final double top = (index * 120.0) % size.height;
final double top =
(index * 120.0) % MediaQuery.of(context).size.height;
return Positioned( return Positioned(
left: left + math.sin(_backgroundAnimation.value + index) * 30, left: left,
top: top + _floatingAnimation.value + (index * 10), top: top,
child: Container( child: Container(
width: size, width: circleSize,
height: size, height: circleSize,
decoration: BoxDecoration( decoration: BoxDecoration(
shape: BoxShape.circle, shape: BoxShape.circle,
color: Colors.white.withOpacity(0.1), color: Colors.white.withOpacity(0.1),
@@ -206,12 +112,12 @@ class _LoginPageState extends State<LoginPage> with TickerProviderStateMixin {
); );
}), }),
// Rotating geometric shapes // Geometric shapes
Positioned( Positioned(
top: 100, top: 100,
right: 50, right: 50,
child: Transform.rotate( child: Transform.rotate(
angle: _backgroundAnimation.value, angle: math.pi / 4,
child: Container( child: Container(
width: 60, width: 60,
height: 60, height: 60,
@@ -230,46 +136,37 @@ class _LoginPageState extends State<LoginPage> with TickerProviderStateMixin {
Positioned( Positioned(
bottom: 150, bottom: 150,
left: 30, left: 30,
child: Transform.rotate( child: Container(
angle: -_backgroundAnimation.value * 0.5, width: 80,
child: Container( height: 80,
width: 80, decoration: BoxDecoration(
height: 80, color: Colors.white.withOpacity(0.06),
decoration: BoxDecoration( shape: BoxShape.circle,
color: Colors.white.withOpacity(0.06), border: Border.all(
shape: BoxShape.circle, color: Colors.white.withOpacity(0.12),
border: Border.all( width: 1,
color: Colors.white.withOpacity(0.12),
width: 1,
),
), ),
), ),
), ),
), ),
// Floating particles // Static particles
...List.generate(8, (index) { ...List.generate(8, (index) {
return Positioned( return Positioned(
left: (index * 45.0) % MediaQuery.of(context).size.width, left: (index * 45.0) % size.width,
top: (index * 80.0) % MediaQuery.of(context).size.height, top: (index * 80.0) % size.height,
child: Transform.translate( child: Container(
offset: Offset( width: 4.0 + (index % 3) * 2,
math.sin(_backgroundAnimation.value + index * 0.5) * 20, height: 4.0 + (index % 3) * 2,
math.cos(_backgroundAnimation.value + index * 0.3) * 15, decoration: BoxDecoration(
), shape: BoxShape.circle,
child: Container( color: Colors.white.withOpacity(0.3),
width: 4 + (index % 3) * 2,
height: 4 + (index % 3) * 2,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.white.withOpacity(0.3),
),
), ),
), ),
); );
}), }),
// Gradient overlay for better text readability // Gradient overlay
Container( Container(
decoration: BoxDecoration( decoration: BoxDecoration(
gradient: LinearGradient( gradient: LinearGradient(
@@ -295,29 +192,13 @@ class _LoginPageState extends State<LoginPage> with TickerProviderStateMixin {
style: AppStyle.h1.copyWith( style: AppStyle.h1.copyWith(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: AppColor.white, color: AppColor.white,
shadows: [
Shadow(
offset: const Offset(0, 2),
blurRadius: 10,
color: Colors.black.withOpacity(0.3),
),
],
), ),
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
const SpaceHeight(8), const SpaceHeight(8),
Text( Text(
context.lang.login_desc, context.lang.login_desc,
style: AppStyle.lg.copyWith( style: AppStyle.lg.copyWith(color: AppColor.textLight),
color: AppColor.textLight,
shadows: [
Shadow(
offset: const Offset(0, 1),
blurRadius: 5,
color: Colors.black.withOpacity(0.2),
),
],
),
), ),
], ],
); );
@@ -353,7 +234,6 @@ class _LoginPageState extends State<LoginPage> with TickerProviderStateMixin {
autovalidateMode: showErrorMessages autovalidateMode: showErrorMessages
? AutovalidateMode.always ? AutovalidateMode.always
: AutovalidateMode.disabled, : AutovalidateMode.disabled,
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
@@ -363,7 +243,7 @@ class _LoginPageState extends State<LoginPage> with TickerProviderStateMixin {
const SpaceHeight(16), const SpaceHeight(16),
_buildForgetPassword(context), _buildForgetPassword(context),
const SpaceHeight(32), const SpaceHeight(32),
_buildLoginButton(isLoading), _buildLoginButton(context, isLoading),
], ],
), ),
), ),
@@ -386,11 +266,13 @@ class _LoginPageState extends State<LoginPage> with TickerProviderStateMixin {
); );
} }
Widget _buildLoginButton(bool isLoading) { Widget _buildLoginButton(BuildContext context, bool isLoading) {
return AppElevatedButton( return AppElevatedButton(
text: context.lang.sign_in, text: context.lang.sign_in,
isLoading: isLoading, isLoading: isLoading,
onPressed: _handleLogin, onPressed: () {
context.read<LoginFormBloc>().add(LoginFormEvent.submitted());
},
); );
} }
} }
+4 -2
View File
@@ -13,14 +13,14 @@ class ErrorPage extends StatefulWidget {
final IconData? errorIcon; final IconData? errorIcon;
const ErrorPage({ const ErrorPage({
Key? key, super.key,
this.title, this.title,
this.message, this.message,
this.onRetry, this.onRetry,
this.onBack, this.onBack,
this.errorCode, this.errorCode,
this.errorIcon, this.errorIcon,
}) : super(key: key); });
@override @override
State<ErrorPage> createState() => _ErrorPageState(); State<ErrorPage> createState() => _ErrorPageState();
@@ -462,6 +462,8 @@ class _ErrorPageState extends State<ErrorPage> with TickerProviderStateMixin {
// Usage Example dengan berbagai variasi // Usage Example dengan berbagai variasi
class ErrorPageExamples extends StatelessWidget { class ErrorPageExamples extends StatelessWidget {
const ErrorPageExamples({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Column( return Column(
@@ -0,0 +1,890 @@
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 'package:shimmer/shimmer.dart';
import '../../../application/analytic/exclusive_summary_loader/exclusive_summary_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 '../../components/spacer/spacer.dart';
@RoutePage()
class ExclusiveSummaryPage extends StatefulWidget
implements AutoRouteWrapper {
const ExclusiveSummaryPage({super.key});
@override
State<ExclusiveSummaryPage> createState() => _ExclusiveSummaryPageState();
@override
Widget wrappedRoute(BuildContext context) => BlocProvider(
create: (context) => getIt<ExclusiveSummaryLoaderBloc>()
..add(ExclusiveSummaryLoaderEvent.fetched()),
child: this,
);
}
class _ExclusiveSummaryPageState extends State<ExclusiveSummaryPage>
with TickerProviderStateMixin {
late AnimationController _fadeController;
late AnimationController _slideController;
late Animation<double> _fadeAnimation;
late Animation<Offset> _slideAnimation;
@override
void initState() {
super.initState();
_fadeController = AnimationController(
duration: const Duration(milliseconds: 800),
vsync: this,
);
_slideController = AnimationController(
duration: const Duration(milliseconds: 900),
vsync: this,
);
_fadeAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(parent: _fadeController, curve: Curves.easeOut),
);
_slideAnimation =
Tween<Offset>(begin: const Offset(0, 0.3), end: Offset.zero).animate(
CurvedAnimation(parent: _slideController, curve: Curves.easeOutCubic),
);
_fadeController.forward();
_slideController.forward();
}
@override
void dispose() {
_fadeController.dispose();
_slideController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppColor.background,
body: BlocListener<ExclusiveSummaryLoaderBloc,
ExclusiveSummaryLoaderState>(
listenWhen: (prev, curr) =>
prev.dateFrom != curr.dateFrom || prev.dateTo != curr.dateTo,
listener: (context, state) {
context
.read<ExclusiveSummaryLoaderBloc>()
.add(ExclusiveSummaryLoaderEvent.fetched());
},
child: BlocBuilder<ExclusiveSummaryLoaderBloc,
ExclusiveSummaryLoaderState>(
builder: (context, state) {
return RefreshIndicator(
color: AppColor.primary,
onRefresh: () async {
context
.read<ExclusiveSummaryLoaderBloc>()
.add(ExclusiveSummaryLoaderEvent.fetched());
await context
.read<ExclusiveSummaryLoaderBloc>()
.stream
.firstWhere((s) => !s.isFetching);
},
child: CustomScrollView(
physics: const AlwaysScrollableScrollPhysics(),
slivers: [
// App Bar
SliverAppBar(
expandedHeight: 120,
floating: false,
pinned: true,
backgroundColor: AppColor.primary,
flexibleSpace: CustomAppBar(
title: context.lang.exclusive_summary,
),
),
// Date Range Picker
SliverToBoxAdapter(
child: FadeTransition(
opacity: _fadeAnimation,
child: Padding(
padding: const EdgeInsets.all(16),
child: DateRangePickerField(
maxDate: DateTime.now(),
startDate: state.dateFrom,
endDate: state.dateTo,
onChanged: (startDate, endDate) {
context.read<ExclusiveSummaryLoaderBloc>().add(
ExclusiveSummaryLoaderEvent.rangeDateChanged(
startDate!,
endDate!,
),
);
},
),
),
),
),
// Content
SliverToBoxAdapter(
child: SlideTransition(
position: _slideAnimation,
child: FadeTransition(
opacity: _fadeAnimation,
child: state.isFetching
? _buildShimmer()
: _buildContent(state.exclusiveSummary),
),
),
),
const SliverToBoxAdapter(child: SpaceHeight(80)),
],
),
);
},
),
),
);
}
// ─── SHIMMER ────────────────────────────────────────────────────────────────
Widget _buildShimmer() {
return Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
_shimmerBox(height: 160),
const SpaceHeight(16),
Row(children: [
Expanded(child: _shimmerBox(height: 100)),
const SpaceWidth(12),
Expanded(child: _shimmerBox(height: 100)),
]),
const SpaceHeight(16),
_shimmerBox(height: 200),
const SpaceHeight(16),
_shimmerBox(height: 150),
],
),
);
}
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),
),
),
);
}
// ─── CONTENT ────────────────────────────────────────────────────────────────
Widget _buildContent(ExclusiveSummary data) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildNetProfitCard(data.summary),
const SpaceHeight(16),
_buildSummaryGrid(data.summary),
const SpaceHeight(16),
_buildReimburseCard(data.reimburse),
const SpaceHeight(16),
if (data.hppBreakdown.isNotEmpty) ...[
_buildBreakdownSection(
title: context.lang.hpp_breakdown,
icon: LineIcons.shoppingBag,
color: AppColor.error,
items: data.hppBreakdown,
),
const SpaceHeight(16),
],
if (data.operationalExpenseBreakdown.isNotEmpty) ...[
_buildBreakdownSection(
title: context.lang.operational_expense_breakdown,
icon: LineIcons.receipt,
color: AppColor.warning,
items: data.operationalExpenseBreakdown,
),
const SpaceHeight(16),
],
if (data.dailySummary.isNotEmpty) ...[
_buildDailySummarySection(data.dailySummary),
const SpaceHeight(16),
],
if (data.dailyTransactions.isNotEmpty) ...[
_buildTransactionsSection(data.dailyTransactions),
const SpaceHeight(16),
],
],
),
);
}
// ─── NET PROFIT HERO CARD ───────────────────────────────────────────────────
Widget _buildNetProfitCard(ExclusiveSummarySummary summary) {
final isPositive = summary.netProfit >= 0;
final gradientColors = isPositive
? AppColor.successGradient
: [AppColor.error, AppColor.error.withOpacity(0.7)];
return TweenAnimationBuilder<double>(
tween: Tween(begin: 0.0, end: 1.0),
duration: const Duration(milliseconds: 900),
curve: Curves.elasticOut,
builder: (context, value, _) => Transform.scale(
scale: value.clamp(0.0, 1.0),
child: Container(
width: double.infinity,
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: gradientColors,
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: (isPositive ? AppColor.success : AppColor.error)
.withOpacity(0.3),
blurRadius: 20,
offset: const Offset(0, 8),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(12),
),
child: Icon(
isPositive ? LineIcons.lineChart : LineIcons.arrowDown,
color: Colors.white,
size: 22,
),
),
const SpaceWidth(12),
Text(
context.lang.net_profit,
style: const TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
],
),
const SpaceHeight(16),
Text(
summary.netProfit.currencyFormatRp,
style: const TextStyle(
color: Colors.white,
fontSize: 32,
fontWeight: FontWeight.bold,
letterSpacing: -0.5,
),
),
const SpaceHeight(8),
Row(
children: [
_buildHeroStat(
context.lang.total_sales,
summary.sales.currencyFormatRp,
),
const SpaceWidth(24),
_buildHeroStat(
context.lang.total_cost,
summary.totalCost.currencyFormatRp,
),
],
),
],
),
),
),
);
}
Widget _buildHeroStat(String label, String value) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: TextStyle(
color: Colors.white.withOpacity(0.8),
fontSize: 12,
),
),
Text(
value,
style: const TextStyle(
color: Colors.white,
fontSize: 15,
fontWeight: FontWeight.bold,
),
),
],
);
}
// ─── SUMMARY GRID ───────────────────────────────────────────────────────────
Widget _buildSummaryGrid(ExclusiveSummarySummary summary) {
return Column(
children: [
Row(
children: [
Expanded(
child: _buildStatCard(
icon: LineIcons.arrowUp,
label: context.lang.gross_profit,
value: summary.grossProfit.currencyFormatRp,
color: AppColor.success,
),
),
const SpaceWidth(12),
Expanded(
child: _buildStatCard(
icon: LineIcons.shoppingBag,
label: context.lang.hpp,
value: summary.hpp.currencyFormatRp,
color: AppColor.error,
),
),
],
),
const SpaceHeight(12),
Row(
children: [
Expanded(
child: _buildStatCard(
icon: LineIcons.users,
label: context.lang.salary_total,
value: summary.salaryTotal.currencyFormatRp,
color: AppColor.info,
),
),
const SpaceWidth(12),
Expanded(
child: _buildStatCard(
icon: LineIcons.receipt,
label: context.lang.operational_expenses,
value: summary.operationalExpensesTotal.currencyFormatRp,
color: AppColor.warning,
),
),
],
),
],
);
}
Widget _buildStatCard({
required IconData icon,
required String label,
required String value,
required Color color,
}) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColor.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: color.withOpacity(0.08),
blurRadius: 12,
offset: const Offset(0, 4),
),
],
border: Border.all(color: color.withOpacity(0.12)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(10),
),
child: Icon(icon, color: color, size: 18),
),
const SpaceHeight(10),
Text(
label,
style: AppStyle.sm.copyWith(color: AppColor.textSecondary),
),
const SpaceHeight(4),
Text(
value,
style: AppStyle.md.copyWith(
color: AppColor.textPrimary,
fontWeight: FontWeight.bold,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
);
}
// ─── REIMBURSE CARD ─────────────────────────────────────────────────────────
Widget _buildReimburseCard(ExclusiveSummaryReimburse reimburse) {
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: AppColor.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: AppColor.primary.withOpacity(0.08),
blurRadius: 12,
offset: const Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: AppColor.primary.withOpacity(0.1),
borderRadius: BorderRadius.circular(10),
),
child: const Icon(
LineIcons.moneyBill,
color: AppColor.primary,
size: 18,
),
),
const SpaceWidth(10),
Text(
context.lang.reimburse_summary,
style: AppStyle.lg.copyWith(fontWeight: FontWeight.bold),
),
],
),
const SpaceHeight(16),
_buildReimburseRow(
context.lang.total_cost,
reimburse.totalCost.currencyFormatRp,
),
const Divider(height: 20),
_buildReimburseRow(
context.lang.excluded_salary_staff,
reimburse.excludedSalaryStaff.currencyFormatRp,
),
const Divider(height: 20),
_buildReimburseRow(
context.lang.total_reimburse,
reimburse.totalReimburse.currencyFormatRp,
isHighlighted: true,
),
],
),
);
}
Widget _buildReimburseRow(
String label,
String value, {
bool isHighlighted = false,
}) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
label,
style: AppStyle.sm.copyWith(
color: isHighlighted
? AppColor.textPrimary
: AppColor.textSecondary,
fontWeight:
isHighlighted ? FontWeight.bold : FontWeight.normal,
),
),
Text(
value,
style: AppStyle.sm.copyWith(
color: isHighlighted ? AppColor.primary : AppColor.textPrimary,
fontWeight:
isHighlighted ? FontWeight.bold : FontWeight.w600,
),
),
],
);
}
// ─── BREAKDOWN SECTION ──────────────────────────────────────────────────────
Widget _buildBreakdownSection({
required String title,
required IconData icon,
required Color color,
required List<ExclusiveSummaryBreakdown> items,
}) {
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: AppColor.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.04),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(10),
),
child: Icon(icon, color: color, size: 18),
),
const SpaceWidth(10),
Expanded(
child: Text(
title,
style: AppStyle.lg.copyWith(fontWeight: FontWeight.bold),
),
),
],
),
const SpaceHeight(16),
...items.map((item) => _buildBreakdownItem(item, color)),
],
),
);
}
Widget _buildBreakdownItem(
ExclusiveSummaryBreakdown item,
Color color,
) {
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
item.categoryName,
style: AppStyle.sm.copyWith(
fontWeight: FontWeight.w600,
color: AppColor.textPrimary,
),
),
),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
item.amount.currencyFormatRp,
style: AppStyle.sm.copyWith(
fontWeight: FontWeight.bold,
color: color,
),
),
Text(
'${item.percentage.toStringAsFixed(1)}%',
style: AppStyle.xs.copyWith(
color: AppColor.textSecondary,
),
),
],
),
],
),
const SpaceHeight(6),
ClipRRect(
borderRadius: BorderRadius.circular(4),
child: TweenAnimationBuilder<double>(
tween: Tween(begin: 0.0, end: item.percentage / 100),
duration: const Duration(milliseconds: 800),
curve: Curves.easeOutCubic,
builder: (context, value, _) => LinearProgressIndicator(
value: value.clamp(0.0, 1.0),
backgroundColor: color.withOpacity(0.1),
valueColor: AlwaysStoppedAnimation<Color>(color),
minHeight: 6,
),
),
),
],
),
);
}
// ─── DAILY SUMMARY ──────────────────────────────────────────────────────────
Widget _buildDailySummarySection(List<ExclusiveSummaryDaily> items) {
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: AppColor.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.04),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
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(10),
),
child: const Icon(
LineIcons.calendar,
color: AppColor.info,
size: 18,
),
),
const SpaceWidth(10),
Text(
context.lang.daily_summary,
style: AppStyle.lg.copyWith(fontWeight: FontWeight.bold),
),
],
),
const SpaceHeight(16),
ListView.separated(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: items.length,
separatorBuilder: (_, __) => const Divider(height: 16),
itemBuilder: (context, index) {
final item = items[index];
return Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: AppColor.primary.withOpacity(0.08),
borderRadius: BorderRadius.circular(8),
),
child: Text(
'${item.date.day}',
style: AppStyle.md.copyWith(
color: AppColor.primary,
fontWeight: FontWeight.bold,
),
),
),
const SpaceWidth(12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
item.date.toDate,
style: AppStyle.sm.copyWith(
fontWeight: FontWeight.w600,
),
),
Text(
'${item.transactionCount} ${context.lang.transactions}',
style: AppStyle.xs.copyWith(
color: AppColor.textSecondary,
),
),
],
),
),
Text(
item.totalCost.currencyFormatRp,
style: AppStyle.sm.copyWith(
fontWeight: FontWeight.bold,
color: AppColor.error,
),
),
],
);
},
),
],
),
);
}
// ─── DAILY TRANSACTIONS ─────────────────────────────────────────────────────
Widget _buildTransactionsSection(
List<ExclusiveSummaryTransaction> items) {
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: AppColor.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.04),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
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(10),
),
child: const Icon(
LineIcons.list,
color: AppColor.secondary,
size: 18,
),
),
const SpaceWidth(10),
Text(
context.lang.daily_transactions,
style: AppStyle.lg.copyWith(fontWeight: FontWeight.bold),
),
],
),
const SpaceHeight(16),
ListView.separated(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: items.length,
separatorBuilder: (_, __) => const Divider(height: 16),
itemBuilder: (context, index) {
final tx = items[index];
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
decoration: BoxDecoration(
color: _sourceColor(tx.source).withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Text(
_sourceLabel(tx.source),
style: AppStyle.xs.copyWith(
color: _sourceColor(tx.source),
fontWeight: FontWeight.w600,
),
),
),
const SpaceWidth(10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
tx.description,
style: AppStyle.sm.copyWith(
fontWeight: FontWeight.w600,
),
),
Text(
'${tx.categoryName} · ${tx.date.toShortDate}',
style: AppStyle.xs.copyWith(
color: AppColor.textSecondary,
),
),
],
),
),
Text(
tx.amount.currencyFormatRp,
style: AppStyle.sm.copyWith(
fontWeight: FontWeight.bold,
color: AppColor.error,
),
),
],
);
},
),
],
),
);
}
Color _sourceColor(String source) {
switch (source) {
case 'purchase_order':
return AppColor.info;
case 'salary':
return AppColor.warning;
case 'operational':
return AppColor.secondary;
default:
return AppColor.primary;
}
}
String _sourceLabel(String source) {
switch (source) {
case 'purchase_order':
return 'PO';
case 'salary':
return 'Gaji';
case 'operational':
return 'Ops';
default:
return source;
}
}
}
@@ -191,7 +191,7 @@ class _DailyTasksFormPageState extends State<DailyTasksFormPage>
const SizedBox(height: 16), const SizedBox(height: 16),
...section.questions.map((question) { ...section.questions.map((question) {
return _buildQuestionCard(question); return _buildQuestionCard(question);
}).toList(), }),
], ],
), ),
); );
+128 -120
View File
@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:line_icons/line_icons.dart'; import 'package:line_icons/line_icons.dart';
import '../../../application/analytic/exclusive_summary_loader/exclusive_summary_loader_bloc.dart';
import '../../../application/home/home_bloc.dart'; import '../../../application/home/home_bloc.dart';
import '../../../application/outlet/outlet_list_loader/outlet_list_loader_bloc.dart'; import '../../../application/outlet/outlet_list_loader/outlet_list_loader_bloc.dart';
import '../../../application/outlet/selected_outlet/selected_outlet_bloc.dart'; import '../../../application/outlet/selected_outlet/selected_outlet_bloc.dart';
@@ -13,7 +14,8 @@ import '../../components/button/button.dart';
import '../../components/spacer/spacer.dart'; import '../../components/spacer/spacer.dart';
import 'widgets/feature.dart'; import 'widgets/feature.dart';
import 'widgets/header.dart'; import 'widgets/header.dart';
import 'widgets/promo_banner.dart'; import 'widgets/home_top_products.dart';
import 'widgets/home_warnings.dart';
import 'widgets/stats.dart'; import 'widgets/stats.dart';
@RoutePage() @RoutePage()
@@ -31,13 +33,16 @@ class HomePage extends StatefulWidget implements AutoRouteWrapper {
getIt<HomeBloc>()..add(HomeEvent.fetchedDashboard()), getIt<HomeBloc>()..add(HomeEvent.fetchedDashboard()),
), ),
BlocProvider( BlocProvider(
create: (context) => getIt<OutletListLoaderBloc>() create: (context) =>
..add(const OutletListLoaderEvent.fetched()), getIt<OutletListLoaderBloc>()
..add(const OutletListLoaderEvent.fetched()),
), ),
BlocProvider( BlocProvider(
create: (context) => getIt<SelectedOutletBloc>() create: (context) =>
..add(const SelectedOutletEvent.loaded()), getIt<SelectedOutletBloc>()
..add(const SelectedOutletEvent.loaded()),
), ),
BlocProvider(create: (context) => getIt<ExclusiveSummaryLoaderBloc>()),
], ],
child: this, child: this,
); );
@@ -89,129 +94,132 @@ class _HomePageState extends State<HomePage> with TickerProviderStateMixin {
previous.selectedOutletId != current.selectedOutletId, previous.selectedOutletId != current.selectedOutletId,
listener: (context, state) => _refetchDashboard(context), listener: (context, state) => _refetchDashboard(context),
child: Scaffold( child: Scaffold(
backgroundColor: AppColor.background, backgroundColor: AppColor.background,
body: RefreshIndicator( body: RefreshIndicator(
backgroundColor: AppColor.white, backgroundColor: AppColor.white,
color: AppColor.primary, color: AppColor.primary,
onRefresh: () { onRefresh: () {
_refetchDashboard(context); _refetchDashboard(context);
return Future.value(); return Future.value();
}, },
child: BlocBuilder<HomeBloc, HomeState>( child: BlocBuilder<HomeBloc, HomeState>(
builder: (context, state) { builder: (context, state) {
return CustomScrollView( return CustomScrollView(
physics: const BouncingScrollPhysics( physics: const BouncingScrollPhysics(
parent: ClampingScrollPhysics(), parent: ClampingScrollPhysics(),
), ),
slivers: [ slivers: [
// SliverAppBar with HomeHeader as background // SliverAppBar with HomeHeader as background
SliverAppBar( SliverAppBar(
expandedHeight: 300, // Adjust based on HomeHeader height expandedHeight: 440, // Adjusted for new header with slider
floating: false, floating: false,
pinned: true, pinned: true,
snap: false, snap: false,
elevation: 0, elevation: 0,
scrolledUnderElevation: 8, scrolledUnderElevation: 8,
backgroundColor: AppColor.primary, backgroundColor: AppColor.primary,
surfaceTintColor: Colors.transparent, surfaceTintColor: Colors.transparent,
flexibleSpace: LayoutBuilder( flexibleSpace: LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) { builder: (BuildContext context, BoxConstraints constraints) {
// Calculate collapse progress (0.0 = expanded, 1.0 = collapsed) // Calculate collapse progress (0.0 = expanded, 1.0 = collapsed)
final double expandedHeight = 200; final double expandedHeight = 200;
final double collapsedHeight = final double collapsedHeight =
kToolbarHeight + MediaQuery.of(context).padding.top; kToolbarHeight + MediaQuery.of(context).padding.top;
final double currentHeight = constraints.maxHeight; final double currentHeight = constraints.maxHeight;
double collapseProgress = double collapseProgress =
1.0 - 1.0 -
((currentHeight - collapsedHeight) / ((currentHeight - collapsedHeight) /
(expandedHeight - collapsedHeight)); (expandedHeight - collapsedHeight));
collapseProgress = collapseProgress.clamp(0.0, 1.0); collapseProgress = collapseProgress.clamp(0.0, 1.0);
return FlexibleSpaceBar( return FlexibleSpaceBar(
title: Opacity( title: Opacity(
opacity: opacity:
collapseProgress, // Title muncul saat collapse collapseProgress, // Title muncul saat collapse
child: Row( child: Row(
children: [ children: [
Expanded( Expanded(
child: Text( child: Text(
AppConstant.appName, AppConstant.appName,
style: AppStyle.xl.copyWith( style: AppStyle.xl.copyWith(
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
fontSize: 18, fontSize: 18,
letterSpacing: -0.5, letterSpacing: -0.5,
color: AppColor.white, color: AppColor.white,
),
), ),
), ),
), ActionIconButton(
ActionIconButton( onTap: () {},
onTap: () {}, icon: LineIcons.bell,
icon: LineIcons.bell,
),
],
),
),
titlePadding: const EdgeInsets.only(
left: 20,
right: 12,
bottom: 16,
),
background: AnimatedBuilder(
animation: _headerAnimationController,
builder: (context, child) {
return Transform.translate(
offset: Offset(
0,
50 * (1 - _headerAnimationController.value),
),
child: Opacity(
opacity: _headerAnimationController.value,
child: HomeHeader(
totalRevenue:
state.dashboard.overview.totalSales,
), ),
), ],
); ),
},
),
);
},
),
),
// Main Content
SliverToBoxAdapter(
child: AnimatedBuilder(
animation: _contentAnimationController,
builder: (context, child) {
return Transform.translate(
offset: Offset(
0,
30 * (1 - _contentAnimationController.value),
),
child: Opacity(
opacity: _contentAnimationController.value,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const HomePromoBanner(),
HomeFeature(),
HomeStats(overview: state.dashboard.overview),
const SpaceHeight(40),
],
), ),
), titlePadding: const EdgeInsets.only(
); left: 20,
}, right: 12,
bottom: 16,
),
background: AnimatedBuilder(
animation: _headerAnimationController,
builder: (context, child) {
return Transform.translate(
offset: Offset(
0,
50 * (1 - _headerAnimationController.value),
),
child: Opacity(
opacity: _headerAnimationController.value,
child: HomeHeader(
totalRevenue:
state.dashboard.overview.totalSales,
),
),
);
},
),
);
},
),
), ),
),
], // Main Content
); SliverToBoxAdapter(
}, child: AnimatedBuilder(
animation: _contentAnimationController,
builder: (context, child) {
return Transform.translate(
offset: Offset(
0,
30 * (1 - _contentAnimationController.value),
),
child: Opacity(
opacity: _contentAnimationController.value,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
HomeFeature(),
HomeWarnings(),
HomeStats(overview: state.dashboard.overview),
HomeTopProducts(
products: state.dashboard.topProducts,
),
const SpaceHeight(40),
],
),
),
);
},
),
),
],
);
},
),
), ),
), ), // Scaffold
), // Scaffold
); // BlocListener ); // BlocListener
} }
} }
@@ -13,14 +13,10 @@ class HomeFeature extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return Container(
margin: const EdgeInsets.symmetric( margin: const EdgeInsets.symmetric().copyWith(bottom: 0),
vertical: 24,
horizontal: AppValue.padding,
).copyWith(bottom: 0),
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 10), padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 10),
decoration: BoxDecoration( decoration: BoxDecoration(
color: AppColor.white, color: AppColor.white,
borderRadius: BorderRadius.circular(AppValue.radius),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: Colors.black.withOpacity(0.04), color: Colors.black.withOpacity(0.04),
@@ -34,7 +30,7 @@ class HomeFeature extends StatelessWidget {
children: [ children: [
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceAround, mainAxisAlignment: MainAxisAlignment.spaceAround,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.end,
children: [ children: [
HomeFeatureTile( HomeFeatureTile(
title: context.lang.sales, title: context.lang.sales,
@@ -43,18 +39,24 @@ class HomeFeature extends StatelessWidget {
), ),
HomeFeatureTile( HomeFeatureTile(
title: context.lang.purchase, title: context.lang.purchase,
iconPath: Assets.icons.icReportPurchase.path, iconPath: Assets.icons.icReportPurchase.path,
onTap: () => context.router.push(PurchaseRoute()), onTap: () => context.router.push(PurchaseRoute()),
), ),
HomeFeatureTile( HomeFeatureTile(
title: context.lang.profit_loss, title: context.lang.profit_loss,
iconPath: Assets.icons.icReportProfitLoss.path, iconPath: Assets.icons.icReportProfitLoss.path,
onTap: () => context.router.push(FinanceRoute()), onTap: () => context.router.push(FinanceRoute()),
isHighlighted: true,
),
HomeFeatureTile(
title: context.lang.stock,
iconPath: Assets.icons.icReportStock.path,
onTap: () => context.router.push(InventoryRoute()),
), ),
HomeFeatureTile( HomeFeatureTile(
title: context.lang.product, title: context.lang.product,
iconPath: Assets.icons.icReportProduct.path, iconPath: Assets.icons.icReportProduct.path,
onTap: () => context.router.push(ProductAnalyticRoute()), onTap: () => context.router.push(ProductRoute()),
), ),
], ],
), ),
@@ -7,15 +7,21 @@ class HomeFeatureTile extends StatelessWidget {
final String title; final String title;
final String iconPath; final String iconPath;
final Function() onTap; final Function() onTap;
final bool isHighlighted;
const HomeFeatureTile({ const HomeFeatureTile({
super.key, super.key,
required this.title, required this.title,
required this.iconPath, required this.iconPath,
required this.onTap, required this.onTap,
this.isHighlighted = false,
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final double iconSize = isHighlighted ? 72 : 56;
final double borderRadius = isHighlighted ? 20 : 16;
return Expanded( return Expanded(
child: InkWell( child: InkWell(
onTap: onTap, onTap: onTap,
@@ -26,24 +32,29 @@ class HomeFeatureTile extends StatelessWidget {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Container( Container(
width: 56, width: iconSize,
height: 56, height: iconSize,
decoration: BoxDecoration( decoration: BoxDecoration(
gradient: LinearGradient( gradient: LinearGradient(
colors: [AppColor.primary.withOpacity(0.1), AppColor.primary.withOpacity(0.05)], colors: [
AppColor.primary.withOpacity(0.1),
AppColor.primary.withOpacity(0.05),
],
begin: Alignment.topLeft, begin: Alignment.topLeft,
end: Alignment.bottomRight, end: Alignment.bottomRight,
), ),
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(borderRadius),
), ),
child: Image.asset(iconPath), child: Image.asset(iconPath),
), ),
const SpaceHeight(12), const SpaceHeight(12),
Text( Text(
title, title,
style: AppStyle.sm.copyWith( style: AppStyle.xs.copyWith(
fontWeight: FontWeight.w600, fontWeight: isHighlighted ? FontWeight.w700 : FontWeight.w600,
color: AppColor.textPrimary, color: isHighlighted
? const Color(0xFF388E3C)
: AppColor.textPrimary,
letterSpacing: -0.2, letterSpacing: -0.2,
), ),
textAlign: TextAlign.center, textAlign: TextAlign.center,
+223 -275
View File
@@ -1,14 +1,17 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../application/analytic/exclusive_summary_loader/exclusive_summary_loader_bloc.dart';
import '../../../../application/auth/auth_bloc.dart'; import '../../../../application/auth/auth_bloc.dart';
import '../../../../common/constant/app_constant.dart'; import '../../../../application/outlet/selected_outlet/selected_outlet_bloc.dart';
import '../../../../common/extension/extension.dart'; import '../../../../common/extension/extension.dart';
import '../../../../common/painter/wave_painter.dart'; import '../../../../common/painter/wave_painter.dart';
import '../../../../common/theme/theme.dart'; import '../../../../common/theme/theme.dart';
import '../../../../domain/user/user.dart';
import '../../../components/spacer/spacer.dart'; import '../../../components/spacer/spacer.dart';
import 'omset_balance.dart'; import 'header_date_filter.dart';
import 'header_outlet_selector.dart';
import 'header_summary_slider.dart';
import 'header_top_bar.dart';
class HomeHeader extends StatefulWidget { class HomeHeader extends StatefulWidget {
final int totalRevenue; final int totalRevenue;
@@ -18,12 +21,15 @@ class HomeHeader extends StatefulWidget {
State<HomeHeader> createState() => _HomeHeaderState(); State<HomeHeader> createState() => _HomeHeaderState();
} }
class _HomeHeaderState extends State<HomeHeader> with SingleTickerProviderStateMixin { class _HomeHeaderState extends State<HomeHeader>
with SingleTickerProviderStateMixin {
late AnimationController _animationController; late AnimationController _animationController;
late Animation<double> _fadeInAnimation; late Animation<double> _fadeInAnimation;
late Animation<Offset> _slideAnimation; late Animation<Offset> _slideAnimation;
late Animation<double> _scaleAnimation;
/// 0 = Hari Ini, 1 = MTD (Bulan)
int _selectedDateFilter = 0;
bool _isValueVisible = true;
@override @override
void initState() { void initState() {
@@ -42,21 +48,37 @@ class _HomeHeaderState extends State<HomeHeader> with SingleTickerProviderStateM
); );
_slideAnimation = _slideAnimation =
Tween<Offset>(begin: const Offset(0, 0.5), end: Offset.zero).animate( Tween<Offset>(begin: const Offset(0, 0.3), end: Offset.zero).animate(
CurvedAnimation( CurvedAnimation(
parent: _animationController, parent: _animationController,
curve: const Interval(0.2, 0.8, curve: Curves.easeOutCubic), curve: const Interval(0.2, 0.8, curve: Curves.easeOutCubic),
), ),
); );
_scaleAnimation = Tween<double>(begin: 0.8, end: 1.0).animate(
CurvedAnimation(
parent: _animationController,
curve: const Interval(0.0, 0.7, curve: Curves.elasticOut),
),
);
_animationController.forward(); _animationController.forward();
WidgetsBinding.instance.addPostFrameCallback((_) {
_fetchSummary();
});
}
void _fetchSummary() {
final now = DateTime.now();
DateTime dateFrom;
DateTime dateTo;
if (_selectedDateFilter == 0) {
dateFrom = DateTime(now.year, now.month, now.day);
dateTo = DateTime(now.year, now.month, now.day);
} else {
// MTD: tanggal 1 s/d hari ini
dateFrom = DateTime(now.year, now.month, 1);
dateTo = DateTime(now.year, now.month, now.day);
}
context.read<ExclusiveSummaryLoaderBloc>()
..add(ExclusiveSummaryLoaderEvent.rangeDateChanged(dateFrom, dateTo))
..add(const ExclusiveSummaryLoaderEvent.fetched());
} }
@override @override
@@ -67,279 +89,205 @@ class _HomeHeaderState extends State<HomeHeader> with SingleTickerProviderStateM
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocBuilder<AuthBloc, AuthState>( return BlocListener<SelectedOutletBloc, SelectedOutletState>(
builder: (context, state) { listenWhen: (prev, curr) =>
return Container( prev.selectedOutletId != curr.selectedOutletId,
height: 280, listener: (context, state) => _fetchSummary(),
decoration: BoxDecoration( child: BlocBuilder<AuthBloc, AuthState>(
gradient: LinearGradient( builder: (context, authState) {
colors: [ return Container(
AppColor.primary, decoration: BoxDecoration(
AppColor.primaryLight, gradient: LinearGradient(
AppColor.primaryLight.withOpacity(0.8), colors: [
], AppColor.primary,
begin: Alignment.topLeft, AppColor.primary.withOpacity(0.9),
end: Alignment.bottomRight, AppColor.primaryLight.withOpacity(0.85),
stops: const [0.0, 0.7, 1.0], ],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
stops: const [0.0, 0.7, 1.0],
),
), ),
boxShadow: [ child: Stack(
BoxShadow( children: [
color: AppColor.primary.withOpacity(0.3), // Decorative circles
blurRadius: 20, _buildDecorations(),
offset: const Offset(0, 10),
),
],
),
child: Stack(
children: [
// Static decorative circles (right side)
Positioned(
top: -50,
right: -50,
child: Container(
width: 150,
height: 150,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: AppColor.white.withOpacity(0.10),
),
),
),
Positioned(
top: 80,
right: -20,
child: Container(
width: 80,
height: 80,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: AppColor.white.withOpacity(0.05),
),
),
),
Positioned(
top: 150,
right: 30,
child: Container(
width: 40,
height: 40,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: AppColor.white.withOpacity(0.07),
),
),
),
// Static decorative circles (left side) // Wave pattern
Positioned( Positioned.fill(
top: 60, child: CustomPaint(
left: -30, painter: WavePainter(
child: Container( animation: 0.0,
width: 100, color: AppColor.white.withOpacity(0.08),
height: 100,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: AppColor.white.withOpacity(0.03),
),
),
),
Positioned(
bottom: 20,
left: -20,
child: Container(
width: 60,
height: 60,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: AppColor.white.withOpacity(0.04),
),
),
),
// Static sparkle icons
...List.generate(8, (index) {
return Positioned(
left: (index * 60.0) % (MediaQuery.of(context).size.width),
top: 30 + (index * 25.0),
child: Icon(
Icons.auto_awesome,
size: 8 + (index % 3) * 3,
color: AppColor.white.withOpacity(0.25),
),
);
}),
// Wave pattern (static)
Positioned.fill(
child: CustomPaint(
painter: WavePainter(
animation: 0.0,
color: AppColor.white.withOpacity(0.08),
),
),
),
// Gradient overlay for depth
Container(
decoration: BoxDecoration(
gradient: RadialGradient(
center: const Alignment(0.8, -0.3),
radius: 1.5,
colors: [
Colors.transparent,
AppColor.primary.withOpacity(0.1),
Colors.transparent,
],
),
),
),
// Main content
SafeArea(child: _buildContent(context, state.user)),
],
),
);
},
);
}
Widget _buildContent(BuildContext context, User user) {
String greeting(BuildContext context) {
final hour = DateTime.now().hour;
if (hour >= 4 && hour < 10) {
return context.lang.good_morning;
} else if (hour >= 10 && hour < 15) {
return context.lang.good_afternoon;
} else if (hour >= 15 && hour < 18) {
return context.lang.good_evening;
} else {
return context.lang.good_night;
}
}
return Padding(
padding: EdgeInsets.all(AppValue.padding),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
// Top bar with enhanced animation
SlideTransition(
position: _slideAnimation,
child: FadeTransition(
opacity: _fadeInAnimation,
child: Transform.scale(
scale: _scaleAnimation.value,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
AppConstant.appName,
style: AppStyle.lg.copyWith(
color: AppColor.white.withOpacity(0.9),
fontWeight: FontWeight.w600,
letterSpacing: 0.3,
shadows: [
Shadow(
color: Colors.black.withOpacity(0.2),
offset: const Offset(0, 1),
blurRadius: 2,
),
],
),
),
const SpaceHeight(2),
Text(
user.role.toTitleCase,
style: AppStyle.sm.copyWith(
color: AppColor.white.withOpacity(0.7),
fontSize: 11,
fontWeight: FontWeight.w400,
),
),
],
),
), ),
// Notification icon ),
Container( ),
padding: const EdgeInsets.all(10),
decoration: BoxDecoration( // Main content
color: AppColor.white.withOpacity(0.25), SafeArea(
borderRadius: BorderRadius.circular(14), child: Padding(
border: Border.all( padding: const EdgeInsets.symmetric(
color: AppColor.white.withOpacity(0.3), horizontal: 16,
width: 1, vertical: 12,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
// Top bar
SlideTransition(
position: _slideAnimation,
child: FadeTransition(
opacity: _fadeInAnimation,
child: HeaderTopBar(user: authState.user),
),
), ),
boxShadow: [
BoxShadow( const SpaceHeight(16),
color: AppColor.white.withOpacity(0.2),
blurRadius: 8, // Outlet selector
offset: const Offset(0, 2), SlideTransition(
position: _slideAnimation,
child: FadeTransition(
opacity: _fadeInAnimation,
child: const HeaderOutletSelector(),
), ),
], ),
),
child: const Icon(
Icons.notifications_none_rounded,
color: AppColor.white,
size: 20,
),
),
],
),
),
),
),
const SpaceHeight(24), const SpaceHeight(12),
// Greeting Section with enhanced animations // Date filter tabs
SlideTransition( FadeTransition(
position: _slideAnimation, opacity: _fadeInAnimation,
child: FadeTransition( child: HeaderDateFilter(
opacity: _fadeInAnimation, selectedIndex: _selectedDateFilter,
child: Transform.scale( onChanged: (index) {
scale: _scaleAnimation.value, setState(() => _selectedDateFilter = index);
child: Column( _fetchSummary();
crossAxisAlignment: CrossAxisAlignment.start, },
children: [
Text(
'${greeting(context)}, ${user.name}! đź‘‹',
style: AppStyle.md.copyWith(
color: AppColor.white,
fontWeight: FontWeight.w500,
shadows: [
Shadow(
color: Colors.black.withOpacity(0.2),
offset: const Offset(0, 1),
blurRadius: 2,
), ),
], ),
),
const SpaceHeight(12),
// Ringkasan label
FadeTransition(
opacity: _fadeInAnimation,
child: _buildRingkasanLabel(),
),
const SpaceHeight(12),
// Sliding summary cards
FadeTransition(
opacity: _fadeInAnimation,
child: HeaderSummarySlider(
isValueVisible: _isValueVisible,
),
),
],
), ),
], ),
), ),
), ],
), ),
), );
},
const SpaceHeight(16),
// Today's highlight
FadeTransition(
opacity: _fadeInAnimation,
child: SlideTransition(
position: _slideAnimation,
child: HomeOmsetBalance(totalOmset: widget.totalRevenue, user: user),
),
),
],
), ),
); );
} }
Widget _buildDecorations() {
return Stack(
children: [
Positioned(
top: -50,
right: -50,
child: Container(
width: 150,
height: 150,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: AppColor.white.withOpacity(0.10),
),
),
),
Positioned(
top: 80,
right: -20,
child: Container(
width: 80,
height: 80,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: AppColor.white.withOpacity(0.05),
),
),
),
Positioned(
top: 60,
left: -30,
child: Container(
width: 100,
height: 100,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: AppColor.white.withOpacity(0.03),
),
),
),
],
);
}
Widget _buildRingkasanLabel() {
final now = DateTime.now();
final dateLabel = _selectedDateFilter == 0
? '${context.lang.summary_today} · ${now.day} ${_monthName(now.month)} ${now.year}'
: '${context.lang.summary_mtd} · 1 - ${now.day} ${_monthName(now.month)} ${now.year}';
return Row(
children: [
Expanded(
child: Text(
dateLabel,
style: AppStyle.sm.copyWith(
color: AppColor.white.withOpacity(0.9),
fontWeight: FontWeight.w600,
),
),
),
GestureDetector(
onTap: () {
setState(() => _isValueVisible = !_isValueVisible);
},
child: Icon(
_isValueVisible
? Icons.visibility_outlined
: Icons.visibility_off_outlined,
color: AppColor.white.withOpacity(0.7),
size: 20,
),
),
],
);
}
String _monthName(int month) {
const months = [
'Jan',
'Feb',
'Mar',
'Apr',
'Mei',
'Jun',
'Jul',
'Agu',
'Sep',
'Okt',
'Nov',
'Des',
];
return months[month - 1];
}
} }
@@ -0,0 +1,60 @@
import 'package:flutter/material.dart';
import '../../../../common/theme/theme.dart';
class HeaderDateFilter extends StatelessWidget {
final int selectedIndex;
final ValueChanged<int> onChanged;
const HeaderDateFilter({
super.key,
required this.selectedIndex,
required this.onChanged,
});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: AppColor.white.withOpacity(0.15),
borderRadius: BorderRadius.circular(12),
),
child: Row(
children: [
_buildTabItem('Hari Ini', 0),
_buildTabItem('MTD (Bulan)', 1),
],
),
);
}
Widget _buildTabItem(String label, int index) {
final isSelected = selectedIndex == index;
return Expanded(
child: GestureDetector(
onTap: () {
if (selectedIndex != index) {
onChanged(index);
}
},
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
padding: const EdgeInsets.symmetric(vertical: 10),
decoration: BoxDecoration(
color: isSelected ? AppColor.white : Colors.transparent,
borderRadius: BorderRadius.circular(10),
),
alignment: Alignment.center,
child: Text(
label,
style: AppStyle.sm.copyWith(
color: isSelected ? AppColor.primary : AppColor.white,
fontWeight: FontWeight.w700,
),
),
),
),
);
}
}
@@ -0,0 +1,251 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../application/outlet/outlet_list_loader/outlet_list_loader_bloc.dart';
import '../../../../application/outlet/selected_outlet/selected_outlet_bloc.dart';
import '../../../../common/theme/theme.dart';
import '../../../components/spacer/spacer.dart';
class HeaderOutletBottomSheet extends StatelessWidget {
const HeaderOutletBottomSheet({super.key});
static void show(BuildContext context) {
showModalBottomSheet(
context: context,
backgroundColor: Colors.transparent,
isScrollControlled: true,
builder: (_) => MultiBlocProvider(
providers: [
BlocProvider.value(value: context.read<OutletListLoaderBloc>()),
BlocProvider.value(value: context.read<SelectedOutletBloc>()),
],
child: const HeaderOutletBottomSheet(),
),
);
}
@override
Widget build(BuildContext context) {
return Container(
constraints: BoxConstraints(
maxHeight: MediaQuery.of(context).size.height * 0.6,
),
decoration: const BoxDecoration(
color: AppColor.white,
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Handle bar
const SizedBox(height: 12),
Container(
width: 40,
height: 4,
decoration: BoxDecoration(
color: AppColor.border,
borderRadius: BorderRadius.circular(2),
),
),
const SizedBox(height: 16),
// Title
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Row(
children: [
Text(
'Pilih Outlet',
style: AppStyle.lg.copyWith(
fontWeight: FontWeight.w700,
color: AppColor.textPrimary,
),
),
const Spacer(),
GestureDetector(
onTap: () => Navigator.pop(context),
child: const Icon(
Icons.close_rounded,
color: AppColor.textSecondary,
size: 24,
),
),
],
),
),
const SizedBox(height: 16),
// List
Flexible(
child: BlocBuilder<OutletListLoaderBloc, OutletListLoaderState>(
builder: (context, outletListState) {
if (outletListState.isFetching &&
outletListState.outlets.isEmpty) {
return const Center(
child: Padding(
padding: EdgeInsets.all(24),
child: CircularProgressIndicator(),
),
);
}
return BlocBuilder<SelectedOutletBloc, SelectedOutletState>(
builder: (context, selectedState) {
return ListView(
shrinkWrap: true,
padding: const EdgeInsets.symmetric(horizontal: 20),
children: [
// Semua Outlet
_OutletTile(
title: 'Semua Outlet',
subtitle: '${outletListState.outlets.length} outlet',
icon: Icons.store_rounded,
isSelected: selectedState.isAllOutlets,
onTap: () {
context.read<SelectedOutletBloc>().add(
const SelectedOutletEvent.cleared(),
);
Navigator.pop(context);
},
),
const SizedBox(height: 8),
// Individual outlets
...outletListState.outlets.map((outlet) {
final isSelected =
selectedState.selectedOutletId == outlet.id;
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: _OutletTile(
title: outlet.name,
subtitle: outlet.isActive
? 'Aktif'
: 'Tidak aktif',
icon: Icons.storefront_rounded,
isSelected: isSelected,
isActive: outlet.isActive,
onTap: () {
context.read<SelectedOutletBloc>().add(
SelectedOutletEvent.selected(outlet),
);
Navigator.pop(context);
},
),
);
}),
const SizedBox(height: 16),
],
);
},
);
},
),
),
],
),
);
}
}
class _OutletTile extends StatelessWidget {
final String title;
final String subtitle;
final IconData icon;
final bool isSelected;
final bool isActive;
final VoidCallback onTap;
const _OutletTile({
required this.title,
required this.subtitle,
required this.icon,
required this.isSelected,
this.isActive = true,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
decoration: BoxDecoration(
color: isSelected
? AppColor.primary.withOpacity(0.08)
: AppColor.background,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: isSelected ? AppColor.primary : AppColor.border,
width: isSelected ? 1.5 : 1,
),
),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: isSelected
? AppColor.primary.withOpacity(0.15)
: AppColor.border.withOpacity(0.5),
borderRadius: BorderRadius.circular(10),
),
child: Icon(
icon,
color: isSelected ? AppColor.primary : AppColor.textSecondary,
size: 20,
),
),
const SpaceWidth(12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: AppStyle.md.copyWith(
fontWeight: FontWeight.w600,
color: AppColor.textPrimary,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 2),
Text(
subtitle,
style: AppStyle.xs.copyWith(color: AppColor.textSecondary),
),
],
),
),
if (isSelected)
Icon(
Icons.check_circle_rounded,
color: AppColor.primary,
size: 22,
),
if (!isSelected && !isActive)
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: AppColor.error.withOpacity(0.1),
borderRadius: BorderRadius.circular(6),
),
child: Text(
'Off',
style: AppStyle.xs.copyWith(
color: AppColor.error,
fontWeight: FontWeight.w600,
fontSize: 10,
),
),
),
],
),
),
);
}
}
@@ -0,0 +1,56 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../application/outlet/selected_outlet/selected_outlet_bloc.dart';
import '../../../../common/theme/theme.dart';
import '../../../components/spacer/spacer.dart';
import 'header_outlet_bottom_sheet.dart';
class HeaderOutletSelector extends StatelessWidget {
const HeaderOutletSelector({super.key});
@override
Widget build(BuildContext context) {
return BlocBuilder<SelectedOutletBloc, SelectedOutletState>(
builder: (context, state) {
return GestureDetector(
onTap: () => HeaderOutletBottomSheet.show(context),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
color: AppColor.white.withOpacity(0.15),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppColor.white.withOpacity(0.3)),
),
child: Row(
children: [
Icon(
Icons.location_on_outlined,
color: AppColor.white,
size: 18,
),
const SpaceWidth(8),
Expanded(
child: Text(
state.displayName,
style: AppStyle.md.copyWith(
color: AppColor.white,
fontWeight: FontWeight.w600,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
Icon(
Icons.keyboard_arrow_down_rounded,
color: AppColor.white,
size: 20,
),
],
),
),
);
},
);
}
}
@@ -0,0 +1,184 @@
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';
class HeaderSummaryCard extends StatelessWidget {
final IconData icon;
final Color iconColor;
final String title;
final int value;
final String subtitle;
final List<ExclusiveSummaryDaily> dailyData;
final double? percentage;
final bool isValueVisible;
final VoidCallback? onTap;
const HeaderSummaryCard({
super.key,
required this.icon,
required this.iconColor,
required this.title,
required this.value,
required this.subtitle,
required this.dailyData,
this.percentage,
this.isValueVisible = true,
this.onTap,
});
@override
Widget build(BuildContext context) {
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,
),
],
),
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,
),
),
const Spacer(),
// Mini bar chart
_buildMiniBarChart(),
],
),
),
);
}
Widget _buildPercentageBadge() {
final isPositive = (percentage ?? 0) >= 0;
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: isPositive
? const Color(0xFF4CAF50).withOpacity(0.25)
: const Color(0xFFE53E3E).withOpacity(0.25),
borderRadius: BorderRadius.circular(8),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
isPositive
? Icons.trending_up_rounded
: Icons.trending_down_rounded,
color: isPositive
? const Color(0xFF4CAF50)
: const Color(0xFFE53E3E),
size: 12,
),
const SizedBox(width: 3),
Text(
'${percentage!.toStringAsFixed(1)}%',
style: AppStyle.xs.copyWith(
color: isPositive
? const Color(0xFF4CAF50)
: const Color(0xFFE53E3E),
fontWeight: FontWeight.w700,
),
),
],
),
);
}
Widget _buildMiniBarChart() {
if (dailyData.isEmpty) return const SizedBox.shrink();
final maxVal = dailyData
.map((d) => d.totalCost)
.fold<int>(0, (a, b) => a > b ? a : b);
return SizedBox(
height: 24,
child: Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: dailyData.map((d) {
final ratio = maxVal > 0 ? d.totalCost / maxVal : 0.0;
return Expanded(
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 1.5),
height: 6 + (18 * ratio),
decoration: BoxDecoration(
color: AppColor.white.withOpacity(0.4),
borderRadius: BorderRadius.circular(3),
),
),
);
}).toList(),
),
);
}
}
@@ -0,0 +1,146 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../application/analytic/exclusive_summary_loader/exclusive_summary_loader_bloc.dart';
import '../../../../common/extension/extension.dart';
import '../../../../common/theme/theme.dart';
import '../../../router/app_router.gr.dart';
import 'header_summary_card.dart';
class HeaderSummarySlider extends StatefulWidget {
final bool isValueVisible;
const HeaderSummarySlider({super.key, this.isValueVisible = true});
@override
State<HeaderSummarySlider> createState() => _HeaderSummarySliderState();
}
class _HeaderSummarySliderState extends State<HeaderSummarySlider> {
final PageController _pageController = PageController(viewportFraction: 0.92);
int _currentPage = 0;
@override
void dispose() {
_pageController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return BlocBuilder<ExclusiveSummaryLoaderBloc, ExclusiveSummaryLoaderState>(
builder: (context, state) {
final summary = state.exclusiveSummary.summary;
final dailySummary = state.exclusiveSummary.dailySummary;
return Column(
children: [
SizedBox(
height: 170,
child: state.isFetching
? _buildShimmer()
: PageView(
controller: _pageController,
onPageChanged: (index) {
setState(() => _currentPage = index);
},
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: HeaderSummaryCard(
icon: Icons.credit_card_rounded,
iconColor: const Color(0xFFB71C1C),
title: context.lang.sales,
value: summary.sales,
subtitle: context.lang.compared_to_previous_period,
dailyData: dailySummary,
isValueVisible: widget.isValueVisible,
percentage: summary.sales > 0 ? 12.5 : null,
onTap: () =>
context.router.push(const SalesRoute()),
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: HeaderSummaryCard(
icon: Icons.shopping_cart_outlined,
iconColor: const Color(0xFF00BCD4),
title: context.lang.purchase,
value: summary.hpp,
subtitle: context.lang.compared_to_previous_period,
dailyData: dailySummary,
isValueVisible: widget.isValueVisible,
percentage: summary.sales > 0
? (summary.hpp / summary.sales * 100)
: null,
onTap: () =>
context.router.push(const PurchaseRoute()),
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: HeaderSummaryCard(
icon: Icons.account_balance_wallet_outlined,
iconColor: const Color(0xFFFF9800),
title: context.lang.profit_loss,
value: summary.netProfit,
subtitle: context.lang.compared_to_previous_period,
dailyData: dailySummary,
isValueVisible: widget.isValueVisible,
percentage: summary.sales > 0
? (summary.netProfit / summary.sales * 100)
: null,
onTap: () =>
context.router.push(const FinanceRoute()),
),
),
],
),
),
const SizedBox(height: 8),
// Page indicator
_buildPageIndicator(),
],
);
},
);
}
Widget _buildShimmer() {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 4),
decoration: BoxDecoration(
color: AppColor.white.withOpacity(0.15),
borderRadius: BorderRadius.circular(16),
),
child: Center(
child: CircularProgressIndicator(
color: AppColor.white.withOpacity(0.5),
strokeWidth: 2,
),
),
);
}
Widget _buildPageIndicator() {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(3, (index) {
final isActive = _currentPage == index;
return AnimatedContainer(
duration: const Duration(milliseconds: 200),
margin: const EdgeInsets.symmetric(horizontal: 3),
width: isActive ? 20 : 6,
height: 6,
decoration: BoxDecoration(
color: isActive ? AppColor.white : AppColor.white.withOpacity(0.4),
borderRadius: BorderRadius.circular(3),
),
);
}),
);
}
}
@@ -0,0 +1,94 @@
import 'package:flutter/material.dart';
import '../../../../common/extension/extension.dart';
import '../../../../common/theme/theme.dart';
import '../../../../domain/user/user.dart';
import '../../../components/assets/assets.gen.dart';
import '../../../components/spacer/spacer.dart';
class HeaderTopBar extends StatelessWidget {
final User user;
const HeaderTopBar({super.key, required this.user});
@override
Widget build(BuildContext context) {
return Row(
children: [
// Logo + Greeting
Expanded(
child: Row(
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Assets.images.logo.image(width: 64),
Text(
'${_greeting(context)}, ${user.name}',
style: AppStyle.md.copyWith(
color: AppColor.white.withOpacity(0.9),
fontWeight: FontWeight.w700,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
],
),
),
// Notification
GestureDetector(
onTap: () {},
child: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: AppColor.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(12),
),
child: const Icon(
Icons.notifications_none_rounded,
color: AppColor.white,
size: 20,
),
),
),
const SpaceWidth(8),
// Avatar
Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: AppColor.white,
borderRadius: BorderRadius.circular(12),
),
alignment: Alignment.center,
child: Text(
_getInitials(user.name),
style: AppStyle.sm.copyWith(
color: AppColor.primary,
fontWeight: FontWeight.w700,
),
),
),
],
);
}
String _greeting(BuildContext context) {
final hour = DateTime.now().hour;
if (hour >= 4 && hour < 10) return context.lang.good_morning;
if (hour >= 10 && hour < 15) return context.lang.good_afternoon;
if (hour >= 15 && hour < 18) return context.lang.good_evening;
return context.lang.good_night;
}
String _getInitials(String name) {
final parts = name.trim().split(' ');
if (parts.length >= 2) {
return '${parts[0][0]}${parts[1][0]}'.toUpperCase();
}
return parts[0].isNotEmpty ? parts[0][0].toUpperCase() : '';
}
}
@@ -0,0 +1,155 @@
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';
class HomeTopProducts extends StatelessWidget {
final List<DashboardTopProduct> products;
const HomeTopProducts({super.key, required this.products});
// Colors for product icon backgrounds
static const _iconBgColors = [
Color(0xFFB9F6CA), // green light
Color(0xFFB3E5FC), // blue light
Color(0xFFFFF9C4), // yellow light
Color(0xFFFFCDD2), // red light
Color(0xFFE1BEE7), // purple light
];
@override
Widget build(BuildContext context) {
if (products.isEmpty) return const SizedBox.shrink();
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: AppValue.padding,
vertical: 24,
).copyWith(bottom: 0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
context.lang.best_selling_products,
style: AppStyle.xl.copyWith(
fontWeight: FontWeight.w800,
color: AppColor.textPrimary,
),
),
const SpaceHeight(16),
// Product list card
Container(
decoration: BoxDecoration(
color: AppColor.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.04),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: ListView.separated(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
padding: const EdgeInsets.symmetric(vertical: 8),
itemCount: products.length > 5 ? 5 : products.length,
separatorBuilder: (_, __) => Divider(
height: 1,
color: AppColor.border.withOpacity(0.4),
indent: 76,
endIndent: 16,
),
itemBuilder: (context, index) {
return _ProductTile(
product: products[index],
bgColor: _iconBgColors[index % _iconBgColors.length],
);
},
),
),
],
),
);
}
}
class _ProductTile extends StatelessWidget {
final DashboardTopProduct product;
final Color bgColor;
const _ProductTile({required this.product, required this.bgColor});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
child: Row(
children: [
// Product icon placeholder
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: bgColor,
borderRadius: BorderRadius.circular(14),
),
child: Center(
child: Text(
product.productName.isNotEmpty
? product.productName[0].toUpperCase()
: '?',
style: AppStyle.lg.copyWith(
fontWeight: FontWeight.w700,
color: AppColor.textPrimary,
),
),
),
),
const SpaceWidth(12),
// Name + qty
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
product.productName,
style: AppStyle.md.copyWith(
fontWeight: FontWeight.w700,
color: AppColor.textPrimary,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 2),
Text(
context.lang.portion_sold(product.quantitySold),
style: AppStyle.xs.copyWith(color: AppColor.textSecondary),
),
],
),
),
const SpaceWidth(8),
// Revenue
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
product.revenue.currencyFormatRp,
style: AppStyle.md.copyWith(
fontWeight: FontWeight.w700,
color: AppColor.textPrimary,
),
),
],
),
],
),
);
}
}
@@ -0,0 +1,231 @@
import 'package:flutter/material.dart';
import '../../../../common/extension/extension.dart';
import '../../../../common/theme/theme.dart';
import '../../../components/spacer/spacer.dart';
class HomeWarnings extends StatelessWidget {
const HomeWarnings({super.key});
@override
Widget build(BuildContext context) {
// TODO: Integrate with actual warning data from backend
final warnings = <_WarningItem>[];
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: AppValue.padding,
vertical: 24,
).copyWith(bottom: 0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header
Row(
children: [
Text(
context.lang.warning_title,
style: AppStyle.xl.copyWith(
fontWeight: FontWeight.w800,
color: AppColor.textPrimary,
),
),
const SpaceWidth(8),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: AppColor.error,
borderRadius: BorderRadius.circular(10),
),
child: Text(
'${warnings.length}',
style: AppStyle.xs.copyWith(
color: AppColor.white,
fontWeight: FontWeight.w700,
),
),
),
],
),
const SpaceHeight(4),
Text(
context.lang.warning_desc,
style: AppStyle.sm.copyWith(color: AppColor.textSecondary),
),
const SpaceHeight(16),
// Warning list / empty state
Container(
width: double.infinity,
decoration: BoxDecoration(
color: AppColor.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.04),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: warnings.isEmpty
? _buildEmptyState(context)
: ListView.separated(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
padding: const EdgeInsets.symmetric(vertical: 8),
itemCount: warnings.length,
separatorBuilder: (_, __) => Divider(
height: 1,
color: AppColor.border.withOpacity(0.5),
indent: 72,
),
itemBuilder: (context, index) {
final item = warnings[index];
return _WarningTile(item: item);
},
),
),
],
),
);
}
Widget _buildEmptyState(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 32, horizontal: 16),
child: Column(
children: [
Icon(
Icons.check_circle_outline_rounded,
color: AppColor.success.withOpacity(0.6),
size: 48,
),
const SpaceHeight(12),
Text(
context.lang.no_warning,
style: AppStyle.md.copyWith(
fontWeight: FontWeight.w600,
color: AppColor.textPrimary,
),
),
const SpaceHeight(4),
Text(
context.lang.no_warning_desc,
style: AppStyle.sm.copyWith(color: AppColor.textSecondary),
),
],
),
);
}
}
class _WarningTile extends StatelessWidget {
final _WarningItem item;
const _WarningTile({required this.item});
@override
Widget build(BuildContext context) {
return InkWell(
onTap: () {},
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
child: Row(
children: [
// Warning icon
Container(
width: 44,
height: 44,
decoration: BoxDecoration(
color: item.severity == _WarningSeverity.tinggi
? AppColor.error.withOpacity(0.1)
: AppColor.warning.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Icon(
Icons.warning_amber_rounded,
color: item.severity == _WarningSeverity.tinggi
? AppColor.error
: AppColor.warning,
size: 22,
),
),
const SpaceWidth(12),
// Title + subtitle
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
item.title,
style: AppStyle.md.copyWith(
fontWeight: FontWeight.w700,
color: AppColor.textPrimary,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 2),
Text(
item.subtitle,
style: AppStyle.xs.copyWith(color: AppColor.textSecondary),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
),
const SpaceWidth(8),
// Severity badge
Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: item.severity == _WarningSeverity.tinggi
? AppColor.error.withOpacity(0.1)
: AppColor.warning.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Text(
item.severity == _WarningSeverity.tinggi
? context.lang.severity_high
: context.lang.severity_medium,
style: AppStyle.xs.copyWith(
color: item.severity == _WarningSeverity.tinggi
? AppColor.error
: AppColor.warning,
fontWeight: FontWeight.w700,
),
),
),
const SpaceWidth(4),
// Chevron
Icon(
Icons.chevron_right_rounded,
color: AppColor.textSecondary.withOpacity(0.5),
size: 20,
),
],
),
),
);
}
}
// ignore: unused_element
enum _WarningSeverity { tinggi }
// ignore: unused_element
class _WarningItem {
final String title;
final String subtitle;
final _WarningSeverity severity;
const _WarningItem({
required this.title,
required this.subtitle,
required this.severity,
});
}
@@ -1,233 +0,0 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:line_icons/line_icon.dart';
import 'package:line_icons/line_icons.dart';
import '../../../../../../common/theme/theme.dart';
import '../../../../application/outlet/selected_outlet/selected_outlet_bloc.dart';
import '../../../../common/extension/extension.dart';
import '../../../../domain/user/user.dart';
import '../../../components/spacer/spacer.dart';
class HomeOmsetBalance extends StatefulWidget {
final int totalOmset;
final User user;
const HomeOmsetBalance({super.key, required this.totalOmset, required this.user});
@override
State<HomeOmsetBalance> createState() => _HomeOmsetBalanceState();
}
class _HomeOmsetBalanceState extends State<HomeOmsetBalance> {
late DateTime _now;
late Timer _timer;
bool _isBalanceVisible = true;
@override
void initState() {
super.initState();
_now = DateTime.now();
_timer = Timer.periodic(const Duration(seconds: 1), (_) {
setState(() => _now = DateTime.now());
});
}
@override
void dispose() {
_timer.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 8),
clipBehavior: Clip.none,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
border: Border.all(color: AppColor.white.withOpacity(0.3), width: 1),
boxShadow: [
BoxShadow(
color: AppColor.white.withOpacity(0.1),
offset: const Offset(0, 2),
),
],
),
child: Column(
children: [_top(context), _middle(context), _bottom(context)],
),
);
}
Widget _bottom(BuildContext context) {
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {},
child: Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: AppColor.white,
border: Border(top: BorderSide(color: AppColor.border)),
borderRadius: const BorderRadius.vertical(
bottom: Radius.circular(16),
),
),
child: Row(
children: [
LineIcon(LineIcons.calendar, color: AppColor.black, size: 14),
SpaceWidth(6),
Expanded(
child: Text(
_now.toDate,
style: AppStyle.md.copyWith(
color: AppColor.black,
fontWeight: FontWeight.w600,
letterSpacing: 0.3,
),
),
),
LineIcon(LineIcons.clock, color: AppColor.textSecondary, size: 14),
SpaceWidth(4),
Text(
_now.toHourMinuteSecond,
style: AppStyle.md.copyWith(
color: AppColor.textSecondary,
fontWeight: FontWeight.w500,
fontFeatures: [const FontFeature.tabularFigures()],
),
),
],
),
),
);
}
Container _middle(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
decoration: BoxDecoration(color: AppColor.white),
child: Row(
children: [
Expanded(
child: GestureDetector(
onTap: () {},
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
context.lang.sales_today,
style: AppStyle.sm.copyWith(
color: AppColor.black,
fontWeight: FontWeight.w400,
letterSpacing: 0.3,
),
),
SpaceHeight(2),
AnimatedSwitcher(
duration: const Duration(milliseconds: 400),
transitionBuilder: (child, animation) {
return FadeTransition(
opacity: animation,
child: SlideTransition(
position: Tween<Offset>(
begin: const Offset(0, 0.3),
end: Offset.zero,
).animate(CurvedAnimation(
parent: animation,
curve: Curves.easeOutCubic,
)),
child: child,
),
);
},
child: _isBalanceVisible
? Text(
widget.totalOmset.currencyFormatRp,
key: const ValueKey('visible'),
style: AppStyle.xxl.copyWith(
color: AppColor.black,
fontWeight: FontWeight.w900,
letterSpacing: 0.3,
),
)
: Text(
'Rp ••••••••',
key: const ValueKey('hidden'),
style: AppStyle.xl.copyWith(
color: AppColor.black,
fontWeight: FontWeight.w900,
letterSpacing: 2,
),
),
),
],
),
),
),
GestureDetector(
onTap: () => setState(() => _isBalanceVisible = !_isBalanceVisible),
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 200),
child: LineIcon(
_isBalanceVisible ? LineIcons.eye : LineIcons.eyeSlash,
key: ValueKey(_isBalanceVisible),
color: AppColor.primary,
size: 16,
),
),
),
],
),
);
}
GestureDetector _top(BuildContext context) {
return GestureDetector(
onTap: () {},
child: BlocBuilder<SelectedOutletBloc, SelectedOutletState>(
builder: (context, state) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
gradient: LinearGradient(colors: AppColor.primaryGradient),
borderRadius: const BorderRadius.vertical(
top: Radius.circular(16),
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
state.displayName,
style: AppStyle.sm.copyWith(
color: AppColor.white,
fontWeight: FontWeight.w600,
letterSpacing: 0.3,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
),
SpaceWidth(6),
LineIcon(
LineIcons.alternateExchange,
color: AppColor.white,
size: 14,
),
],
),
);
},
),
);
}
}
@@ -1,324 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../application/outlet/outlet_list_loader/outlet_list_loader_bloc.dart';
import '../../../../application/outlet/selected_outlet/selected_outlet_bloc.dart';
import '../../../../common/theme/theme.dart';
import '../../../../domain/outlet/outlet.dart';
import '../../../components/spacer/spacer.dart';
import '../../../components/widgets/particle_card.dart';
class HomePromoBanner extends StatelessWidget {
const HomePromoBanner({super.key});
@override
Widget build(BuildContext context) {
return BlocBuilder<OutletListLoaderBloc, OutletListLoaderState>(
builder: (context, outletListState) {
if (outletListState.isFetching && outletListState.outlets.isEmpty) {
return const _PromoBannerSkeleton();
}
if (outletListState.outlets.isEmpty) {
return const SizedBox.shrink();
}
return BlocBuilder<SelectedOutletBloc, SelectedOutletState>(
builder: (context, selectedState) {
return Padding(
padding: const EdgeInsets.fromLTRB(
AppValue.padding,
24,
AppValue.padding,
0,
),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: [
// Card "Semua Outlet" di posisi pertama
_AllOutletCard(
isSelected: selectedState.isAllOutlets,
onTap: () {
if (!selectedState.isAllOutlets) {
context
.read<SelectedOutletBloc>()
.add(const SelectedOutletEvent.cleared());
}
},
),
const SpaceWidth(12),
for (int i = 0; i < outletListState.outlets.length; i++) ...[
_OutletCard(
outlet: outletListState.outlets[i],
isSelected: selectedState.selectedOutletId ==
outletListState.outlets[i].id,
onTap: () {
final tapped = outletListState.outlets[i];
if (selectedState.selectedOutletId == tapped.id) {
// Tap outlet yang sama → deselect (Semua Outlet)
context
.read<SelectedOutletBloc>()
.add(const SelectedOutletEvent.cleared());
} else {
context
.read<SelectedOutletBloc>()
.add(SelectedOutletEvent.selected(tapped));
}
},
),
if (i < outletListState.outlets.length - 1)
const SpaceWidth(12),
],
],
),
),
);
},
);
},
);
}
}
class _AllOutletCard extends StatelessWidget {
final bool isSelected;
final VoidCallback onTap;
const _AllOutletCard({
required this.isSelected,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
width: 130,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: isSelected ? AppColor.white : Colors.transparent,
width: 2,
),
boxShadow: isSelected
? [
BoxShadow(
color: AppColor.white.withOpacity(0.3),
blurRadius: 8,
offset: const Offset(0, 2),
),
]
: [],
),
child: Opacity(
opacity: isSelected ? 1.0 : 0.55,
child: ParticleCard(
height: 110,
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
decorationOpacity: 0.8,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
const Spacer(),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8, vertical: 3),
decoration: BoxDecoration(
color: AppColor.white.withOpacity(0.25),
borderRadius: BorderRadius.circular(12),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.store_rounded,
color: AppColor.white,
size: 12,
),
const SpaceWidth(4),
Text(
'Semua',
style: AppStyle.xs.copyWith(
color: AppColor.white,
fontWeight: FontWeight.w700,
fontSize: 9,
),
),
],
),
),
],
),
const SpaceHeight(6),
Text(
'Semua Outlet',
style: AppStyle.sm.copyWith(
color: AppColor.white,
fontWeight: FontWeight.w800,
height: 1.25,
),
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
],
),
),
),
),
);
}
}
class _OutletCard extends StatelessWidget {
final Outlet outlet;
final bool isSelected;
final VoidCallback onTap;
const _OutletCard({
required this.outlet,
required this.isSelected,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
width: 130,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: isSelected ? AppColor.white : Colors.transparent,
width: 2,
),
boxShadow: isSelected
? [
BoxShadow(
color: AppColor.white.withOpacity(0.3),
blurRadius: 8,
offset: const Offset(0, 2),
),
]
: [],
),
child: Opacity(
opacity: isSelected ? 1.0 : 0.55,
child: ParticleCard(
height: 110,
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
decorationOpacity: 0.8,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
const Spacer(),
_buildStatusBadge(outlet.isActive),
],
),
const SpaceHeight(6),
Text(
outlet.name,
style: AppStyle.sm.copyWith(
color: AppColor.white,
fontWeight: FontWeight.w800,
height: 1.25,
),
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
],
),
),
),
),
);
}
Widget _buildStatusBadge(bool isActive) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration(
color: isActive
? AppColor.success.withOpacity(0.9)
: AppColor.error.withOpacity(0.9),
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: (isActive ? AppColor.success : AppColor.error)
.withOpacity(0.3),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
isActive ? Icons.check_circle : Icons.warning_rounded,
color: AppColor.white,
size: 12,
),
const SpaceWidth(4),
Text(
isActive ? 'Sehat' : 'Tidak Sehat',
style: AppStyle.xs.copyWith(
color: AppColor.white,
fontWeight: FontWeight.w700,
fontSize: 9,
),
),
],
),
);
}
}
class _PromoBannerSkeleton extends StatelessWidget {
const _PromoBannerSkeleton();
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.fromLTRB(
AppValue.padding,
24,
AppValue.padding,
0,
),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: [
_skeletonCard(),
const SpaceWidth(12),
_skeletonCard(),
const SpaceWidth(12),
_skeletonCard(),
],
),
),
);
}
Widget _skeletonCard() {
return Container(
width: 130,
height: 110,
decoration: BoxDecoration(
color: AppColor.border,
borderRadius: BorderRadius.circular(12),
),
);
}
}

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