Compare commits
37
Commits
dev
...
ded5516bb1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ded5516bb1 | ||
|
|
77651e2e95 | ||
|
|
0ae599d8f8 | ||
|
|
36ecf95ef2 | ||
|
|
9dbc092313 | ||
|
|
ad11a9e43f | ||
|
|
20a9c65229 | ||
|
|
c03cfdfa80 | ||
|
|
c69a84569b | ||
|
|
d99dae0ca9 | ||
|
|
5a0f37fd3c | ||
|
|
e5c92b26c5 | ||
|
|
7955ebb7d1 | ||
|
|
508b72a52e | ||
|
|
8d982c468f | ||
|
|
6ebfae9d5b | ||
|
|
926e45170d | ||
|
|
f9cbd16b28 | ||
|
|
3ae2e93531 | ||
|
|
a182a3504c | ||
|
|
88972b28b5 | ||
|
|
2c8e9a3fb4 | ||
|
|
086c17a217 | ||
|
|
9a7681680d | ||
|
|
823e009121 | ||
|
|
602647ff26 | ||
|
|
d9a553708b | ||
|
|
aaa8eba31b | ||
|
|
31a82e14cb | ||
|
|
e88eff28f0 | ||
|
|
720c63b6c6 | ||
|
|
ca2cc8bf6b | ||
|
|
d56c3f89db | ||
|
|
f7f5682924 | ||
|
|
b51749a01b | ||
|
|
5fd870c507 | ||
|
|
d8d8fd9d16 |
@@ -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
|
||||
@@ -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>
|
||||
@@ -39,5 +39,13 @@ end
|
||||
post_install do |installer|
|
||||
installer.pods_project.targets.each do |target|
|
||||
flutter_additional_ios_build_settings(target)
|
||||
target.build_configurations.each do |config|
|
||||
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '14.0'
|
||||
config.build_settings['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
|
||||
|
||||
+45
-1
@@ -5,12 +5,19 @@ PODS:
|
||||
- Flutter
|
||||
- Firebase/CoreOnly (10.25.0):
|
||||
- FirebaseCore (= 10.25.0)
|
||||
- Firebase/Crashlytics (10.25.0):
|
||||
- Firebase/CoreOnly
|
||||
- FirebaseCrashlytics (~> 10.25.0)
|
||||
- Firebase/Messaging (10.25.0):
|
||||
- Firebase/CoreOnly
|
||||
- FirebaseMessaging (~> 10.25.0)
|
||||
- firebase_core (2.32.0):
|
||||
- Firebase/CoreOnly (= 10.25.0)
|
||||
- Flutter
|
||||
- firebase_crashlytics (3.5.7):
|
||||
- Firebase/Crashlytics (= 10.25.0)
|
||||
- firebase_core
|
||||
- Flutter
|
||||
- firebase_messaging (14.7.10):
|
||||
- Firebase/Messaging (= 10.25.0)
|
||||
- firebase_core
|
||||
@@ -19,8 +26,19 @@ PODS:
|
||||
- FirebaseCoreInternal (~> 10.0)
|
||||
- GoogleUtilities/Environment (~> 7.12)
|
||||
- GoogleUtilities/Logger (~> 7.12)
|
||||
- FirebaseCoreExtension (10.29.0):
|
||||
- FirebaseCore (~> 10.0)
|
||||
- FirebaseCoreInternal (10.29.0):
|
||||
- "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):
|
||||
- FirebaseCore (~> 10.0)
|
||||
- GoogleUtilities/Environment (~> 7.8)
|
||||
@@ -35,6 +53,16 @@ PODS:
|
||||
- GoogleUtilities/Reachability (~> 7.8)
|
||||
- GoogleUtilities/UserDefaults (~> 7.8)
|
||||
- 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_local_notifications (0.0.1):
|
||||
- Flutter
|
||||
@@ -84,6 +112,8 @@ PODS:
|
||||
- permission_handler_apple (9.3.0):
|
||||
- Flutter
|
||||
- PromisesObjC (2.4.0)
|
||||
- PromisesSwift (2.4.0):
|
||||
- PromisesObjC (= 2.4.0)
|
||||
- shared_preferences_foundation (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
@@ -97,6 +127,7 @@ DEPENDENCIES:
|
||||
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
|
||||
- device_info_plus (from `.symlinks/plugins/device_info_plus/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`)
|
||||
- Flutter (from `Flutter`)
|
||||
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
|
||||
@@ -113,13 +144,18 @@ SPEC REPOS:
|
||||
trunk:
|
||||
- Firebase
|
||||
- FirebaseCore
|
||||
- FirebaseCoreExtension
|
||||
- FirebaseCoreInternal
|
||||
- FirebaseCrashlytics
|
||||
- FirebaseInstallations
|
||||
- FirebaseMessaging
|
||||
- FirebaseRemoteConfigInterop
|
||||
- FirebaseSessions
|
||||
- GoogleDataTransport
|
||||
- GoogleUtilities
|
||||
- nanopb
|
||||
- PromisesObjC
|
||||
- PromisesSwift
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
connectivity_plus:
|
||||
@@ -128,6 +164,8 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/device_info_plus/ios"
|
||||
firebase_core:
|
||||
:path: ".symlinks/plugins/firebase_core/ios"
|
||||
firebase_crashlytics:
|
||||
:path: ".symlinks/plugins/firebase_crashlytics/ios"
|
||||
firebase_messaging:
|
||||
:path: ".symlinks/plugins/firebase_messaging/ios"
|
||||
Flutter:
|
||||
@@ -156,11 +194,16 @@ SPEC CHECKSUMS:
|
||||
device_info_plus: bf2e3232933866d73fe290f2942f2156cdd10342
|
||||
Firebase: 0312a2352584f782ea56f66d91606891d4607f06
|
||||
firebase_core: a626d00494efa398e7c54f25f1454a64c8abf197
|
||||
firebase_crashlytics: 17e856fabec68d993662abaf2f6fe2413f0abece
|
||||
firebase_messaging: 1541105e2a2a6ef8bd869bcc44157d31e82f3a50
|
||||
FirebaseCore: 7ec4d0484817f12c3373955bc87762d96842d483
|
||||
FirebaseCoreExtension: 705ca5b14bf71d2564a0ddc677df1fc86ffa600f
|
||||
FirebaseCoreInternal: df84dd300b561c27d5571684f389bf60b0a5c934
|
||||
FirebaseCrashlytics: 4b96efb0ce73b38b2a85e8b8bd1bd8f63f09d015
|
||||
FirebaseInstallations: 913cf60d0400ebd5d6b63a28b290372ab44590dd
|
||||
FirebaseMessaging: 88950ba9485052891ebe26f6c43a52bb62248952
|
||||
FirebaseRemoteConfigInterop: 6efda51fb5e2f15b16585197e26eaa09574e8a4d
|
||||
FirebaseSessions: dbd14adac65ce996228652c1fc3a3f576bdf3ecc
|
||||
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
|
||||
flutter_local_notifications: df98d66e515e1ca797af436137b4459b160ad8c9
|
||||
GoogleDataTransport: 6c09b596d841063d76d4288cc2d2f42cc36e1e2a
|
||||
@@ -172,10 +215,11 @@ SPEC CHECKSUMS:
|
||||
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
|
||||
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
|
||||
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
||||
PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851
|
||||
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
|
||||
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
|
||||
url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe
|
||||
|
||||
PODFILE CHECKSUM: e30f02f9d1c72c47bb6344a0a748c9d268180865
|
||||
PODFILE CHECKSUM: 5f0fa675e57bf6c9b78950d2f469725f8fefc4a3
|
||||
|
||||
COCOAPODS: 1.16.2
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import Flutter
|
||||
import UIKit
|
||||
import UserNotifications
|
||||
import Firebase
|
||||
|
||||
@main
|
||||
@objc class AppDelegate: FlutterAppDelegate {
|
||||
@@ -8,11 +9,18 @@ import UserNotifications
|
||||
_ application: UIApplication,
|
||||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
||||
) -> Bool {
|
||||
|
||||
FirebaseApp.configure()
|
||||
|
||||
// Set notification delegate so notifications show in foreground & background
|
||||
UNUserNotificationCenter.current().delegate = 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
|
||||
@@ -24,7 +32,7 @@ import UserNotifications
|
||||
completionHandler([.banner, .badge, .sound])
|
||||
}
|
||||
|
||||
// Called when user taps a notification (foreground or background)
|
||||
// Called when user taps a notification
|
||||
override func userNotificationCenter(
|
||||
_ center: UNUserNotificationCenter,
|
||||
didReceive response: UNNotificationResponse,
|
||||
@@ -32,4 +40,4 @@ import UserNotifications
|
||||
) {
|
||||
completionHandler()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -0,0 +1 @@
|
||||
2edd7a931624573eff7b4280d22a7f4e
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
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,
|
||||
);
|
||||
|
||||
final newState = result.fold(
|
||||
(f) => state.copyWith(failureOptionPurchasing: optionOf(f)),
|
||||
(purchasing) => state.copyWith(purchasing: purchasing),
|
||||
);
|
||||
|
||||
emit(newState.copyWith(isFetching: false));
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
+818
@@ -0,0 +1,818 @@
|
||||
// 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;
|
||||
|
||||
/// 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,
|
||||
});
|
||||
|
||||
$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,
|
||||
}) {
|
||||
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?,
|
||||
)
|
||||
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,
|
||||
});
|
||||
|
||||
@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,
|
||||
}) {
|
||||
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?,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
|
||||
class _$PurchasingAnalyticLoaderStateImpl
|
||||
implements _PurchasingAnalyticLoaderState {
|
||||
const _$PurchasingAnalyticLoaderStateImpl({
|
||||
required this.purchasing,
|
||||
required this.failureOptionPurchasing,
|
||||
this.isFetching = false,
|
||||
required this.dateFrom,
|
||||
required this.dateTo,
|
||||
this.outletId,
|
||||
});
|
||||
|
||||
@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
|
||||
String toString() {
|
||||
return 'PurchasingAnalyticLoaderState(purchasing: $purchasing, failureOptionPurchasing: $failureOptionPurchasing, isFetching: $isFetching, dateFrom: $dateFrom, dateTo: $dateTo, outletId: $outletId)';
|
||||
}
|
||||
|
||||
@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));
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
runtimeType,
|
||||
purchasing,
|
||||
failureOptionPurchasing,
|
||||
isFetching,
|
||||
dateFrom,
|
||||
dateTo,
|
||||
outletId,
|
||||
);
|
||||
|
||||
/// 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,
|
||||
}) = _$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;
|
||||
|
||||
/// 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;
|
||||
}
|
||||
+15
@@ -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;
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
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,
|
||||
}) = _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) {
|
||||
// Ambil device info dan FCM token secara paralel
|
||||
final results = await Future.wait([
|
||||
_deviceInfoService.getDeviceInfo(),
|
||||
_fcmService.getToken(),
|
||||
]);
|
||||
|
||||
final deviceInfo = results[0] as DeviceInfo;
|
||||
final fcmToken = results[1] as String?;
|
||||
final deviceInfo = await _deviceInfoService.getDeviceInfo();
|
||||
final fcmToken = await _fcmService.getToken();
|
||||
|
||||
failureOrAuth = await _repository.login(
|
||||
email: state.email,
|
||||
|
||||
@@ -65,5 +65,8 @@ class ThemeApp {
|
||||
),
|
||||
iconTheme: const IconThemeData(color: AppColor.white),
|
||||
),
|
||||
bottomSheetTheme: BottomSheetThemeData(
|
||||
backgroundColor: AppColor.white,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ class ApiPath {
|
||||
static const String productAnalytic = '/api/v1/analytics/products';
|
||||
static const String paymentMethodAnalytic =
|
||||
'/api/v1/analytics/paymentMethods';
|
||||
static const String purchasingAnalytic = '/api/v1/analytics/purchasing';
|
||||
|
||||
// Inventory
|
||||
static const String inventoryReportDetail =
|
||||
|
||||
@@ -101,17 +101,26 @@ class FcmService {
|
||||
}
|
||||
|
||||
// 7. Print FCM token for debugging
|
||||
final token = await getToken();
|
||||
debugPrint('[FCM] Token: $token');
|
||||
try {
|
||||
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 {
|
||||
final settings = await _messaging.requestPermission(
|
||||
alert: true,
|
||||
badge: true,
|
||||
sound: true,
|
||||
);
|
||||
debugPrint('[FCM] Permission status: ${settings.authorizationStatus}');
|
||||
try {
|
||||
final settings = await _messaging.requestPermission(
|
||||
alert: true,
|
||||
badge: true,
|
||||
sound: true,
|
||||
);
|
||||
debugPrint('[FCM] Permission status: ${settings.authorizationStatus}');
|
||||
} catch (e) {
|
||||
debugPrint('[FCM] Permission request failed: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _setupLocalNotifications() async {
|
||||
@@ -185,7 +194,15 @@ class FcmService {
|
||||
}
|
||||
|
||||
/// 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').
|
||||
Future<void> subscribeToTopic(String topic) =>
|
||||
|
||||
@@ -11,4 +11,5 @@ part 'entities/inventory_analytic_entity.dart';
|
||||
part 'entities/dashboard_analytic_entity.dart';
|
||||
part 'entities/product_analytic_entity.dart';
|
||||
part 'entities/payment_method_analytic_entity.dart';
|
||||
part 'entities/purchasing_analytic_entity.dart';
|
||||
part 'failures/analytic_failure.dart';
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,116 @@
|
||||
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 totalPurchaseOrders,
|
||||
required int totalQuantity,
|
||||
required double averagePurchaseOrderValue,
|
||||
required int totalIngredients,
|
||||
required int totalVendors,
|
||||
}) = _PurchasingAnalyticSummary;
|
||||
|
||||
factory PurchasingAnalyticSummary.empty() =>
|
||||
const PurchasingAnalyticSummary(
|
||||
totalPurchases: 0,
|
||||
totalPurchaseOrders: 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 purchaseOrders,
|
||||
required int quantity,
|
||||
required int ingredients,
|
||||
required int vendors,
|
||||
}) = _PurchasingAnalyticData;
|
||||
|
||||
factory PurchasingAnalyticData.empty() => PurchasingAnalyticData(
|
||||
date: DateTime.fromMillisecondsSinceEpoch(0),
|
||||
purchases: 0,
|
||||
purchaseOrders: 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,10 @@ abstract class IAnalyticRepository {
|
||||
required DateTime dateTo,
|
||||
String? outletId,
|
||||
});
|
||||
|
||||
Future<Either<AnalyticFailure, PurchasingAnalytic>> getPurchasing({
|
||||
required DateTime dateFrom,
|
||||
required DateTime dateTo,
|
||||
String? outletId,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -12,3 +12,4 @@ part 'dto/inventory_analytic_dto.dart';
|
||||
part 'dto/dashboard_analytic_dto.dart';
|
||||
part 'dto/product_analytic_dto.dart';
|
||||
part 'dto/payment_method_analytic_dto.dart';
|
||||
part 'dto/purchasing_analytic_dto.dart';
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -637,3 +637,139 @@ Map<String, dynamic> _$$PaymentMethodSummaryDtoImplToJson(
|
||||
'total_payments': instance.totalPayments,
|
||||
'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?,
|
||||
totalPurchaseOrders: json['total_purchase_orders'] 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,
|
||||
'total_purchase_orders': instance.totalPurchaseOrders,
|
||||
'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?,
|
||||
purchaseOrders: json['purchase_orders'] 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,
|
||||
'purchase_orders': instance.purchaseOrders,
|
||||
'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,
|
||||
};
|
||||
|
||||
@@ -231,4 +231,35 @@ class AnalyticRemoteDataProvider {
|
||||
return DC.error(AnalyticFailure.serverError(e));
|
||||
}
|
||||
}
|
||||
|
||||
Future<DC<AnalyticFailure, PurchasingAnalyticDto>> fetchPurchasing({
|
||||
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.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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
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: 'total_purchase_orders') num? totalPurchaseOrders,
|
||||
@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,
|
||||
totalPurchaseOrders: totalPurchaseOrders?.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: 'purchase_orders') num? purchaseOrders,
|
||||
@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,
|
||||
purchaseOrders: purchaseOrders?.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,25 @@ class AnalyticRepository implements IAnalyticRepository {
|
||||
return left(const AnalyticFailure.unexpectedError());
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<AnalyticFailure, PurchasingAnalytic>> getPurchasing({
|
||||
required DateTime dateFrom,
|
||||
required DateTime dateTo,
|
||||
String? outletId,
|
||||
}) async {
|
||||
try {
|
||||
final result = await _dataProvider.fetchPurchasing(
|
||||
dateFrom: dateFrom,
|
||||
dateTo: dateTo,
|
||||
outletId: _resolveOutletId(outletId),
|
||||
);
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,8 @@ import 'package:apskel_owner_flutter/application/analytic/product_analytic_loade
|
||||
as _i221;
|
||||
import 'package:apskel_owner_flutter/application/analytic/profit_loss_loader/profit_loss_loader_bloc.dart'
|
||||
as _i11;
|
||||
import 'package:apskel_owner_flutter/application/analytic/purchasing_analytic_loader/purchasing_analytic_loader_bloc.dart'
|
||||
as _i755;
|
||||
import 'package:apskel_owner_flutter/application/analytic/sales_loader/sales_loader_bloc.dart'
|
||||
as _i889;
|
||||
import 'package:apskel_owner_flutter/application/auth/auth_bloc.dart' as _i945;
|
||||
@@ -248,6 +250,9 @@ extension GetItInjectableX on _i174.GetIt {
|
||||
gh<_i850.OutletLocalDataProvider>(),
|
||||
),
|
||||
);
|
||||
gh.factory<_i755.PurchasingAnalyticLoaderBloc>(
|
||||
() => _i755.PurchasingAnalyticLoaderBloc(gh<_i477.IAnalyticRepository>()),
|
||||
);
|
||||
gh.factory<_i889.SalesLoaderBloc>(
|
||||
() => _i889.SalesLoaderBloc(gh<_i477.IAnalyticRepository>()),
|
||||
);
|
||||
|
||||
+17
-5
@@ -1,3 +1,4 @@
|
||||
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
@@ -25,12 +26,23 @@ void main() async {
|
||||
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
|
||||
await getIt<FcmService>().initialize(
|
||||
onMessageTap: (message) {
|
||||
debugPrint('[FCM] Navigate based on: ${message.data}');
|
||||
},
|
||||
);
|
||||
try {
|
||||
await getIt<FcmService>().initialize(
|
||||
onMessageTap: (message) {
|
||||
debugPrint('[FCM] Navigate based on: ${message.data}');
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('[FCM] Initialization failed: $e');
|
||||
}
|
||||
|
||||
runApp(const AppWidget());
|
||||
}
|
||||
|
||||
@@ -56,6 +56,11 @@ class HomeFeature extends StatelessWidget {
|
||||
iconPath: Assets.icons.icReportProduct.path,
|
||||
onTap: () => context.router.push(ProductAnalyticRoute()),
|
||||
),
|
||||
// HomeFeatureTile(
|
||||
// title: context.lang.inventory,
|
||||
// iconPath: Assets.icons.icReportProduct.path,
|
||||
// onTap: () => context.router.push(InventoryRoute()),
|
||||
// ),
|
||||
],
|
||||
),
|
||||
// Row(
|
||||
|
||||
@@ -1,51 +1,70 @@
|
||||
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/purchasing_analytic_loader/purchasing_analytic_loader_bloc.dart';
|
||||
import '../../../application/outlet/outlet_list_loader/outlet_list_loader_bloc.dart';
|
||||
import '../../../common/extension/extension.dart';
|
||||
import '../../../common/theme/theme.dart';
|
||||
import '../../../injection.dart';
|
||||
import '../../components/appbar/appbar.dart';
|
||||
import 'widgets/purchase_tile.dart';
|
||||
import '../../components/field/date_range_picker_field.dart';
|
||||
import '../../components/spacer/spacer.dart';
|
||||
import 'widgets/ingredient_card.dart';
|
||||
import 'widgets/outlet_selector_field.dart';
|
||||
import 'widgets/purchase_daily_tile.dart';
|
||||
import 'widgets/stat_card.dart';
|
||||
import 'widgets/status_chip.dart';
|
||||
import 'widgets/vendor_card.dart';
|
||||
|
||||
@RoutePage()
|
||||
class PurchasePage extends StatefulWidget {
|
||||
class PurchasePage extends StatefulWidget implements AutoRouteWrapper {
|
||||
const PurchasePage({super.key});
|
||||
|
||||
@override
|
||||
State<PurchasePage> createState() => _PurchasePageState();
|
||||
|
||||
@override
|
||||
Widget wrappedRoute(BuildContext context) => MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider(
|
||||
create: (context) => getIt<PurchasingAnalyticLoaderBloc>()
|
||||
..add(const PurchasingAnalyticLoaderEvent.fetched()),
|
||||
),
|
||||
BlocProvider(
|
||||
create: (context) => getIt<OutletListLoaderBloc>()
|
||||
..add(const OutletListLoaderEvent.fetched()),
|
||||
),
|
||||
],
|
||||
child: this,
|
||||
);
|
||||
}
|
||||
|
||||
class _PurchasePageState extends State<PurchasePage>
|
||||
with TickerProviderStateMixin {
|
||||
late AnimationController cardAnimation;
|
||||
String selectedFilter = 'Semua';
|
||||
final List<String> filterOptions = [
|
||||
'Semua',
|
||||
'Pending',
|
||||
'Completed',
|
||||
'Cancelled',
|
||||
];
|
||||
|
||||
final List<Map<String, dynamic>> purchaseData = [
|
||||
];
|
||||
late AnimationController _fadeController;
|
||||
late Animation<double> _fadeAnimation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
cardAnimation = AnimationController(
|
||||
duration: const Duration(milliseconds: 1200),
|
||||
_fadeController = AnimationController(
|
||||
duration: const Duration(milliseconds: 600),
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
cardAnimation.forward();
|
||||
_fadeAnimation = CurvedAnimation(
|
||||
parent: _fadeController,
|
||||
curve: Curves.easeOut,
|
||||
);
|
||||
Future.delayed(const Duration(milliseconds: 200), () {
|
||||
if (mounted) _fadeController.forward();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
cardAnimation.dispose();
|
||||
_fadeController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -53,141 +72,521 @@ class _PurchasePageState extends State<PurchasePage>
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: AppColor.background,
|
||||
body: CustomScrollView(
|
||||
slivers: [
|
||||
SliverAppBar(
|
||||
expandedHeight: 120.0,
|
||||
floating: false,
|
||||
pinned: true,
|
||||
elevation: 0,
|
||||
backgroundColor: AppColor.primary,
|
||||
|
||||
flexibleSpace: CustomAppBar(title: context.lang.purchase),
|
||||
body: MultiBlocListener(
|
||||
listeners: [
|
||||
// Re-fetch when date range changes
|
||||
BlocListener<PurchasingAnalyticLoaderBloc,
|
||||
PurchasingAnalyticLoaderState>(
|
||||
listenWhen: (prev, curr) =>
|
||||
prev.dateFrom != curr.dateFrom ||
|
||||
prev.dateTo != curr.dateTo,
|
||||
listener: (context, _) => context
|
||||
.read<PurchasingAnalyticLoaderBloc>()
|
||||
.add(const PurchasingAnalyticLoaderEvent.fetched()),
|
||||
),
|
||||
|
||||
// Stats Cards
|
||||
SliverToBoxAdapter(
|
||||
child: Container(
|
||||
color: AppColor.background,
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: PurchaseStatCard(
|
||||
title: context.lang.total_purchase,
|
||||
value: 'Rp 0',
|
||||
icon: LineIcons.shoppingCart,
|
||||
iconColor: AppColor.success,
|
||||
cardAnimation: cardAnimation,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: PurchaseStatCard(
|
||||
title: context.lang.pending_order,
|
||||
value: '0 ${context.lang.orders}',
|
||||
icon: LineIcons.clock,
|
||||
iconColor: AppColor.warning,
|
||||
cardAnimation: cardAnimation,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Re-fetch when outlet changes
|
||||
BlocListener<PurchasingAnalyticLoaderBloc,
|
||||
PurchasingAnalyticLoaderState>(
|
||||
listenWhen: (prev, curr) => prev.outletId != curr.outletId,
|
||||
listener: (context, _) => context
|
||||
.read<PurchasingAnalyticLoaderBloc>()
|
||||
.add(const PurchasingAnalyticLoaderEvent.fetched()),
|
||||
),
|
||||
],
|
||||
child: BlocBuilder<OutletListLoaderBloc, OutletListLoaderState>(
|
||||
builder: (context, outletListState) {
|
||||
return BlocBuilder<PurchasingAnalyticLoaderBloc,
|
||||
PurchasingAnalyticLoaderState>(
|
||||
builder: (context, state) {
|
||||
return CustomScrollView(
|
||||
slivers: [
|
||||
// App Bar
|
||||
SliverAppBar(
|
||||
expandedHeight: 120.0,
|
||||
floating: false,
|
||||
pinned: true,
|
||||
elevation: 0,
|
||||
backgroundColor: AppColor.primary,
|
||||
flexibleSpace:
|
||||
CustomAppBar(title: context.lang.purchase),
|
||||
),
|
||||
|
||||
// Filter Section
|
||||
SliverToBoxAdapter(
|
||||
child: Container(
|
||||
color: AppColor.surface,
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
context.lang.history_purchase,
|
||||
style: AppStyle.lg.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
// Date Range + Outlet Picker
|
||||
SliverToBoxAdapter(
|
||||
child: FadeTransition(
|
||||
opacity: _fadeAnimation,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 0),
|
||||
child: Column(
|
||||
children: [
|
||||
DateRangePickerField(
|
||||
maxDate: DateTime.now(),
|
||||
startDate: state.dateFrom,
|
||||
endDate: state.dateTo,
|
||||
onChanged: (startDate, endDate) {
|
||||
context
|
||||
.read<PurchasingAnalyticLoaderBloc>()
|
||||
.add(
|
||||
PurchasingAnalyticLoaderEvent
|
||||
.rangeDateChanged(
|
||||
startDate!,
|
||||
endDate!,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
const SpaceHeight(8),
|
||||
PurchaseOutletSelectorField(
|
||||
selectedOutletId: state.outletId,
|
||||
outlets: outletListState.outlets,
|
||||
isLoading: outletListState.isFetching,
|
||||
onOutletChanged: (outletId) {
|
||||
context
|
||||
.read<PurchasingAnalyticLoaderBloc>()
|
||||
.add(
|
||||
PurchasingAnalyticLoaderEvent
|
||||
.outletChanged(outletId),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 6,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.primary,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Text(
|
||||
'${purchaseData.length} ${context.lang.orders}',
|
||||
style: AppStyle.sm.copyWith(
|
||||
color: AppColor.textWhite,
|
||||
),
|
||||
|
||||
const SliverToBoxAdapter(child: SpaceHeight(16)),
|
||||
|
||||
// Summary Section
|
||||
SliverToBoxAdapter(
|
||||
child: FadeTransition(
|
||||
opacity: _fadeAnimation,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
context.lang.summary,
|
||||
style: AppStyle.xxl.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColor.textPrimary,
|
||||
),
|
||||
),
|
||||
const SpaceHeight(16),
|
||||
state.isFetching
|
||||
? _buildSummaryShimmer()
|
||||
: _buildSummaryCards(state),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SliverToBoxAdapter(child: SpaceHeight(24)),
|
||||
|
||||
// Total Purchases Highlight Card
|
||||
SliverToBoxAdapter(
|
||||
child: FadeTransition(
|
||||
opacity: _fadeAnimation,
|
||||
child: state.isFetching
|
||||
? _buildHighlightShimmer()
|
||||
: _buildTotalPurchasesCard(state),
|
||||
),
|
||||
),
|
||||
|
||||
const SliverToBoxAdapter(child: SpaceHeight(24)),
|
||||
|
||||
// Daily Breakdown Header
|
||||
SliverToBoxAdapter(
|
||||
child: FadeTransition(
|
||||
opacity: _fadeAnimation,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Text(
|
||||
context.lang.daily_breakdown,
|
||||
style: AppStyle.xxl.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColor.textPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SliverToBoxAdapter(child: SpaceHeight(12)),
|
||||
|
||||
state.isFetching
|
||||
? _buildListShimmer()
|
||||
: _buildDailyList(state),
|
||||
|
||||
const SliverToBoxAdapter(child: SpaceHeight(24)),
|
||||
|
||||
// Ingredient Header
|
||||
SliverToBoxAdapter(
|
||||
child: FadeTransition(
|
||||
opacity: _fadeAnimation,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Text(
|
||||
'Bahan Baku',
|
||||
style: AppStyle.xxl.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColor.textPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SliverToBoxAdapter(child: SpaceHeight(12)),
|
||||
|
||||
state.isFetching
|
||||
? _buildListShimmer()
|
||||
: _buildIngredientList(state),
|
||||
|
||||
const SliverToBoxAdapter(child: SpaceHeight(24)),
|
||||
|
||||
// Vendor Header
|
||||
SliverToBoxAdapter(
|
||||
child: FadeTransition(
|
||||
opacity: _fadeAnimation,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Text(
|
||||
'Vendor',
|
||||
style: AppStyle.xxl.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColor.textPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SliverToBoxAdapter(child: SpaceHeight(12)),
|
||||
|
||||
state.isFetching
|
||||
? _buildListShimmer()
|
||||
: _buildVendorList(state),
|
||||
|
||||
const SliverToBoxAdapter(child: SpaceHeight(80)),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Summary Cards ────────────────────────────────────────────────────────
|
||||
|
||||
Widget _buildSummaryCards(PurchasingAnalyticLoaderState state) {
|
||||
final s = state.purchasing.summary;
|
||||
return Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: PurchaseStatCard(
|
||||
title: 'Total Pembelian',
|
||||
value: s.totalPurchases.currencyFormatRp,
|
||||
icon: LineIcons.shoppingCart,
|
||||
iconColor: AppColor.primary,
|
||||
cardAnimation: _fadeAnimation,
|
||||
),
|
||||
),
|
||||
const SpaceWidth(12),
|
||||
Expanded(
|
||||
child: PurchaseStatCard(
|
||||
title: 'Total PO',
|
||||
value: '${s.totalPurchaseOrders} PO',
|
||||
icon: LineIcons.fileAlt,
|
||||
iconColor: AppColor.info,
|
||||
cardAnimation: _fadeAnimation,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SpaceHeight(12),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: PurchaseStatCard(
|
||||
title: 'Total Qty',
|
||||
value: '${s.totalQuantity} pcs',
|
||||
icon: LineIcons.boxes,
|
||||
iconColor: AppColor.warning,
|
||||
cardAnimation: _fadeAnimation,
|
||||
),
|
||||
),
|
||||
const SpaceWidth(12),
|
||||
Expanded(
|
||||
child: PurchaseStatCard(
|
||||
title: 'Rata-rata PO',
|
||||
value: s.averagePurchaseOrderValue.round().currencyFormatRp,
|
||||
icon: LineIcons.dollarSign,
|
||||
iconColor: AppColor.secondary,
|
||||
cardAnimation: _fadeAnimation,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SpaceHeight(12),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: PurchaseStatCard(
|
||||
title: 'Bahan Baku',
|
||||
value: '${s.totalIngredients} item',
|
||||
icon: LineIcons.leaf,
|
||||
iconColor: AppColor.secondaryDark,
|
||||
cardAnimation: _fadeAnimation,
|
||||
),
|
||||
),
|
||||
const SpaceWidth(12),
|
||||
Expanded(
|
||||
child: PurchaseStatCard(
|
||||
title: 'Vendor',
|
||||
value: '${s.totalVendors} vendor',
|
||||
icon: LineIcons.truck,
|
||||
iconColor: AppColor.primaryDark,
|
||||
cardAnimation: _fadeAnimation,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTotalPurchasesCard(PurchasingAnalyticLoaderState state) {
|
||||
return TweenAnimationBuilder<double>(
|
||||
tween: Tween(begin: 0.0, end: 1.0),
|
||||
duration: const Duration(milliseconds: 900),
|
||||
curve: Curves.bounceOut,
|
||||
builder: (context, value, _) {
|
||||
return Transform.scale(
|
||||
scale: value,
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16),
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
colors: AppColor.primaryGradient,
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: AppColor.primary.withOpacity(0.35),
|
||||
blurRadius: 16,
|
||||
offset: const Offset(0, 6),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: const Icon(
|
||||
LineIcons.shoppingBag,
|
||||
color: AppColor.textWhite,
|
||||
size: 28,
|
||||
),
|
||||
),
|
||||
const SpaceWidth(16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
context.lang.total_purchase,
|
||||
style: TextStyle(
|
||||
color: AppColor.textWhite.withOpacity(0.9),
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SpaceHeight(4),
|
||||
Text(
|
||||
state.purchasing.summary.totalPurchases
|
||||
.currencyFormatRp,
|
||||
style: const TextStyle(
|
||||
color: AppColor.textWhite,
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
SizedBox(
|
||||
height: 40,
|
||||
child: ListView.builder(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: filterOptions.length,
|
||||
itemBuilder: (context, index) {
|
||||
final isSelected =
|
||||
selectedFilter == filterOptions[index];
|
||||
return PurchaseStatusChip(
|
||||
isSelected: isSelected,
|
||||
text: filterOptions[index],
|
||||
onSelected: (selected) {
|
||||
setState(() {
|
||||
selectedFilter = filterOptions[index];
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Purchase List
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
sliver: SliverList(
|
||||
delegate: SliverChildBuilderDelegate((context, index) {
|
||||
final purchase = purchaseData[index];
|
||||
return AnimatedBuilder(
|
||||
animation: cardAnimation,
|
||||
builder: (context, child) {
|
||||
final delay = index * 0.1;
|
||||
final animValue = (cardAnimation.value - delay).clamp(
|
||||
0.0,
|
||||
1.0,
|
||||
);
|
||||
|
||||
return Transform.translate(
|
||||
offset: Offset(0, 30 * (1 - animValue)),
|
||||
child: Opacity(
|
||||
opacity: animValue,
|
||||
child: PurchaseTile(purchase: purchase, index: index),
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
'${state.purchasing.summary.totalPurchaseOrders} PO',
|
||||
style: const TextStyle(
|
||||
color: AppColor.textWhite,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}, childCount: purchaseData.length),
|
||||
),
|
||||
Text(
|
||||
'purchase order',
|
||||
style: TextStyle(
|
||||
color: AppColor.textWhite.withOpacity(0.8),
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Bottom spacing for FAB
|
||||
const SliverToBoxAdapter(child: SizedBox(height: 80)),
|
||||
],
|
||||
// ─── Lists ────────────────────────────────────────────────────────────────
|
||||
|
||||
Widget _buildDailyList(PurchasingAnalyticLoaderState state) {
|
||||
if (state.purchasing.data.isEmpty) {
|
||||
return SliverToBoxAdapter(
|
||||
child: _buildEmptyState('Tidak ada data harian'));
|
||||
}
|
||||
return SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) => PurchaseDailyTile(
|
||||
data: state.purchasing.data[index],
|
||||
index: index,
|
||||
animation: _fadeAnimation,
|
||||
),
|
||||
childCount: state.purchasing.data.length,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildIngredientList(PurchasingAnalyticLoaderState state) {
|
||||
if (state.purchasing.ingredientData.isEmpty) {
|
||||
return SliverToBoxAdapter(
|
||||
child: _buildEmptyState('Tidak ada data bahan baku'));
|
||||
}
|
||||
return SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) => PurchaseIngredientCard(
|
||||
data: state.purchasing.ingredientData[index],
|
||||
index: index,
|
||||
animation: _fadeAnimation,
|
||||
),
|
||||
childCount: state.purchasing.ingredientData.length,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildVendorList(PurchasingAnalyticLoaderState state) {
|
||||
if (state.purchasing.vendorData.isEmpty) {
|
||||
return SliverToBoxAdapter(
|
||||
child: _buildEmptyState('Tidak ada data vendor'));
|
||||
}
|
||||
return SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) => PurchaseVendorCard(
|
||||
data: state.purchasing.vendorData[index],
|
||||
index: index,
|
||||
animation: _fadeAnimation,
|
||||
),
|
||||
childCount: state.purchasing.vendorData.length,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Empty State ──────────────────────────────────────────────────────────
|
||||
|
||||
Widget _buildEmptyState(String message) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
padding: const EdgeInsets.all(24),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.surface,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: AppColor.border.withOpacity(0.3)),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
message,
|
||||
style: AppStyle.md.copyWith(color: AppColor.textSecondary),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Shimmer Loaders ──────────────────────────────────────────────────────
|
||||
|
||||
Widget _buildSummaryShimmer() {
|
||||
return Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(child: _shimmerCard(height: 100)),
|
||||
const SpaceWidth(12),
|
||||
Expanded(child: _shimmerCard(height: 100)),
|
||||
],
|
||||
),
|
||||
const SpaceHeight(12),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(child: _shimmerCard(height: 100)),
|
||||
const SpaceWidth(12),
|
||||
Expanded(child: _shimmerCard(height: 100)),
|
||||
],
|
||||
),
|
||||
const SpaceHeight(12),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(child: _shimmerCard(height: 100)),
|
||||
const SpaceWidth(12),
|
||||
Expanded(child: _shimmerCard(height: 100)),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHighlightShimmer() {
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: _shimmerCard(height: 88),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildListShimmer() {
|
||||
return SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(_, __) => Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
|
||||
child: _shimmerCard(height: 72),
|
||||
),
|
||||
childCount: 4,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _shimmerCard({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(12),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:line_icons/line_icons.dart';
|
||||
|
||||
import '../../../../common/extension/extension.dart';
|
||||
import '../../../../common/theme/theme.dart';
|
||||
import '../../../../domain/analytic/analytic.dart';
|
||||
|
||||
class PurchaseIngredientCard extends StatelessWidget {
|
||||
final PurchasingIngredientData data;
|
||||
final int index;
|
||||
final Animation<double> animation;
|
||||
|
||||
const PurchaseIngredientCard({
|
||||
super.key,
|
||||
required this.data,
|
||||
required this.index,
|
||||
required this.animation,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FadeTransition(
|
||||
opacity: animation,
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.surface,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(color: AppColor.border.withOpacity(0.25)),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.04),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 3),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// Rank badge
|
||||
Container(
|
||||
width: 36,
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
colors: AppColor.primaryGradient,
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
'${index + 1}',
|
||||
style: AppStyle.sm.copyWith(
|
||||
color: AppColor.textWhite,
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
|
||||
// Ingredient icon
|
||||
Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.secondary.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Icon(
|
||||
LineIcons.leaf,
|
||||
color: AppColor.secondary,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
|
||||
// Name & details
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
data.ingredientName,
|
||||
style: AppStyle.md.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColor.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
_Chip(
|
||||
label: '${data.quantity} pcs',
|
||||
color: AppColor.warning,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
_Chip(
|
||||
label: '${data.purchaseOrderCount} PO',
|
||||
color: AppColor.info,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Cost column
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
data.totalCost.currencyFormatRp,
|
||||
style: AppStyle.md.copyWith(
|
||||
fontWeight: FontWeight.w800,
|
||||
color: AppColor.primary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
'@ ${data.averageUnitCost.round().currencyFormatRp}',
|
||||
style: AppStyle.xs.copyWith(
|
||||
color: AppColor.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Chip extends StatelessWidget {
|
||||
final String label;
|
||||
final Color color;
|
||||
|
||||
const _Chip({required this.label, required this.color});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
label,
|
||||
style: AppStyle.xs.copyWith(
|
||||
color: color,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,264 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../../common/theme/theme.dart';
|
||||
import '../../../../domain/outlet/outlet.dart';
|
||||
|
||||
/// Outlet selector field — styled like DateRangePickerField.
|
||||
/// Opens a bottom sheet to pick an outlet or "Semua Outlet".
|
||||
class PurchaseOutletSelectorField extends StatefulWidget {
|
||||
final String? selectedOutletId;
|
||||
final List<Outlet> outlets;
|
||||
final bool isLoading;
|
||||
final ValueChanged<String?> onOutletChanged;
|
||||
|
||||
const PurchaseOutletSelectorField({
|
||||
super.key,
|
||||
required this.selectedOutletId,
|
||||
required this.outlets,
|
||||
required this.isLoading,
|
||||
required this.onOutletChanged,
|
||||
});
|
||||
|
||||
@override
|
||||
State<PurchaseOutletSelectorField> createState() =>
|
||||
_PurchaseOutletSelectorFieldState();
|
||||
}
|
||||
|
||||
class _PurchaseOutletSelectorFieldState
|
||||
extends State<PurchaseOutletSelectorField> {
|
||||
bool _isPressed = false;
|
||||
|
||||
Outlet? get _selectedOutlet => widget.outlets
|
||||
.where((o) => o.id == widget.selectedOutletId)
|
||||
.firstOrNull;
|
||||
|
||||
String get _label => _selectedOutlet?.name ?? 'Semua Outlet';
|
||||
|
||||
bool get _hasValue => widget.selectedOutletId != null;
|
||||
|
||||
void _showSheet() {
|
||||
if (widget.isLoading) return;
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||||
),
|
||||
builder: (_) => _OutletBottomSheet(
|
||||
outlets: widget.outlets,
|
||||
selectedOutletId: widget.selectedOutletId,
|
||||
onSelected: (outletId) {
|
||||
Navigator.pop(context);
|
||||
widget.onOutletChanged(outletId);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: _showSheet,
|
||||
onTapDown: (_) => setState(() => _isPressed = true),
|
||||
onTapUp: (_) => setState(() => _isPressed = false),
|
||||
onTapCancel: () => setState(() => _isPressed = false),
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 150),
|
||||
height: 52,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: _isPressed ? AppColor.backgroundLight : AppColor.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: _isPressed ? AppColor.primary : AppColor.border,
|
||||
width: _isPressed ? 2 : 1,
|
||||
),
|
||||
boxShadow: _isPressed
|
||||
? [
|
||||
BoxShadow(
|
||||
color: AppColor.primary.withOpacity(0.1),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
]
|
||||
: null,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
_label,
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight:
|
||||
_hasValue ? FontWeight.w500 : FontWeight.w400,
|
||||
color: _hasValue
|
||||
? AppColor.textPrimary
|
||||
: AppColor.textSecondary,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.primary.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: widget.isLoading
|
||||
? const SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: AppColor.primary,
|
||||
),
|
||||
)
|
||||
: const Icon(
|
||||
Icons.store_rounded,
|
||||
size: 20,
|
||||
color: AppColor.primary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Bottom Sheet ─────────────────────────────────────────────────────────────
|
||||
|
||||
class _OutletBottomSheet extends StatelessWidget {
|
||||
final List<Outlet> outlets;
|
||||
final String? selectedOutletId;
|
||||
final ValueChanged<String?> onSelected;
|
||||
|
||||
const _OutletBottomSheet({
|
||||
required this.outlets,
|
||||
required this.selectedOutletId,
|
||||
required this.onSelected,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 16, 20, 20),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Handle bar
|
||||
Center(
|
||||
child: Container(
|
||||
width: 40,
|
||||
height: 4,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade300,
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Pilih Outlet',
|
||||
style: AppStyle.lg.copyWith(fontWeight: FontWeight.w700),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// "Semua Outlet" option
|
||||
_OutletItem(
|
||||
label: 'Semua Outlet',
|
||||
icon: Icons.store_rounded,
|
||||
isSelected: selectedOutletId == null,
|
||||
onTap: () => onSelected(null),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
|
||||
// Individual outlets
|
||||
...outlets.map(
|
||||
(outlet) => Column(
|
||||
children: [
|
||||
_OutletItem(
|
||||
label: outlet.name,
|
||||
icon: Icons.storefront_rounded,
|
||||
isSelected: selectedOutletId == outlet.id,
|
||||
isActive: outlet.isActive,
|
||||
onTap: () => onSelected(outlet.id),
|
||||
),
|
||||
if (outlet != outlets.last) const Divider(height: 1),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── List Item ────────────────────────────────────────────────────────────────
|
||||
|
||||
class _OutletItem extends StatelessWidget {
|
||||
final String label;
|
||||
final IconData icon;
|
||||
final bool isSelected;
|
||||
final bool? isActive;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const _OutletItem({
|
||||
required this.label,
|
||||
required this.icon,
|
||||
required this.isSelected,
|
||||
required this.onTap,
|
||||
this.isActive,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListTile(
|
||||
onTap: onTap,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2),
|
||||
leading: Container(
|
||||
width: 36,
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected
|
||||
? AppColor.primary.withOpacity(0.1)
|
||||
: AppColor.background,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Icon(
|
||||
icon,
|
||||
size: 18,
|
||||
color: isSelected ? AppColor.primary : AppColor.textSecondary,
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
label,
|
||||
style: AppStyle.md.copyWith(
|
||||
fontWeight: isSelected ? FontWeight.w700 : FontWeight.w500,
|
||||
color: isSelected ? AppColor.primary : AppColor.textPrimary,
|
||||
),
|
||||
),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (isActive != null) ...[
|
||||
Container(
|
||||
width: 8,
|
||||
height: 8,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: isActive! ? AppColor.success : AppColor.error,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
],
|
||||
if (isSelected)
|
||||
const Icon(Icons.check_rounded, color: AppColor.primary, size: 20),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:line_icons/line_icons.dart';
|
||||
|
||||
import '../../../../common/extension/extension.dart';
|
||||
import '../../../../common/theme/theme.dart';
|
||||
import '../../../../domain/analytic/analytic.dart';
|
||||
|
||||
class PurchaseDailyTile extends StatelessWidget {
|
||||
final PurchasingAnalyticData data;
|
||||
final int index;
|
||||
final Animation<double> animation;
|
||||
|
||||
const PurchaseDailyTile({
|
||||
super.key,
|
||||
required this.data,
|
||||
required this.index,
|
||||
required this.animation,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FadeTransition(
|
||||
opacity: animation,
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.surface,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(color: AppColor.border.withOpacity(0.25)),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.04),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 3),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: ExpansionTile(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
),
|
||||
collapsedShape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
),
|
||||
leading: Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.primary.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Icon(
|
||||
LineIcons.calendar,
|
||||
color: AppColor.primary,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
data.date.toDate,
|
||||
style: AppStyle.md.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColor.textPrimary,
|
||||
),
|
||||
),
|
||||
subtitle: Text(
|
||||
data.purchases.currencyFormatRp,
|
||||
style: AppStyle.sm.copyWith(
|
||||
color: AppColor.primary,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
trailing: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.info.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Text(
|
||||
'${data.purchaseOrders} PO',
|
||||
style: AppStyle.xs.copyWith(
|
||||
color: AppColor.info,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _DetailItem(
|
||||
icon: LineIcons.boxes,
|
||||
label: 'Qty',
|
||||
value: '${data.quantity} pcs',
|
||||
color: AppColor.warning,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: _DetailItem(
|
||||
icon: LineIcons.leaf,
|
||||
label: 'Bahan',
|
||||
value: '${data.ingredients} item',
|
||||
color: AppColor.secondary,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: _DetailItem(
|
||||
icon: LineIcons.truck,
|
||||
label: 'Vendor',
|
||||
value: '${data.vendors}',
|
||||
color: AppColor.primaryDark,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _DetailItem extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final String label;
|
||||
final String value;
|
||||
final Color color;
|
||||
|
||||
const _DetailItem({
|
||||
required this.icon,
|
||||
required this.label,
|
||||
required this.value,
|
||||
required this.color,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(icon, color: color, size: 18),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
label,
|
||||
style: AppStyle.xs.copyWith(color: AppColor.textSecondary),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
value,
|
||||
style: AppStyle.sm.copyWith(
|
||||
color: AppColor.textPrimary,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,314 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:line_icons/line_icons.dart';
|
||||
|
||||
import '../../../../common/extension/extension.dart';
|
||||
import '../../../../common/theme/theme.dart';
|
||||
|
||||
class PurchaseTile extends StatelessWidget {
|
||||
final Map<String, dynamic> purchase;
|
||||
final int index;
|
||||
const PurchaseTile({super.key, required this.purchase, required this.index});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Color statusColor;
|
||||
|
||||
switch (purchase['status']) {
|
||||
case 'Completed':
|
||||
statusColor = AppColor.success;
|
||||
break;
|
||||
case 'Pending':
|
||||
statusColor = AppColor.warning;
|
||||
break;
|
||||
case 'Cancelled':
|
||||
statusColor = AppColor.error;
|
||||
break;
|
||||
default:
|
||||
statusColor = AppColor.textSecondary;
|
||||
}
|
||||
return AnimatedContainer(
|
||||
duration: Duration(milliseconds: 300 + (index * 50)),
|
||||
curve: Curves.easeOutCubic,
|
||||
margin: const EdgeInsets.only(bottom: 16),
|
||||
child: Material(
|
||||
elevation: 0,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [AppColor.surface, AppColor.surface.withOpacity(0.95)],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(
|
||||
color: AppColor.border.withOpacity(0.2),
|
||||
width: 1,
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: AppColor.primary.withOpacity(0.08),
|
||||
blurRadius: 25,
|
||||
offset: const Offset(0, 10),
|
||||
spreadRadius: 0,
|
||||
),
|
||||
BoxShadow(
|
||||
color: AppColor.black.withOpacity(0.04),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 6,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: AppColor.primaryGradient,
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: AppColor.primary.withOpacity(0.3),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Text(
|
||||
purchase['id'],
|
||||
style: AppStyle.sm.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColor.textWhite,
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 8,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
statusColor.withOpacity(0.15),
|
||||
statusColor.withOpacity(0.05),
|
||||
],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(
|
||||
color: statusColor.withOpacity(0.2),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 8,
|
||||
height: 8,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: statusColor,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
purchase['status'],
|
||||
style: AppStyle.xs.copyWith(
|
||||
color: statusColor,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.background.withOpacity(0.5),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: AppColor.border.withOpacity(0.3),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.primary.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(
|
||||
LineIcons.building,
|
||||
color: AppColor.primary,
|
||||
size: 16,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
purchase['supplier'],
|
||||
style: AppStyle.md.copyWith(
|
||||
color: AppColor.textPrimary,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.info.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(
|
||||
LineIcons.calendar,
|
||||
color: AppColor.info,
|
||||
size: 16,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
purchase['date'],
|
||||
style: AppStyle.sm.copyWith(
|
||||
color: AppColor.textSecondary,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.secondary.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
LineIcons.shoppingBag,
|
||||
color: AppColor.secondary,
|
||||
size: 14,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'${purchase['items']} ${context.lang.items}',
|
||||
style: AppStyle.xs.copyWith(
|
||||
color: AppColor.secondary,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
context.lang.total_purchase,
|
||||
style: AppStyle.xs.copyWith(
|
||||
color: AppColor.textSecondary,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Rp ${purchase['total'].toString().replaceAllMapped(RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'), (Match m) => '${m[1]}.')}',
|
||||
style: AppStyle.lg.copyWith(
|
||||
fontWeight: FontWeight.w800,
|
||||
color: AppColor.primary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
_buildActionButton(LineIcons.eye, AppColor.info, () {}),
|
||||
const SizedBox(width: 8),
|
||||
_buildActionButton(
|
||||
LineIcons.edit,
|
||||
AppColor.warning,
|
||||
() {},
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
_buildActionButton(
|
||||
LineIcons.trash,
|
||||
AppColor.error,
|
||||
() {},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildActionButton(
|
||||
IconData icon,
|
||||
Color color,
|
||||
VoidCallback onPressed,
|
||||
) {
|
||||
return Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [color.withOpacity(0.15), color.withOpacity(0.05)],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: color.withOpacity(0.2), width: 1),
|
||||
),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
onTap: onPressed,
|
||||
child: Center(child: Icon(icon, color: color, size: 18)),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ class PurchaseStatCard extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final Color iconColor;
|
||||
final Animation<double> cardAnimation;
|
||||
|
||||
const PurchaseStatCard({
|
||||
super.key,
|
||||
required this.title,
|
||||
@@ -25,65 +26,41 @@ class PurchaseStatCard extends StatelessWidget {
|
||||
return Transform.scale(
|
||||
scale: 0.8 + (cardAnimation.value * 0.2),
|
||||
child: Opacity(
|
||||
opacity: cardAnimation.value,
|
||||
opacity: cardAnimation.value.clamp(0.0, 1.0),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [AppColor.surface, AppColor.surface.withOpacity(0.9)],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
color: AppColor.surface,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: AppColor.primary.withOpacity(0.1),
|
||||
blurRadius: 20,
|
||||
offset: const Offset(0, 8),
|
||||
spreadRadius: 0,
|
||||
color: AppColor.primary.withOpacity(0.08),
|
||||
blurRadius: 16,
|
||||
offset: const Offset(0, 6),
|
||||
),
|
||||
BoxShadow(
|
||||
color: AppColor.black.withOpacity(0.05),
|
||||
blurRadius: 10,
|
||||
color: AppColor.black.withOpacity(0.04),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
border: Border.all(
|
||||
color: AppColor.border.withOpacity(0.3),
|
||||
color: AppColor.border.withOpacity(0.25),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
iconColor.withOpacity(0.15),
|
||||
iconColor.withOpacity(0.05),
|
||||
],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: iconColor.withOpacity(0.2),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Icon(icon, color: iconColor, size: 24),
|
||||
),
|
||||
],
|
||||
Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: iconColor.withOpacity(0.12),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Icon(icon, color: iconColor, size: 22),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
title,
|
||||
style: AppStyle.sm.copyWith(
|
||||
@@ -91,14 +68,16 @@ class PurchaseStatCard extends StatelessWidget {
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
value,
|
||||
style: AppStyle.lg.copyWith(
|
||||
style: AppStyle.md.copyWith(
|
||||
fontWeight: FontWeight.w800,
|
||||
color: AppColor.textPrimary,
|
||||
height: 1.2,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../../common/theme/theme.dart';
|
||||
|
||||
class PurchaseStatusChip extends StatelessWidget {
|
||||
final bool isSelected;
|
||||
final String text;
|
||||
final Function(bool) onSelected;
|
||||
const PurchaseStatusChip({
|
||||
super.key,
|
||||
required this.isSelected,
|
||||
required this.text,
|
||||
required this.onSelected,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(right: 8),
|
||||
child: FilterChip(
|
||||
selected: isSelected,
|
||||
label: Text(
|
||||
text,
|
||||
style: AppStyle.sm.copyWith(
|
||||
color: isSelected ? AppColor.textWhite : AppColor.textSecondary,
|
||||
),
|
||||
),
|
||||
backgroundColor: AppColor.backgroundLight,
|
||||
selectedColor: AppColor.primary,
|
||||
onSelected: onSelected,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:line_icons/line_icons.dart';
|
||||
|
||||
import '../../../../common/extension/extension.dart';
|
||||
import '../../../../common/theme/theme.dart';
|
||||
import '../../../../domain/analytic/analytic.dart';
|
||||
|
||||
class PurchaseVendorCard extends StatelessWidget {
|
||||
final PurchasingVendorData data;
|
||||
final int index;
|
||||
final Animation<double> animation;
|
||||
|
||||
const PurchaseVendorCard({
|
||||
super.key,
|
||||
required this.data,
|
||||
required this.index,
|
||||
required this.animation,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FadeTransition(
|
||||
opacity: animation,
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.surface,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(color: AppColor.border.withOpacity(0.25)),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.04),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 3),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// Rank badge
|
||||
Container(
|
||||
width: 36,
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
colors: AppColor.primaryGradient,
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
'${index + 1}',
|
||||
style: AppStyle.sm.copyWith(
|
||||
color: AppColor.textWhite,
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
|
||||
// Vendor icon
|
||||
Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.info.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Icon(
|
||||
LineIcons.truck,
|
||||
color: AppColor.info,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
|
||||
// Name & details
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
data.vendorName.trim(),
|
||||
style: AppStyle.md.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColor.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
_Chip(
|
||||
label: '${data.purchaseOrderCount} PO',
|
||||
color: AppColor.info,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
_Chip(
|
||||
label: '${data.ingredientCount} bahan',
|
||||
color: AppColor.secondary,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
_Chip(
|
||||
label: '${data.quantity} pcs',
|
||||
color: AppColor.warning,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Total cost
|
||||
Text(
|
||||
data.totalCost.currencyFormatRp,
|
||||
style: AppStyle.md.copyWith(
|
||||
fontWeight: FontWeight.w800,
|
||||
color: AppColor.primary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Chip extends StatelessWidget {
|
||||
final String label;
|
||||
final Color color;
|
||||
|
||||
const _Chip({required this.label, required this.color});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
label,
|
||||
style: AppStyle.xs.copyWith(
|
||||
color: color,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -501,7 +501,7 @@ class PurchaseRoute extends _i26.PageRouteInfo<void> {
|
||||
static _i26.PageInfo page = _i26.PageInfo(
|
||||
name,
|
||||
builder: (data) {
|
||||
return const _i21.PurchasePage();
|
||||
return _i26.WrappedRoute(child: const _i21.PurchasePage());
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import connectivity_plus
|
||||
import device_info_plus
|
||||
import file_selector_macos
|
||||
import firebase_core
|
||||
import firebase_crashlytics
|
||||
import firebase_messaging
|
||||
import flutter_local_notifications
|
||||
import open_file_mac
|
||||
@@ -23,6 +24,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
|
||||
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
|
||||
FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin"))
|
||||
FLTFirebaseCrashlyticsPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCrashlyticsPlugin"))
|
||||
FLTFirebaseMessagingPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseMessagingPlugin"))
|
||||
FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin"))
|
||||
OpenFilePlugin.register(with: registry.registrar(forPlugin: "OpenFilePlugin"))
|
||||
|
||||
+30
-14
@@ -13,10 +13,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: _flutterfire_internals
|
||||
sha256: "37a42d06068e2fe3deddb2da079a8c4d105f241225ba27b7122b37e9865fd8f7"
|
||||
sha256: ff0a84a2734d9e1089f8aedd5c0af0061b82fb94e95260d943404e0ef2134b11
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.35"
|
||||
version: "1.3.59"
|
||||
analyzer:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -461,50 +461,66 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: firebase_core
|
||||
sha256: "26de145bb9688a90962faec6f838247377b0b0d32cc0abecd9a4e43525fc856c"
|
||||
sha256: "7be63a3f841fc9663342f7f3a011a42aef6a61066943c90b1c434d79d5c995c5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.32.0"
|
||||
version: "3.15.2"
|
||||
firebase_core_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_core_platform_interface
|
||||
sha256: "8bcfad6d7033f5ea951d15b867622a824b13812178bfec0c779b9d81de011bbb"
|
||||
sha256: "0ecda14c1bfc9ed8cac303dd0f8d04a320811b479362a9a4efb14fd331a473ce"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.4.2"
|
||||
version: "6.0.3"
|
||||
firebase_core_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_core_web
|
||||
sha256: eb3afccfc452b2b2075acbe0c4b27de62dd596802b4e5e19869c1e926cbb20b3
|
||||
sha256: "0ed0dc292e8f9ac50992e2394e9d336a0275b6ae400d64163fdf0a8a8b556c37"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.24.0"
|
||||
version: "2.24.1"
|
||||
firebase_crashlytics:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: firebase_crashlytics
|
||||
sha256: "662ae6443da91bca1fb0be8aeeac026fa2975e8b7ddfca36e4d90ebafa35dde1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.3.10"
|
||||
firebase_crashlytics_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_crashlytics_platform_interface
|
||||
sha256: "7222a8a40077c79f6b8b3f3439241c9f2b34e9ddfde8381ffc512f7b2e61f7eb"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.8.10"
|
||||
firebase_messaging:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: firebase_messaging
|
||||
sha256: "980259425fa5e2afc03e533f33723335731d21a56fd255611083bceebf4373a8"
|
||||
sha256: "60be38574f8b5658e2f22b7e311ff2064bea835c248424a383783464e8e02fcc"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "14.7.10"
|
||||
version: "15.2.10"
|
||||
firebase_messaging_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_messaging_platform_interface
|
||||
sha256: "87c4a922cb6f811cfb7a889bdbb3622702443c52a0271636cbc90d813ceac147"
|
||||
sha256: "685e1771b3d1f9c8502771ccc9f91485b376ffe16d553533f335b9183ea99754"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.5.37"
|
||||
version: "4.6.10"
|
||||
firebase_messaging_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_messaging_web
|
||||
sha256: "90dc7ed885e90a24bb0e56d661d4d2b5f84429697fd2cbb9e5890a0ca370e6f4"
|
||||
sha256: "0d1be17bc89ed3ff5001789c92df678b2e963a51b6fa2bdb467532cc9dbed390"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.5.18"
|
||||
version: "3.10.10"
|
||||
fixnum:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
+4
-3
@@ -3,7 +3,7 @@ description: "A new Flutter project."
|
||||
|
||||
publish_to: "none"
|
||||
|
||||
version: 1.0.0+1
|
||||
version: 1.0.2+6
|
||||
|
||||
environment:
|
||||
sdk: ^3.8.1
|
||||
@@ -48,8 +48,9 @@ dependencies:
|
||||
pdf: ^3.11.3
|
||||
open_file: ^3.5.10
|
||||
permission_handler: ^12.0.1
|
||||
firebase_core: 2.32.0
|
||||
firebase_messaging: 14.7.10
|
||||
firebase_core: ^3.13.0
|
||||
firebase_messaging: ^15.2.5
|
||||
firebase_crashlytics: ^4.3.5
|
||||
flutter_local_notifications: ^18.0.1
|
||||
hugeicons: ^1.1.6
|
||||
|
||||
|
||||
Reference in New Issue
Block a user